Compare commits

..

No commits in common. "main" and "feature-navigation" have entirely different histories.

219 changed files with 5349 additions and 19561 deletions

2
.gitignore vendored
View File

@ -27,11 +27,11 @@ migrate_working_dir/
**/doc/api/
**/ios/Flutter/.last_build_id
.dart_tool/
.flutter-plugins
.flutter-plugins-dependencies
.pub-cache/
.pub/
/build/
/coverage/
# Symbolication related
app.*.symbols

View File

@ -4,7 +4,7 @@
# This file should be version controlled and should not be manually edited.
version:
revision: "a0e9b9dbf78c8a5ef39b45a7efd40ed2de19c1a7"
revision: "ea121f8859e4b13e47a8f845e4586164519588bc"
channel: "stable"
project_type: app
@ -13,11 +13,26 @@ project_type: app
migration:
platforms:
- platform: root
create_revision: a0e9b9dbf78c8a5ef39b45a7efd40ed2de19c1a7
base_revision: a0e9b9dbf78c8a5ef39b45a7efd40ed2de19c1a7
create_revision: ea121f8859e4b13e47a8f845e4586164519588bc
base_revision: ea121f8859e4b13e47a8f845e4586164519588bc
- platform: android
create_revision: ea121f8859e4b13e47a8f845e4586164519588bc
base_revision: ea121f8859e4b13e47a8f845e4586164519588bc
- platform: ios
create_revision: ea121f8859e4b13e47a8f845e4586164519588bc
base_revision: ea121f8859e4b13e47a8f845e4586164519588bc
- platform: linux
create_revision: ea121f8859e4b13e47a8f845e4586164519588bc
base_revision: ea121f8859e4b13e47a8f845e4586164519588bc
- platform: macos
create_revision: a0e9b9dbf78c8a5ef39b45a7efd40ed2de19c1a7
base_revision: a0e9b9dbf78c8a5ef39b45a7efd40ed2de19c1a7
create_revision: ea121f8859e4b13e47a8f845e4586164519588bc
base_revision: ea121f8859e4b13e47a8f845e4586164519588bc
- platform: web
create_revision: ea121f8859e4b13e47a8f845e4586164519588bc
base_revision: ea121f8859e4b13e47a8f845e4586164519588bc
- platform: windows
create_revision: ea121f8859e4b13e47a8f845e4586164519588bc
base_revision: ea121f8859e4b13e47a8f845e4586164519588bc
# User provided section

427
API_MOCK_DATA.md Normal file
View File

@ -0,0 +1,427 @@
# API Mock Data Guide
If you don't have a backend API ready yet, you can use mock data to test the app. This guide shows you how to create and use mock data.
## Creating Mock Data
### Option 1: Modify RouteApiService
Edit `lib/services/api/route_api_service.dart` and replace the API calls with mock data:
```dart
import 'package:uuid/uuid.dart';
Future<List<RouteModel>> getDriverRoutes(String driverId) async {
// Simulate network delay
await Future.delayed(const Duration(seconds: 1));
// Return mock data
return [
RouteModel(
id: 'RT001',
driverId: driverId,
driverName: 'John Doe',
date: DateTime.now(),
status: RouteStatus.notStarted,
totalDistance: 45.5,
estimatedDuration: 120,
vehicleId: 'VH123',
stops: [
StopModel(
id: 'ST001',
customerId: 'C001',
customerName: 'Acme Corporation',
customerPhone: '+1234567890',
location: LocationModel(
latitude: 37.7749,
longitude: -122.4194,
address: '123 Market St, San Francisco, CA 94103',
),
type: StopType.pickup,
status: StopStatus.pending,
scheduledTime: DateTime.now().add(const Duration(hours: 1)),
items: ['Package A', 'Package B', 'Package C'],
orderNumber: 1,
),
StopModel(
id: 'ST002',
customerId: 'C002',
customerName: 'Tech Solutions Inc',
customerPhone: '+1234567891',
location: LocationModel(
latitude: 37.7849,
longitude: -122.4094,
address: '456 Mission St, San Francisco, CA 94105',
),
type: StopType.dropoff,
status: StopStatus.pending,
scheduledTime: DateTime.now().add(const Duration(hours: 2)),
items: ['Package A', 'Package B'],
orderNumber: 2,
),
StopModel(
id: 'ST003',
customerId: 'C003',
customerName: 'Global Supplies Ltd',
customerPhone: '+1234567892',
location: LocationModel(
latitude: 37.7949,
longitude: -122.3994,
address: '789 Howard St, San Francisco, CA 94107',
),
type: StopType.dropoff,
status: StopStatus.pending,
scheduledTime: DateTime.now().add(const Duration(hours: 3)),
items: ['Package C'],
orderNumber: 3,
),
],
),
RouteModel(
id: 'RT002',
driverId: driverId,
driverName: 'John Doe',
date: DateTime.now().add(const Duration(days: 1)),
status: RouteStatus.notStarted,
totalDistance: 32.8,
estimatedDuration: 90,
vehicleId: 'VH123',
stops: [
StopModel(
id: 'ST004',
customerId: 'C004',
customerName: 'Downtown Retail',
customerPhone: '+1234567893',
location: LocationModel(
latitude: 37.7649,
longitude: -122.4294,
address: '321 Broadway, San Francisco, CA 94133',
),
type: StopType.pickup,
status: StopStatus.pending,
scheduledTime: DateTime.now().add(const Duration(days: 1, hours: 1)),
items: ['Box 1', 'Box 2'],
orderNumber: 1,
),
],
),
];
}
Future<RouteModel?> getRouteById(String routeId) async {
await Future.delayed(const Duration(seconds: 1));
final routes = await getDriverRoutes('driver_1');
return routes.firstWhere(
(route) => route.id == routeId,
orElse: () => routes.first,
);
}
Future<bool> updateRouteStatus(String routeId, RouteStatus status) async {
await Future.delayed(const Duration(milliseconds: 500));
// Simulate successful update
return true;
}
Future<bool> updateStopStatus(
String routeId,
String stopId,
StopStatus status, {
String? signature,
String? photo,
String? notes,
}) async {
await Future.delayed(const Duration(milliseconds: 500));
// Simulate successful update
return true;
}
Future<bool> reportIssue(String routeId, String stopId, String issue) async {
await Future.delayed(const Duration(milliseconds: 500));
// Simulate successful report
return true;
}
```
### Option 2: Create a Mock Provider
Create a separate mock service that you can easily swap:
1. Create `lib/services/api/mock_route_api_service.dart`:
```dart
import 'route_api_service.dart';
import '../../features/routes/data/models/route_model.dart';
import '../../features/routes/data/models/stop_model.dart';
import '../../features/routes/data/models/location_model.dart';
class MockRouteApiService extends RouteApiService {
final List<RouteModel> _mockRoutes = [
// Add your mock routes here
];
@override
Future<List<RouteModel>> getDriverRoutes(String driverId) async {
await Future.delayed(const Duration(seconds: 1));
return _mockRoutes;
}
@override
Future<RouteModel?> getRouteById(String routeId) async {
await Future.delayed(const Duration(seconds: 1));
return _mockRoutes.firstWhere(
(route) => route.id == routeId,
orElse: () => _mockRoutes.first,
);
}
@override
Future<bool> updateRouteStatus(String routeId, RouteStatus status) async {
await Future.delayed(const Duration(milliseconds: 500));
return true;
}
@override
Future<bool> updateStopStatus(
String routeId,
String stopId,
StopStatus status, {
String? signature,
String? photo,
String? notes,
}) async {
await Future.delayed(const Duration(milliseconds: 500));
return true;
}
@override
Future<bool> reportIssue(String routeId, String stopId, String issue) async {
await Future.delayed(const Duration(milliseconds: 500));
return true;
}
}
```
2. Use it in `main.dart`:
```dart
void main() {
// Use mock service for development
// RouteApiService().initialize(); // Production
// In development, the provider will use mock data automatically
runApp(const FleetDriverApp());
}
```
## Mock Data Generator
Here's a utility to generate random mock data for testing:
```dart
import 'dart:math';
import 'package:uuid/uuid.dart';
class MockDataGenerator {
static final _random = Random();
static final _uuid = Uuid();
static final List<String> _customerNames = [
'Acme Corporation',
'Tech Solutions Inc',
'Global Supplies Ltd',
'Downtown Retail',
'Bay Area Logistics',
'Pacific Trading Co',
'Metro Wholesale',
'Summit Distribution',
];
static final List<String> _addresses = [
'123 Market St, San Francisco, CA',
'456 Mission St, San Francisco, CA',
'789 Howard St, San Francisco, CA',
'321 Broadway, San Francisco, CA',
'654 Valencia St, San Francisco, CA',
'987 Geary Blvd, San Francisco, CA',
];
static final List<String> _items = [
'Package A',
'Package B',
'Package C',
'Box 1',
'Box 2',
'Crate 1',
'Pallet A',
'Container X',
];
static RouteModel generateRoute({
required String driverId,
int stopCount = 3,
}) {
final stops = List.generate(
stopCount,
(index) => generateStop(orderNumber: index + 1),
);
return RouteModel(
id: 'RT${_random.nextInt(9999).toString().padLeft(4, '0')}',
driverId: driverId,
driverName: 'Driver ${_random.nextInt(100)}',
date: DateTime.now().add(Duration(days: _random.nextInt(7))),
status: RouteStatus.values[_random.nextInt(RouteStatus.values.length)],
stops: stops,
totalDistance: 20.0 + _random.nextDouble() * 80,
estimatedDuration: 60 + _random.nextInt(180),
vehicleId: 'VH${_random.nextInt(999)}',
);
}
static StopModel generateStop({required int orderNumber}) {
return StopModel(
id: _uuid.v4(),
customerId: 'C${_random.nextInt(9999)}',
customerName: _customerNames[_random.nextInt(_customerNames.length)],
customerPhone: '+1${_random.nextInt(999999999).toString().padLeft(9, '0')}',
location: LocationModel(
latitude: 37.7749 + (_random.nextDouble() - 0.5) * 0.1,
longitude: -122.4194 + (_random.nextDouble() - 0.5) * 0.1,
address: _addresses[_random.nextInt(_addresses.length)],
),
type: _random.nextBool() ? StopType.pickup : StopType.dropoff,
status: StopStatus.values[_random.nextInt(StopStatus.values.length)],
scheduledTime: DateTime.now().add(Duration(hours: orderNumber)),
items: List.generate(
1 + _random.nextInt(4),
(_) => _items[_random.nextInt(_items.length)],
),
orderNumber: orderNumber,
);
}
static List<RouteModel> generateRoutes({
required String driverId,
int count = 5,
}) {
return List.generate(
count,
(_) => generateRoute(driverId: driverId),
);
}
}
```
### Usage:
```dart
// In route_api_service.dart
Future<List<RouteModel>> getDriverRoutes(String driverId) async {
await Future.delayed(const Duration(seconds: 1));
return MockDataGenerator.generateRoutes(driverId: driverId, count: 5);
}
```
## Testing with Mock Data
### Local Testing
1. Use mock data during development
2. Test all UI states (loading, error, empty, success)
3. Test edge cases (no routes, single route, many routes)
### State Testing
Test different route and stop statuses:
- Routes: notStarted, inProgress, completed, cancelled
- Stops: pending, inProgress, completed, failed
### Example Test Scenarios
```dart
// Test empty state
Future<List<RouteModel>> getDriverRoutes(String driverId) async {
return [];
}
// Test error state
Future<List<RouteModel>> getDriverRoutes(String driverId) async {
throw Exception('Network error');
}
// Test single route
Future<List<RouteModel>> getDriverRoutes(String driverId) async {
return [MockDataGenerator.generateRoute(driverId: driverId)];
}
// Test many routes
Future<List<RouteModel>> getDriverRoutes(String driverId) async {
return MockDataGenerator.generateRoutes(driverId: driverId, count: 20);
}
```
## Switching to Real API
When your backend is ready:
1. Update API base URL in `lib/core/constants/app_constants.dart`:
```dart
static const String baseApiUrl = 'https://your-api-url.com/api';
```
2. Remove mock data from `route_api_service.dart`
3. Ensure your API returns data in the expected format (matching the models)
4. Test with real API:
```bash
flutter run --dart-define=API_URL=https://your-api-url.com/api
```
## API Response Format
Your backend should return JSON in this format:
### Get Routes Response
```json
[
{
"id": "RT001",
"driverId": "driver_1",
"driverName": "John Doe",
"date": "2025-10-27T10:00:00Z",
"status": "notStarted",
"totalDistance": 45.5,
"estimatedDuration": 120,
"vehicleId": "VH123",
"stops": [
{
"id": "ST001",
"customerId": "C001",
"customerName": "Acme Corporation",
"customerPhone": "+1234567890",
"location": {
"latitude": 37.7749,
"longitude": -122.4194,
"address": "123 Market St, San Francisco, CA"
},
"type": "pickup",
"status": "pending",
"scheduledTime": "2025-10-27T11:00:00Z",
"items": ["Package A", "Package B"],
"orderNumber": 1
}
]
}
]
```
## Tips for Mock Data
1. **Use realistic data**: Coordinates, addresses, names
2. **Test edge cases**: Empty lists, null values, long strings
3. **Simulate delays**: Add realistic network delays
4. **Test errors**: Simulate network failures and API errors
5. **Use different statuses**: Test all possible states
6. **Generate varied data**: Different route lengths, stop counts
7. **Include optional fields**: Test with and without optional data

View File

@ -1,293 +0,0 @@
# Plan B Logistics Flutter App - Implementation Checklist
## Core Architecture & Setup
- [x] CQRS API client with Result<T> pattern
- [x] Strict typing (no `dynamic` or untyped `var`)
- [x] Serializable interface for all models
- [x] Error handling with ApiError types
- [x] HTTP client configuration
- [x] API base URLs (query and command endpoints)
- [x] Riverpod state management setup
- [x] Provider architecture
- [x] Responsive utilities and breakpoints
- [x] Theme configuration (Svrnty design system)
- [x] Material Design 3 implementation
- [x] Dark and light themes
## Authentication & Authorization
- [x] Password Credentials OAuth2 flow with Keycloak
- [x] Username/password login form
- [x] JWT token management
- [x] Secure token storage (flutter_secure_storage)
- [x] Token validation and expiration checking
- [x] User profile decoding from JWT
- [x] Authentication guard in main.dart
- [x] Login page UI with form validation
- [ ] Automatic token refresh on expiration
- [ ] Handle 401 responses with token refresh retry
- [ ] Implement logout with token revocation
## Data Models
- [x] Delivery model
- [x] DeliveryRoute model (updated to match API)
- [x] DeliveryAddress model
- [x] DeliveryContact model
- [x] DeliveryOrder model
- [x] UserInfo model
- [x] UserProfile model
- [x] Command models (CompleteDelivery, MarkAsUncompleted, etc.)
- [x] Query models with Serializable
- [x] All models implement fromJson/toJson
## API Integration
- [x] Remove mock data from providers
- [x] Get delivery routes endpoint
- [x] Get deliveries by route endpoint
- [x] Complete delivery command endpoint
- [x] Mark delivery as uncompleted endpoint
- [x] Bearer token injection in API requests
- [x] Query parameter serialization
- [ ] Upload delivery picture endpoint
- [ ] Skip delivery command endpoint
- [ ] Implement pagination for routes
- [ ] Implement pagination for deliveries
- [ ] Add pull-to-refresh functionality
- [ ] Implement retry logic for failed requests
## Pages & UI Components
### Completed Pages
- [x] Login page with username/password
- [x] Routes page (list/grid view)
- [x] Deliveries page (To Do/Delivered segments)
- [x] Settings page
### Pending Pages
- [ ] Delivery details page
- [ ] Photo capture/upload page
- [ ] Error pages (network error, not found, etc.)
### UI Components
- [x] Route card with progress indicator
- [x] Delivery card with status chips
- [x] Bottom sheet for delivery actions
- [x] User profile menu
- [x] Language selector
- [x] Responsive grid/list layouts
- [ ] Extract reusable components to lib/components/
- [ ] Create DeliveryCard component
- [ ] Create RouteCard component
- [ ] Create CustomAppBar component
- [ ] Create LoadingIndicator component
- [ ] Create ErrorView component
- [ ] Create EmptyStateView component
## Features
### Completed Features
- [x] Phone call integration (url_launcher)
- [x] Maps/navigation integration (Google Maps)
- [x] Mark delivery as completed
- [x] Mark delivery as uncompleted
- [x] Language switching (EN/FR)
- [x] Responsive design (mobile/tablet/desktop)
- [x] Pull-to-refresh on routes page
### Pending Features
- [ ] Photo upload for delivery proof
- [ ] Skip delivery with reason
- [ ] View delivery photos
- [ ] Delivery history/timeline
- [ ] Offline mode support
- [ ] Cache management
- [ ] Push notifications
- [ ] Delivery signatures
- [ ] Barcode/QR code scanning
## Internationalization (i18n)
- [x] ARB files setup (English and French)
- [x] 68+ translation keys defined
- [x] Parameterized strings support
- [x] Language provider in state management
- [ ] Replace ALL hardcoded strings in UI with translations
- [ ] Login page strings
- [ ] Routes page strings
- [ ] Deliveries page strings
- [ ] Settings page strings
- [ ] Error messages
- [ ] Button labels
- [ ] Form validation messages
- [ ] Test language switching in all screens
## Error Handling & UX
- [x] Basic error display with SnackBar
- [x] Loading states in providers
- [ ] Comprehensive error handling UI
- [ ] Specific error messages for different ApiErrorType
- [ ] Network connectivity detection
- [ ] Offline mode indicators
- [ ] Retry strategies with exponential backoff
- [ ] Error recovery flows
- [ ] User-friendly error messages
- [ ] Toast notifications for success/error
## Routing & Navigation
- [x] Basic Navigator.push navigation
- [x] Route parameters passing
- [ ] Configure GoRouter
- [ ] Named routes
- [ ] Deep linking support
- [ ] Route guards for authentication
- [ ] Handle back navigation properly
- [ ] Navigation animations/transitions
## Native Features
- [x] Phone calls with url_launcher
- [x] Maps integration with url_launcher
- [ ] Camera access with image_picker
- [ ] Photo gallery access
- [ ] File system access for photos
- [ ] Location services
- [ ] Background location tracking
- [ ] Local notifications
## Testing
- [ ] Unit tests for AuthService
- [ ] Unit tests for CqrsApiClient
- [ ] Unit tests for data models
- [ ] Provider tests with ProviderContainer
- [ ] Widget tests for LoginPage
- [ ] Widget tests for RoutesPage
- [ ] Widget tests for DeliveriesPage
- [ ] Widget tests for SettingsPage
- [ ] Integration tests for auth flow
- [ ] Integration tests for delivery flow
- [ ] Golden tests for UI components
- [ ] Achieve >80% code coverage
## Build Configuration
- [ ] Set up build flavors (dev, staging, prod)
- [ ] Environment-specific configurations
- [ ] API URL configuration per environment
- [ ] App signing for iOS
- [ ] App signing for Android
- [ ] Build scripts for CI/CD
- [ ] Icon and splash screen configuration
- [ ] Version management
- [ ] Build number automation
## Performance Optimization
- [ ] Image caching (cached_network_image)
- [ ] List virtualization optimizations
- [ ] Lazy loading for deliveries
- [ ] Pagination implementation
- [ ] Memory leak detection
- [ ] App size optimization
- [ ] Startup time optimization
- [ ] Frame rate monitoring
## Documentation
- [x] CLAUDE.md project instructions
- [x] README.md with project overview
- [ ] API documentation
- [ ] Component documentation
- [ ] Architecture documentation
- [ ] Deployment guide
- [ ] User manual
- [ ] Developer onboarding guide
## Security
- [x] Secure token storage
- [x] No hardcoded secrets in code (client_secret removed)
- [x] Public client configuration (no client secret in frontend)
- [ ] Certificate pinning
- [ ] Encryption for sensitive data
- [ ] Obfuscation for production builds
- [ ] Security audit
- [ ] Penetration testing
- [ ] OWASP compliance check
## Deployment
- [ ] iOS App Store submission
- [ ] Android Play Store submission
- [ ] Internal testing distribution
- [ ] Beta testing program
- [ ] Production release
- [ ] Crash reporting (Sentry, Firebase Crashlytics)
- [ ] Analytics integration (Firebase Analytics)
- [ ] Remote configuration
## Known Issues to Fix
- [ ] Fix macOS secure storage error (-34018)
- [ ] Update AppAuth deployment target (10.12 -> 10.13)
- [ ] Handle "Failed to foreground app" warning
- [ ] Add proper error boundaries
- [ ] Fix any linter warnings
## Production Blockers (Critical)
- [x] ~~Authentication disabled~~ (FIXED)
- [x] ~~Using mock data~~ (FIXED)
- [ ] Photo upload not implemented
- [ ] Limited error handling
- [ ] No tests written
- [ ] Hardcoded strings instead of i18n
- [ ] No offline support
## Priority Levels
### HIGH PRIORITY (Must have for v1.0)
1. Replace hardcoded strings with i18n translations
2. Implement photo upload feature
3. Create delivery details page
4. Add comprehensive error handling
5. Write critical unit and widget tests
6. Implement automatic token refresh
7. Add skip delivery feature
### MEDIUM PRIORITY (Should have for v1.0)
1. Configure GoRouter with named routes
2. Extract reusable components
3. Implement pagination
4. Add offline mode indicators
5. Set up build flavors
6. Add image caching
### LOW PRIORITY (Nice to have for v1.0)
1. Performance optimizations
2. Golden tests
3. Enhanced animations
4. Advanced features (signatures, barcodes)
5. Push notifications
## Version History
### v0.1.0 (Current)
- Core architecture implemented
- Real API integration completed
- Authentication with Keycloak working
- Basic UI for routes and deliveries
- Phone and maps integration
### v1.0.0 (Target)
- All production blockers resolved
- Complete i18n implementation
- Photo upload working
- Comprehensive error handling
- Test coverage >80%
- Ready for App Store/Play Store submission

425
CLAUDE.md
View File

@ -1,425 +0,0 @@
# CLAUDE.md - Plan B Logistics Flutter App
This file provides guidance to Claude Code when working with this Flutter/Dart project.
## Project Overview
Plan B Logistics Flutter is a complete refactor of the Ionic Angular delivery management app into Flutter/Dart, maintaining all functionality while applying Svrnty design system (colors, typography, and Material Design 3).
**Key Features:**
- OAuth2/OIDC authentication with Keycloak
- CQRS pattern for API integration with Result<T> error handling
- Delivery route and delivery management
- Photo upload for delivery proof
- i18n support (French/English)
- Native features: Camera, Phone calls, Maps
## Essential Commands
### Setup & Dependencies
```bash
flutter pub get
flutter pub upgrade
flutter pub run build_runner build --delete-conflicting-outputs
```
### Development
```bash
flutter run -d chrome # Web
flutter run -d macos # macOS
flutter run -d ios # iOS simulator
flutter run -d android # Android emulator
```
### Testing & Analysis
```bash
flutter test
flutter analyze
flutter test --coverage
```
### Build
```bash
flutter build web
flutter build ios
flutter build android
```
## Code Architecture
### Core Structure
```
lib/
├── api/ # CQRS API client and types
│ ├── types.dart # Result<T>, Serializable, ApiError
│ ├── client.dart # CqrsApiClient implementation
│ └── openapi_config.dart
├── models/ # Data models (strict typing)
│ ├── delivery.dart
│ ├── delivery_route.dart
│ ├── user_profile.dart
│ └── ...
├── services/ # Business logic
│ └── auth_service.dart
├── providers/ # Riverpod state management
│ └── providers.dart
├── pages/ # Screen widgets
│ ├── login_page.dart
│ ├── routes_page.dart
│ ├── deliveries_page.dart
│ └── settings_page.dart
├── components/ # Reusable UI components
├── l10n/ # i18n translations (*.arb files)
├── utils/ # Utility functions
├── theme.dart # Svrnty theme configuration
└── main.dart # App entry point
```
### Design System (Svrnty)
**Primary Colors:**
- Primary (Crimson): #C44D58
- Secondary (Slate Blue): #475C6C
- Error: #BA1A1A
**Typography:**
- Primary Font: Montserrat (all weights 300-700)
- Monospace Font: IBMPlexMono
- Material Design 3 text styles
**Theme Files:**
- `lib/theme.dart` - Complete Material 3 theme configuration
- Light and dark themes with high-contrast variants
- All colors defined in ColorScheme
## Core Patterns & Standards
### 1. Strict Typing (MANDATORY)
**NO `dynamic`, NO untyped `var`**
```dart
// FORBIDDEN:
var data = fetchData();
dynamic result = api.call();
// REQUIRED:
DeliveryRoute data = fetchData();
Result<DeliveryRoute> result = api.call();
```
### 2. Serializable Interface
All models must implement `Serializable`:
```dart
class Delivery implements Serializable {
final int id;
final String name;
const Delivery({required this.id, required this.name});
factory Delivery.fromJson(Map<String, dynamic> json) {
return Delivery(
id: json['id'] as int,
name: json['name'] as String,
);
}
@override
Map<String, Object?> toJson() => {
'id': id,
'name': name,
};
}
```
### 3. Error Handling with Result<T>
**NEVER use try-catch for API calls. Use Result<T> pattern:**
```dart
final result = await apiClient.executeQuery<DeliveryRoute>(
endpoint: 'deliveryRoutes',
query: GetRoutesQuery(),
fromJson: DeliveryRoute.fromJson,
);
result.when(
success: (route) => showRoute(route),
error: (error) {
switch (error.type) {
case ApiErrorType.network:
showSnackbar('No connection');
case ApiErrorType.timeout:
showSnackbar('Request timeout');
case ApiErrorType.validation:
showValidationErrors(error.details);
case ApiErrorType.http when error.statusCode == 401:
navigateToLogin();
default:
showSnackbar('Error: ${error.message}');
}
},
);
```
### 4. CQRS API Integration
**Query (Read Operations):**
```dart
final result = await client.executeQuery<Delivery>(
endpoint: 'simpleDeliveriesQueryItems',
query: GetDeliveriesQuery(routeFragmentId: 123),
fromJson: Delivery.fromJson,
);
```
**Command (Write Operations):**
```dart
final result = await client.executeCommand(
endpoint: 'completeDelivery',
command: CompleteDeliveryCommand(deliveryId: 123),
);
```
### 5. Riverpod State Management
**Providers Pattern:**
```dart
final authServiceProvider = Provider<AuthService>((ref) {
return AuthService();
});
final userProfileProvider = FutureProvider<UserProfile?>((ref) async {
final authService = ref.watch(authServiceProvider);
final token = await authService.getToken();
return token != null ? authService.decodeToken(token) : null;
});
// Usage in widget:
final userProfile = ref.watch(userProfileProvider);
userProfile.when(
data: (profile) => Text(profile?.fullName ?? ''),
loading: () => const CircularProgressIndicator(),
error: (error, stackTrace) => Text('Error: $error'),
);
```
### 6. Authentication Flow
1. **Login**: `AuthService.login()` triggers OAuth2/OIDC flow with Keycloak
2. **Token Storage**: Secure storage with `flutter_secure_storage`
3. **Token Validation**: Check expiration with `JwtDecoder.isExpired()`
4. **Auto Refresh**: Implement token refresh on 401 responses
5. **Logout**: Clear tokens from secure storage
**Keycloak Configuration:**
- Realm: planb-internal
- Client ID: delivery-mobile-app
- Discovery URL: https://auth.goutezplanb.com/realms/planb-internal/.well-known/openid-configuration
- Scopes: openid, profile, offline_access
### 7. No Emojis Rule
**MANDATORY: NO emojis in code, comments, or commit messages**
```dart
// FORBIDDEN:
// Bug fix for delivery issues
void completeDelivery(int id) { ... } // Done
// REQUIRED:
// Bug fix for delivery completion logic
void completeDelivery(int id) { ... }
```
## API Integration
### Base URLs
```dart
const String queryBaseUrl = 'https://api-route.goutezplanb.com/api/query';
const String commandBaseUrl = 'https://api-route.goutezplanb.com/api/command';
```
### Key Endpoints
- Query: `/api/query/simpleDeliveriesQueryItems`
- Query: `/api/query/simpleDeliveryRouteQueryItems`
- Command: `/api/command/completeDelivery`
- Command: `/api/command/markDeliveryAsUncompleted`
- Upload: `/api/delivery/uploadDeliveryPicture`
### Authorization
All requests to API base URL must include Bearer token:
```dart
final authClient = CqrsApiClient(
config: ApiClientConfig(
baseUrl: 'https://api-route.goutezplanb.com',
defaultHeaders: {'Authorization': 'Bearer $token'},
),
);
```
## Internationalization (i18n)
### File Structure
```
lib/l10n/
├── app_en.arb # English translations
└── app_fr.arb # French translations
```
### ARB Format
```json
{
"appTitle": "Plan B Logistics",
"loginButton": "Login with Keycloak",
"deliveryStatus": "Delivery #{id} is {status}",
"@deliveryStatus": {
"placeholders": {
"id": {"type": "int"},
"status": {"type": "String"}
}
}
}
```
### Usage in Code
```dart
AppLocalizations.of(context)!.appTitle
AppLocalizations.of(context)!.deliveryStatus(id: 123, status: 'completed')
```
## Native Features
### Camera Integration
- Package: `image_picker`
- Use: Photo capture for delivery proof
- Platforms: iOS, Android, Web
```dart
final picker = ImagePicker();
final pickedFile = await picker.pickImage(source: ImageSource.camera);
if (pickedFile != null) {
// Upload to server
}
```
### Phone Calls
- Package: `url_launcher`
- Use: Call customer from delivery details
```dart
final Uri phoneUri = Uri(scheme: 'tel', path: phoneNumber);
if (await canLaunchUrl(phoneUri)) {
await launchUrl(phoneUri);
}
```
### Maps Integration
- Package: `url_launcher`
- Use: Open maps app to show delivery address
```dart
final Uri mapUri = Uri(
scheme: 'https',
host: 'maps.google.com',
queryParameters: {'q': '${address.latitude},${address.longitude}'},
);
if (await canLaunchUrl(mapUri)) {
await launchUrl(mapUri);
}
```
## File Naming Conventions
- **Files**: snake_case (e.g., `delivery_route.dart`)
- **Classes**: PascalCase (e.g., `DeliveryRoute`)
- **Variables/Functions**: camelCase (e.g., `deliveryId`, `completeDelivery()`)
- **Constants**: camelCase or UPPER_SNAKE_CASE (e.g., `kPrimaryColor` or `MAX_RETRIES`)
- **Private members**: Prefix with underscore (e.g., `_secureStorage`)
## Git Conventions
- **Author**: Svrnty
- **Co-Author**: Jean-Philippe Brule <jp@svrnty.io>
- **Commits**: Clear, concise messages describing the "why"
- **NO emojis in commits**
Example:
```
Implement OAuth2/OIDC authentication with Keycloak
Adds AuthService with flutter_appauth integration, JWT token
management with secure storage, and automatic token refresh.
Co-Authored-By: Claude <noreply@anthropic.com>
```
## Common Development Tasks
### Adding a New Page
1. Create widget file in `lib/pages/[name]_page.dart`
2. Extend `ConsumerWidget` for Riverpod access
3. Use strict typing for all parameters and variables
4. Apply Svrnty colors from theme
5. Handle loading/error states with `.when()`
### Adding a New Data Model
1. Create in `lib/models/[name].dart`
2. Implement `Serializable` interface
3. Add `fromJson` factory constructor
4. Implement `toJson()` method
5. Use explicit types (no `dynamic`)
### Implementing API Call
1. Create Query/Command class implementing `Serializable`
2. Use `CqrsApiClient.executeQuery()` or `.executeCommand()`
3. Handle Result<T> with `.when()` pattern
4. Never use try-catch for API calls
5. Provide proper error messages to user
### Adding i18n Support
1. Add key to `app_en.arb` and `app_fr.arb`
2. Use `AppLocalizations.of(context)!.keyName` in widgets
3. For parameterized strings, define placeholders in ARB
4. Test both English and French text
## Testing
- Unit tests: `test/` directory
- Widget tests: `test/` directory with widget_test suffix
- Use Riverpod's testing utilities for provider testing
- Mock API client for service tests
- Maintain >80% code coverage for business logic
## Deployment Checklist
- [ ] All strict typing rules followed
- [ ] No `dynamic` or untyped `var`
- [ ] All API calls use Result<T> pattern
- [ ] i18n translations complete for both languages
- [ ] Theme colors correctly applied
- [ ] No emojis in code or commits
- [ ] Tests passing (flutter test)
- [ ] Static analysis clean (flutter analyze)
- [ ] No secrets in code (tokens, keys, credentials)
- [ ] APK/IPA builds successful
## Key Dependencies
- `flutter_riverpod`: State management
- `flutter_appauth`: OAuth2/OIDC
- `flutter_secure_storage`: Token storage
- `jwt_decoder`: JWT token parsing
- `http`: HTTP client
- `image_picker`: Camera/photo access
- `url_launcher`: Phone calls and maps
- `animate_do`: Animations (from Svrnty)
- `lottie`: Loading animations
- `iconsax`: Icon set
- `intl`: Internationalization
## Support & Documentation
- **Theme**: See `lib/theme.dart` for complete Svrnty design system
- **API Types**: See `lib/api/types.dart` for Result<T> and error handling
- **Models**: See `lib/models/` for data structure examples
- **Providers**: See `lib/providers/providers.dart` for state management setup
- **Auth**: See `lib/services/auth_service.dart` for OAuth2/OIDC flow

View File

@ -1,276 +0,0 @@
# Development Guide - Plan B Logistics Flutter App
This guide covers building and running the Plan B Logistics Flutter app on Android devices (KM10) with local backend communication.
## Prerequisites
- Flutter SDK installed and configured
- Android SDK installed (with platform-tools for adb)
- Android device (KM10) connected via USB with USB debugging enabled
- Backend API running on localhost (Mac)
## Device Setup
### 1. Verify Device Connection
Check that your Android device is connected and recognized:
```bash
flutter devices
```
You should see output similar to:
```
KM10 (mobile) • 24117ad4 • android-arm64 • Android 13 (API 33)
```
Alternatively, use adb directly:
```bash
/Users/mathias/Library/Android/sdk/platform-tools/adb devices -l
```
### 2. Configure ADB Reverse Proxy
The app needs to communicate with your local backend API running on `localhost:7182`. Since the Android device cannot access your Mac's localhost directly, you need to set up a reverse proxy using adb.
#### Get Device Serial Number
First, identify your device's serial number:
```bash
/Users/mathias/Library/Android/sdk/platform-tools/adb devices
```
Example output:
```
24117ad4 device
```
#### Set Up Reverse Proxy
Forward the device's localhost:7182 to your Mac's localhost:7182:
```bash
/Users/mathias/Library/Android/sdk/platform-tools/adb -s 24117ad4 reverse tcp:7182 tcp:7182
```
Replace `24117ad4` with your actual device serial number.
#### Verify Reverse Proxy
Check that the reverse proxy is active:
```bash
/Users/mathias/Library/Android/sdk/platform-tools/adb -s 24117ad4 reverse --list
```
Expected output:
```
tcp:7182 tcp:7182
```
#### Test Backend Connectivity (Optional)
From the device shell, test if the backend is accessible:
```bash
/Users/mathias/Library/Android/sdk/platform-tools/adb -s 24117ad4 shell "curl -k https://localhost:7182/api/query/simpleDeliveryRouteQueryItems -X POST -H 'Content-Type: application/json' -d '{}' -m 5 2>&1 | head -10"
```
## Building and Running
### 1. Install Dependencies
```bash
flutter pub get
```
### 2. Run on Connected Device
Run the app on the KM10 device in debug mode:
```bash
flutter run -d KM10
```
Or using the device serial number:
```bash
flutter run -d 24117ad4
```
### 3. Hot Reload and Restart
While the app is running:
- **Hot Reload** (r): Reload changed code without restarting
- **Hot Restart** (R): Restart the entire app
- **Quit** (q): Stop the app
### 4. Build Release APK
To build a release APK:
```bash
flutter build apk --release
```
The APK will be located at:
```
build/app/outputs/flutter-apk/app-release.apk
```
## Development Workflow
### Full Development Session
```bash
# 1. Check device connection
flutter devices
# 2. Set up ADB reverse proxy (do this once per device connection)
/Users/mathias/Library/Android/sdk/platform-tools/adb -s 24117ad4 reverse tcp:7182 tcp:7182
# 3. Verify reverse proxy
/Users/mathias/Library/Android/sdk/platform-tools/adb -s 24117ad4 reverse --list
# 4. Run the app
flutter run -d KM10
```
### If Backend Connection Fails
If the app cannot connect to the backend API:
1. Verify backend is running on Mac:
```bash
curl https://localhost:7182/api/query/simpleDeliveryRouteQueryItems \
-X POST \
-H 'Content-Type: application/json' \
-d '{}'
```
2. Check ADB reverse proxy is active:
```bash
/Users/mathias/Library/Android/sdk/platform-tools/adb -s 24117ad4 reverse --list
```
3. Re-establish reverse proxy if needed:
```bash
/Users/mathias/Library/Android/sdk/platform-tools/adb -s 24117ad4 reverse --remove-all
/Users/mathias/Library/Android/sdk/platform-tools/adb -s 24117ad4 reverse tcp:7182 tcp:7182
```
4. Check device logs for errors:
```bash
/Users/mathias/Library/Android/sdk/platform-tools/adb -s 24117ad4 logcat -s flutter:I -d -t 100
```
## API Configuration
The app is configured to use the following API endpoints:
- **Query Base URL**: `https://localhost:7182/api/query`
- **Command Base URL**: `https://localhost:7182/api/command`
These are configured in `lib/api/openapi_config.dart`.
## Common Commands Reference
### Device Management
```bash
# List all connected devices
flutter devices
# List devices with adb
/Users/mathias/Library/Android/sdk/platform-tools/adb devices -l
# Get device info
/Users/mathias/Library/Android/sdk/platform-tools/adb -s 24117ad4 shell getprop
```
### ADB Reverse Proxy
```bash
# Set up reverse proxy for port 7182
/Users/mathias/Library/Android/sdk/platform-tools/adb -s <DEVICE_ID> reverse tcp:7182 tcp:7182
# List all reverse proxies
/Users/mathias/Library/Android/sdk/platform-tools/adb -s <DEVICE_ID> reverse --list
# Remove specific reverse proxy
/Users/mathias/Library/Android/sdk/platform-tools/adb -s <DEVICE_ID> reverse --remove tcp:7182
# Remove all reverse proxies
/Users/mathias/Library/Android/sdk/platform-tools/adb -s <DEVICE_ID> reverse --remove-all
```
### Logging
```bash
# View Flutter logs (last 100 lines)
/Users/mathias/Library/Android/sdk/platform-tools/adb -s 24117ad4 logcat -s flutter:I -d -t 100
# View Flutter logs in real-time
/Users/mathias/Library/Android/sdk/platform-tools/adb -s 24117ad4 logcat -s flutter:I -v time
# View all logs (last 300 lines)
/Users/mathias/Library/Android/sdk/platform-tools/adb -s 24117ad4 logcat -d -t 300
```
### Build Commands
```bash
# Debug build (default)
flutter build apk --debug
# Release build
flutter build apk --release
# Profile build (for performance testing)
flutter build apk --profile
# Install APK directly
flutter install -d KM10
```
## Troubleshooting
### Device Not Found
If `flutter devices` doesn't show your device:
1. Check USB debugging is enabled on the device
2. Check device is authorized (check device screen for prompt)
3. Restart adb server:
```bash
/Users/mathias/Library/Android/sdk/platform-tools/adb kill-server
/Users/mathias/Library/Android/sdk/platform-tools/adb start-server
```
### App Crashes on Startup
1. Check logs:
```bash
/Users/mathias/Library/Android/sdk/platform-tools/adb -s 24117ad4 logcat -d | grep -i error
```
2. Clear app data:
```bash
/Users/mathias/Library/Android/sdk/platform-tools/adb -s 24117ad4 shell pm clear com.goutezplanb.planb_logistic
```
3. Uninstall and reinstall:
```bash
/Users/mathias/Library/Android/sdk/platform-tools/adb -s 24117ad4 uninstall com.goutezplanb.planb_logistic
flutter run -d KM10
```
### Backend Connection Issues
1. Verify reverse proxy is active
2. Check backend API is running
3. Check SSL certificate issues (app uses `https://localhost:7182`)
4. Review network logs in the Flutter output
## Additional Resources
- Flutter Documentation: https://docs.flutter.dev
- Android Debug Bridge (adb): https://developer.android.com/studio/command-line/adb
- Project-specific guidelines: See CLAUDE.md for code standards and architecture

View File

@ -1,140 +0,0 @@
# Google Maps API Setup Guide
This guide will help you configure Google Maps API keys for the Plan B Logistics app across different platforms.
## Prerequisites
1. Go to [Google Cloud Console](https://console.cloud.google.com/)
2. Create a new project or select an existing one
3. Enable the following APIs:
- Maps SDK for Android
- Maps SDK for iOS
- Maps JavaScript API (for web)
## Get Your API Key
1. Go to [Google Cloud Console - Credentials](https://console.cloud.google.com/apis/credentials)
2. Click "Create Credentials" > "API Key"
3. Copy the API key
4. **IMPORTANT**: Restrict your API key by platform and add restrictions
## Platform-Specific Configuration
### Android
1. Open `android/app/src/main/AndroidManifest.xml`
2. Add the following inside the `<application>` tag:
```xml
<meta-data
android:name="com.google.android.geo.API_KEY"
android:value="YOUR_ANDROID_API_KEY"/>
```
### iOS
1. Open `ios/Runner/AppDelegate.swift`
2. Import GoogleMaps at the top:
```swift
import GoogleMaps
```
3. Add the following in the `application` method before `GeneratedPluginRegistrant.register(with: self)`:
```swift
GMSServices.provideAPIKey("YOUR_IOS_API_KEY")
```
### Web
1. Open `web/index.html`
2. Add the following script tag in the `<head>` section:
```html
<script src="https://maps.googleapis.com/maps/api/js?key=YOUR_WEB_API_KEY"></script>
```
### macOS
1. Open `macos/Runner/AppDelegate.swift`
2. Import GoogleMaps at the top:
```swift
import GoogleMaps
```
3. Add the following in the `applicationDidFinishLaunching` method:
```swift
GMSServices.provideAPIKey("YOUR_MACOS_API_KEY")
```
## API Key Restrictions (Recommended)
For security, restrict your API keys by platform:
### Android API Key
- Application restrictions: Android apps
- Add your package name: `com.goutezplanb.planb_logistic`
- Add SHA-1 certificate fingerprint
### iOS API Key
- Application restrictions: iOS apps
- Add your bundle identifier: `com.goutezplanb.planb-logistic`
### Web API Key
- Application restrictions: HTTP referrers
- Add your domain: `https://yourdomain.com/*`
### macOS API Key
- Application restrictions: macOS apps (if available, otherwise use iOS restrictions)
## Security Best Practices
1. **Never commit API keys to Git**: Add them to `.gitignore` or use environment variables
2. **Use different keys per platform**: This helps track usage and limit damage if a key is compromised
3. **Set up billing alerts**: Monitor API usage to avoid unexpected costs
4. **Enable only required APIs**: Disable unused APIs to reduce attack surface
## Environment Variables (Optional)
For better security, you can use environment variables or secret management:
1. Create a `.env` file (add to `.gitignore`)
2. Store API keys there
3. Use a package like `flutter_dotenv` to load them at runtime
## Testing
After configuration:
1. Restart your Flutter app
2. Navigate to a delivery route
3. The map should load with markers showing delivery locations
4. If you see a blank map or errors, check:
- API key is correctly configured
- Required APIs are enabled in Google Cloud Console
- No console errors in DevTools
## Troubleshooting
### Map shows but is gray
- Check if the API key is valid
- Verify billing is enabled on your Google Cloud project
### "This page can't load Google Maps correctly"
- API key restrictions might be too strict
- Check the browser console for specific error messages
### Markers don't appear
- Verify delivery addresses have valid latitude/longitude
- Check browser/app console for JavaScript errors
## Cost Management
Google Maps offers a generous free tier:
- $200 free credit per month
- Approximately 28,000 map loads per month free
Monitor your usage at: [Google Cloud Console - Billing](https://console.cloud.google.com/billing)

View File

@ -1,346 +0,0 @@
# Google Navigation Flutter Setup Guide
This document provides detailed instructions for completing the Google Navigation Flutter implementation.
## Overview
The implementation includes:
- Location permissions handling with user dialogs
- Google Navigation session management
- Turn-by-turn navigation for delivery destinations
- Terms and Conditions acceptance for navigation services
- i18n support (English/French)
- Proper error handling and logging
## Prerequisites
Before implementing, you need:
1. **Google Cloud Project** with Navigation SDK enabled
2. **API Keys** for both Android and iOS platforms
3. **Configuration** in Android and iOS native files
## Part 1: API Key Configuration
### Android Setup
1. Open `android/app/build.gradle.kts`
2. Add your Android API key to the metadata section in `AndroidManifest.xml`:
```xml
<application>
<meta-data
android:name="com.google.android.geo.API_KEY"
android:value="YOUR_ANDROID_API_KEY" />
</application>
```
Alternatively, use Secrets Gradle Plugin for better security:
```gradle
// In android/app/build.gradle.kts
android {
buildTypes {
debug {
manifestPlaceholders = [googleMapsApiKey: "YOUR_ANDROID_API_KEY"]
}
release {
manifestPlaceholders = [googleMapsApiKey: "YOUR_ANDROID_API_KEY_RELEASE"]
}
}
}
```
Then in `AndroidManifest.xml`:
```xml
<meta-data
android:name="com.google.android.geo.API_KEY"
android:value="${googleMapsApiKey}" />
```
### iOS Setup
1. Open `ios/Runner/AppDelegate.swift`
2. The API key is already configured in the `provideAPIKey()` method:
```swift
import GoogleMaps
@main
@objc class AppDelegate: FlutterAppDelegate {
override func application(
_ application: UIApplication,
didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?
) -> Bool {
GMSServices.provideAPIKey("YOUR_IOS_API_KEY")
GeneratedPluginRegistrant.register(with: self)
return super.application(application, didFinishLaunchingWithOptions: launchOptions)
}
}
```
Replace `YOUR_IOS_API_KEY` with your actual Google Cloud Navigation API key.
## Part 2: Integration with Deliveries Page
To add navigation button to deliveries, update `lib/pages/deliveries_page.dart`:
```dart
import '../pages/navigation_page.dart';
// In your delivery item or action menu:
ElevatedButton(
onPressed: () {
Navigator.of(context).push(
MaterialPageRoute(
builder: (context) => NavigationPage(
delivery: delivery,
destinationLatitude: delivery.latitude,
destinationLongitude: delivery.longitude,
onNavigationComplete: () {
// Handle navigation completion
// Update delivery status
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(
AppLocalizations.of(context)?.navigationArrived ??
'Navigation completed',
),
),
);
},
onNavigationCancelled: () {
// Handle cancellation
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(
AppLocalizations.of(context)?.cancel ?? 'Navigation cancelled',
),
),
);
},
),
),
);
},
child: Text(AppLocalizations.of(context)?.navigateToAddress ?? 'Navigate'),
)
```
## Part 3: Location Permissions
The app uses the `permission_handler` package for location permissions. Permissions are already configured in:
- **Android**: `android/app/src/main/AndroidManifest.xml`
- Required: `INTERNET`, `ACCESS_FINE_LOCATION`, `ACCESS_COARSE_LOCATION`
- Optional background modes for continuous navigation
- **iOS**: `ios/Runner/Info.plist`
- `NSLocationWhenInUseUsageDescription`: When app is active
- `NSLocationAlwaysAndWhenInUseUsageDescription`: Always
- `NSLocationAlwaysUsageDescription`: Background location
- Background mode: "location" enabled
## Part 4: Available Classes and Services
### NavigationPage
Main UI widget for turn-by-turn navigation.
```dart
NavigationPage(
delivery: deliveryObject,
destinationLatitude: 33.5731,
destinationLongitude: -7.5898,
onNavigationComplete: () { /* Handle arrival */ },
onNavigationCancelled: () { /* Handle cancellation */ },
)
```
### LocationPermissionService
Handles location permission requests and checks.
```dart
final permissionService = LocationPermissionService();
// Check current permission status
final hasPermission = await permissionService.hasLocationPermission();
// Request permission
final result = await permissionService.requestLocationPermission();
result.when(
granted: () { /* Permission granted */ },
denied: () { /* Permission denied */ },
permanentlyDenied: () { /* Need to open settings */ },
error: (message) { /* Handle error */ },
);
```
### NavigationSessionService
Manages the Google Navigation session lifecycle.
```dart
final sessionService = NavigationSessionService();
// Initialize session
await sessionService.initializeSession();
// Set controller from the navigation view
await sessionService.setController(navigationViewController);
// Calculate and set route
final route = await sessionService.calculateRoute(
startLatitude: 33.5731,
startLongitude: -7.5898,
destinationLatitude: 33.5745,
destinationLongitude: -7.5850,
);
// Listen to events
sessionService.addArrivalListener((info) {
print('Arrived at destination');
});
sessionService.addLocationListener((location) {
print('Location: ${location.latitude}, ${location.longitude}');
});
// Start/stop navigation
await sessionService.startNavigation();
await sessionService.stopNavigation();
// Cleanup when done
await sessionService.cleanup();
```
### NavigationTermsAndConditionsDialog
Dialog component to show T&C for navigation services.
```dart
showDialog(
context: context,
builder: (context) => NavigationTermsAndConditionsDialog(
onAccept: () {
// Save acceptance and proceed with navigation
},
onDecline: () {
// User declined, don't start navigation
},
),
);
```
## Part 5: Error Handling
The implementation includes comprehensive error handling for:
1. **Location Permission Errors**
- Permission denied
- Permission permanently denied
- System errors
2. **Navigation Initialization Errors**
- Session initialization failure
- Controller not available
- Route calculation failure
3. **Runtime Errors**
- Network issues
- Location acquisition timeout
- Navigation start/stop failures
All errors are displayed through user-friendly dialogs with action buttons.
## Part 6: Internationalization
Navigation strings are available in English and French:
- `navigationTcTitle`, `navigationTcDescription`
- `locationPermissionRequired`, `locationPermissionMessage`
- `navigationArrived`, `navigatingTo`
Add custom translations to `lib/l10n/app_*.arb` files as needed.
## Part 7: Testing Checklist
### Android Testing
- [ ] Test on API level 23+ device
- [ ] Verify minSdk=23 is set
- [ ] Check desugaring is enabled
- [ ] Test location permissions request
- [ ] Verify navigation starts correctly
- [ ] Test with GPS disabled/enabled
- [ ] Verify Terms & Conditions dialog shows
### iOS Testing
- [ ] Test on iOS 16.0+ device
- [ ] Verify Info.plist has all location keys
- [ ] Test location permissions request
- [ ] Verify background location mode is enabled
- [ ] Test navigation with map open
- [ ] Verify arrival notification
- [ ] Check attribution text is visible
### Common Issues and Solutions
**Issue**: "Navigation SDK not available"
- Solution: Verify API key is correctly added and Navigation SDK is enabled in Google Cloud Console
**Issue**: "Location permission always denied"
- Solution: Clear app data and reinstall, or open app settings and manually enable location
**Issue**: "Navigation session fails to initialize"
- Solution: Check that controller is properly created before calling methods
**Issue**: "Routes not calculating"
- Solution: Ensure start and destination coordinates are valid and within service areas
## Part 8: Production Considerations
Before releasing to production:
1. **API Key Security**
- Use separate API keys for Android and iOS
- Restrict API keys by platform and package name
- Rotate keys periodically
2. **Analytics**
- Track navigation start/completion rates
- Monitor location permission denial rates
- Log any navigation errors
3. **User Experience**
- Provide clear instructions for permission requests
- Show progress during initialization
- Handle network failures gracefully
4. **Compliance**
- Ensure proper attribution to Google
- Display Terms & Conditions for navigation
- Comply with EEA data regulations if applicable
## Files Created/Modified
### Created Files
- `lib/services/location_permission_service.dart` - Permission handling
- `lib/services/navigation_session_service.dart` - Session management
- `lib/pages/navigation_page.dart` - Navigation UI
- `lib/components/navigation_tc_dialog.dart` - T&C dialog
### Modified Files
- `android/app/build.gradle.kts` - Added minSdk=23, desugaring
- `ios/Podfile` - iOS configuration (already set)
- `ios/Runner/Info.plist` - Location permissions (updated)
- `lib/l10n/app_en.arb` - English translations
- `lib/l10n/app_fr.arb` - French translations
## Next Steps
1. Add your API keys to Android and iOS configurations
2. Test location permissions flow
3. Integrate navigation button into delivery items
4. Test navigation on real devices
5. Monitor and handle edge cases in production
For more information, refer to:
- [Google Navigation Flutter Documentation](https://developers.google.com/maps/documentation/navigation/mobile-sdk)
- [Flutter Location Permissions](https://pub.dev/packages/permission_handler)
- [Google Cloud Console](https://console.cloud.google.com)

View File

@ -1,268 +0,0 @@
# Google Navigation Flutter - Implementation Summary
## Overview
Complete implementation of Google Navigation Flutter for the Plan B Logistics app with full support for turn-by-turn navigation, location permissions, and internationalization.
## Configuration Changes
### Android (android/app/build.gradle.kts)
- Set `minSdk = 23` (required for Google Navigation)
- Added desugaring dependency for Java NIO support
- Kotlin version already at 2.1.0 (meets 2.0+ requirement)
### iOS (ios/Runner/Info.plist)
- Added `NSLocationAlwaysUsageDescription` for background location tracking
- Background modes already configured for location
- API key already configured in AppDelegate.swift
## New Services Created
### 1. LocationPermissionService (`lib/services/location_permission_service.dart`)
Handles location permission requests with pattern matching:
- `requestLocationPermission()` - Request user permission
- `hasLocationPermission()` - Check current status
- `openAppSettings()` - Open app settings
Returns `LocationPermissionResult` sealed class with states:
- `granted()` - Permission granted
- `denied()` - Permission denied
- `permanentlyDenied()` - Need to open settings
- `error(message)` - System error occurred
### 2. NavigationSessionService (`lib/services/navigation_session_service.dart`)
Singleton service for managing Google Navigation session:
- `initializeSession()` - Initialize navigation session
- `setController(controller)` - Set view controller
- `calculateRoute(...)` - Calculate route from A to B
- `startNavigation()` - Start turn-by-turn guidance
- `stopNavigation()` - Stop current navigation
- `addLocationListener(callback)` - Track location updates
- `addArrivalListener(callback)` - Handle destination arrival
- `addRemainingDistanceListener(callback)` - Track remaining distance
- `cleanup()` - Cleanup resources
Returns `NavigationRoute` with location and route info.
## New UI Components
### 1. NavigationTermsAndConditionsDialog (`lib/components/navigation_tc_dialog.dart`)
Material 3 themed dialog for T&C acceptance:
- Displays navigation service description
- Shows Google Maps attribution
- Accept/Decline buttons with callbacks
- Fully internationalized
### 2. NavigationPage (`lib/pages/navigation_page.dart`)
Complete turn-by-turn navigation screen:
- Full-screen Google Navigation View
- Automatic location permission handling
- Destination markers and route visualization
- Navigation UI controls enabled
- Arrival notifications
- Error handling dialogs
- Loading states with spinners
Features:
- Initializes navigation session
- Requests location permissions if needed
- Sets delivery destination
- Shows T&C dialog on first use
- Handles navigation events (arrival, location updates)
- Provides completion/cancellation callbacks
## Internationalization
Added translation keys for both English and French:
### Navigation Service Keys
- `navigationTcTitle` - Service name
- `navigationTcDescription` - Service description
- `navigationTcAttribution` - Google Maps attribution
- `navigationTcTerms` - Terms acceptance text
### Permission Keys
- `locationPermissionRequired` - Title
- `locationPermissionMessage` - Permission request message
- `locationPermissionDenied` - Denial message
- `permissionPermanentlyDenied` - Title for settings needed
- `openSettingsMessage` - Settings message
- `openSettings` - Open settings button
### Navigation Keys
- `navigationArrived` - Arrival notification
- `navigatingTo` - Navigation header text
- `initializingNavigation` - Loading message
### General Keys
- `accept`, `decline` - Button labels
- `cancel`, `ok`, `requestPermission` - Common buttons
## File Structure
```
lib/
├── services/
│ ├── location_permission_service.dart (NEW)
│ ├── navigation_session_service.dart (NEW)
│ └── auth_service.dart (existing)
├── pages/
│ ├── navigation_page.dart (NEW)
│ ├── deliveries_page.dart (existing)
│ └── ...
├── components/
│ ├── navigation_tc_dialog.dart (NEW)
│ └── ...
└── l10n/
├── app_en.arb (UPDATED)
└── app_fr.arb (UPDATED)
android/
└── app/
└── build.gradle.kts (UPDATED)
ios/
├── Podfile (already configured)
└── Runner/
└── Info.plist (UPDATED)
```
## Key Features Implemented
1. **Location Permissions**
- Runtime permission request with user dialogs
- Handles denied and permanently denied states
- Opens app settings for permanently denied case
- Uses permission_handler package
2. **Navigation Session Management**
- Singleton pattern for session lifecycle
- Route calculation from start to destination
- Event listeners for location and arrival
- Proper error handling with custom exceptions
3. **Turn-by-Turn Navigation**
- Full-screen Google Navigation View
- Real-time location tracking
- Destination arrival notifications
- Navigation UI with zoom and scroll controls
- Marker clustering for multiple waypoints
4. **User Dialogs**
- Location permission request
- T&C acceptance for navigation services
- Error notifications
- Settings access for denied permissions
5. **Error Handling**
- Initialization errors
- Permission errors
- Route calculation failures
- Navigation start/stop errors
- User-friendly error messages
## Integration Steps
To integrate into deliveries page:
```dart
// Add navigation button to delivery item
FloatingActionButton(
onPressed: () => Navigator.of(context).push(
MaterialPageRoute(
builder: (context) => NavigationPage(
delivery: delivery,
destinationLatitude: delivery.latitude,
destinationLongitude: delivery.longitude,
onNavigationComplete: () {
// Update delivery status
ref.refresh(deliveriesProvider(routeFragmentId));
},
),
),
),
child: const Icon(Icons.navigation),
)
```
## Configuration Requirements
Before testing/releasing:
1. **Add API Keys**
- Android: Add to AndroidManifest.xml or build.gradle
- iOS: Update AppDelegate.swift (already configured)
2. **Update AndroidManifest.xml** (if not using build.gradle)
```xml
<meta-data
android:name="com.google.android.geo.API_KEY"
android:value="YOUR_API_KEY" />
```
3. **Verify Permissions** in AndroidManifest.xml
- `INTERNET`
- `ACCESS_FINE_LOCATION`
- `ACCESS_COARSE_LOCATION`
4. **Test on Devices**
- Android 6.0+ (API 23+)
- iOS 16.0+
- Real devices (emulator may have limited GPS)
## Design System Compliance
All components follow Svrnty design system:
- Material 3 theme colors (Primary: #C44D58, Secondary: #475C6C)
- Montserrat typography
- Dark/light theme support
- High contrast variants compatible
## Code Quality
- Strict typing enforced (no `dynamic` or untyped `var`)
- Sealed classes for type-safe pattern matching
- Result pattern for error handling
- Proper resource cleanup in dispose
- Comprehensive null safety
## Testing Recommendations
### Android
- Test on API 23+ device
- Verify GPS works
- Check location permission dialog
- Verify navigation UI displays correctly
- Test arrival notifications
### iOS
- Test on iOS 16.0+ device
- Verify location permission dialog
- Check background location mode
- Test with navigation UI
- Verify arrival notification
## Known Limitations
- Package is in Beta (expect potential breaking changes)
- Don't combine with other Google Maps SDK versions
- EEA developers subject to regional terms (effective July 8, 2025)
- Navigation requires actual GPS for best results
## Documentation
Comprehensive setup guide provided in `GOOGLE_NAVIGATION_SETUP.md` including:
- API key configuration for both platforms
- Integration examples
- Service usage documentation
- Error handling patterns
- Production considerations
- Testing checklist
## Next Steps
1. Add your Google Cloud API keys
2. Test location permissions flow
3. Integrate navigation button into delivery items
4. Test on real Android and iOS devices
5. Monitor navigation start/completion rates
6. Gather user feedback on navigation experience

346
PROJECT_STRUCTURE.md Normal file
View File

@ -0,0 +1,346 @@
# Project Structure Documentation
## Architecture Overview
This project follows a **Feature-First Clean Architecture** approach, organizing code by features rather than layers. This makes the codebase more maintainable and scalable.
## Directory Structure
### `/lib/core/`
Contains shared resources used across the entire application.
#### `/core/constants/`
- **app_constants.dart**: Application-wide constants
- API endpoints
- Configuration values
- App metadata
- Default settings
#### `/core/theme/`
- **app_theme.dart**: Application theme configuration
- Color schemes
- Text styles
- Component themes
- Helper methods for dynamic theming
#### `/core/utils/`
Utility functions and helper classes (to be added as needed)
- Date formatting helpers
- Validation functions
- Common calculations
#### `/core/widgets/`
Reusable widgets used throughout the app
- **status_badge.dart**: Badge widget for displaying status
### `/lib/features/`
Feature-based modules following clean architecture principles.
#### `/features/routes/`
Main feature for route management.
##### `/features/routes/data/`
Data layer - handles data sources and models.
**`/data/models/`**
- **location_model.dart**: Location data with latitude/longitude
- **stop_model.dart**: Stop information (pickup/dropoff)
- **route_model.dart**: Route details with stops and metadata
**`/data/repositories/`**
Implementation of repository interfaces (to be added as needed)
##### `/features/routes/domain/`
Domain layer - business logic and entities.
**`/domain/entities/`**
Pure business objects (if needed, separate from models)
**`/domain/repositories/`**
Repository interfaces/contracts
##### `/features/routes/presentation/`
Presentation layer - UI and state management.
**`/presentation/pages/`**
- **home_page.dart**: Main dashboard showing all routes
- **route_details_page.dart**: Detailed view of a single route
- **map_view_page.dart**: Map view showing all stops
**`/presentation/widgets/`**
Feature-specific widgets:
- **route_card.dart**: Card displaying route summary
- **stop_card.dart**: Card displaying stop information
**`/presentation/providers/`**
State management using Provider:
- **route_provider.dart**: Manages route state and business logic
#### `/features/navigation/`
Navigation-related features (to be expanded)
### `/lib/services/`
Application-wide services.
#### `/services/location/`
- **location_service.dart**: Handles device location
- Get current position
- Track location changes
- Handle permissions
- Calculate distances
#### `/services/maps/`
- **navigation_service.dart**: Google Maps navigation
- Open Google Maps navigation
- Open Apple Maps (fallback)
- Handle navigation URLs
#### `/services/api/`
- **route_api_service.dart**: Backend API communication
- Fetch routes
- Update route status
- Update stop status
- Report issues
### `/lib/main.dart`
Application entry point:
- Initializes services
- Sets up providers
- Configures app theme
- Defines root widget
## Data Flow
```
UI (Pages/Widgets)
Providers (State Management)
Services/Repositories
API/Data Sources
```
### Example: Loading Routes
1. **UI**: `HomePage` requests routes in `initState`
2. **Provider**: `RouteProvider.loadRoutes()` is called
3. **Service**: `RouteApiService.getDriverRoutes()` fetches data
4. **API**: Makes HTTP request to backend
5. **Response**: Data flows back up through the layers
6. **UI**: Provider notifies listeners, UI rebuilds with data
## State Management
Using **Provider** package for state management:
- **ChangeNotifier**: Used in providers to notify UI of changes
- **Consumer**: Used in widgets to listen to provider changes
- **Provider.of / context.read/watch**: Access provider data
### Example Usage
```dart
// In widget
Consumer<RouteProvider>(
builder: (context, routeProvider, child) {
return Text(routeProvider.currentRoute?.id ?? 'No route');
},
)
// Or
final routeProvider = context.watch<RouteProvider>();
// For actions (no rebuild)
context.read<RouteProvider>().loadRoutes(driverId);
```
## Models
### Location Model
```dart
LocationModel {
double latitude
double longitude
String? address
}
```
### Stop Model
```dart
StopModel {
String id
String customerId
String customerName
String? customerPhone
LocationModel location
StopType type // pickup | dropoff
StopStatus status // pending | inProgress | completed | failed
DateTime scheduledTime
DateTime? completedTime
String? notes
List<String> items
int orderNumber
String? signature
String? photo
}
```
### Route Model
```dart
RouteModel {
String id
String driverId
String driverName
DateTime date
RouteStatus status // notStarted | inProgress | completed | cancelled
List<StopModel> stops
double totalDistance
int estimatedDuration
DateTime? startTime
DateTime? endTime
String? vehicleId
String? notes
}
```
## Key Dependencies
### Core Dependencies
- **flutter**: UI framework
- **provider**: State management
- **google_maps_flutter**: Map integration
- **geolocator**: Location services
- **permission_handler**: Handle permissions
### Network & Data
- **dio**: HTTP client
- **url_launcher**: Open external apps
### Utilities
- **intl**: Internationalization and date formatting
- **uuid**: Generate unique identifiers
## Adding New Features
### Steps to Add a New Feature
1. **Create feature directory structure**
```
lib/features/new_feature/
├── data/
│ ├── models/
│ └── repositories/
├── domain/
│ ├── entities/
│ └── repositories/
└── presentation/
├── pages/
├── widgets/
└── providers/
```
2. **Create models** in `data/models/`
3. **Create services** if needed in `lib/services/`
4. **Create provider** in `presentation/providers/`
5. **Create UI** in `presentation/pages/` and `presentation/widgets/`
6. **Register provider** in `main.dart`:
```dart
MultiProvider(
providers: [
ChangeNotifierProvider(create: (_) => RouteProvider()),
ChangeNotifierProvider(create: (_) => NewFeatureProvider()),
],
// ...
)
```
## Best Practices
### Code Organization
- Keep files small and focused
- One class per file
- Use meaningful names
- Group related functionality
### State Management
- Use Provider for global state
- Use StatefulWidget for local state
- Keep business logic in providers
- Keep UI logic in widgets
### Models
- Include `fromJson` and `toJson` methods
- Include `copyWith` method for immutability
- Add computed properties when useful
- Use enums for fixed values
### Services
- Make services singleton where appropriate
- Handle errors gracefully
- Log important operations
- Use async/await for asynchronous operations
### UI
- Extract reusable widgets
- Use const constructors when possible
- Follow Material Design guidelines
- Handle loading and error states
## Testing Strategy
### Unit Tests
- Test models (fromJson, toJson, copyWith)
- Test business logic in providers
- Test utility functions
### Widget Tests
- Test individual widgets
- Test widget interactions
- Test state changes
### Integration Tests
- Test complete user flows
- Test API integration
- Test navigation
## Performance Considerations
### Optimization Tips
- Use `const` constructors where possible
- Avoid rebuilding entire widget trees
- Use `ListView.builder` for long lists
- Optimize images and assets
- Cache network responses
- Use `compute` for heavy computations
### Memory Management
- Dispose controllers and streams
- Cancel subscriptions
- Clear caches when appropriate
- Monitor memory usage
## Future Enhancements
### Planned Features
1. Offline mode with local database
2. Push notifications
3. Real-time tracking
4. Analytics and reporting
5. Multi-language support
6. Dark mode
7. Driver authentication
8. Chat with dispatcher
### Technical Improvements
1. Add unit tests
2. Add integration tests
3. Implement CI/CD
4. Add error tracking (e.g., Sentry)
5. Add analytics (e.g., Firebase Analytics)
6. Implement proper logging
7. Add code generation (e.g., freezed, json_serializable)

189
README.md
View File

@ -1,97 +1,154 @@
# Plan B Logistics - Flutter Mobile App
# Fleet Driver App
A complete Flutter/Dart refactoring of the Plan B Logistics delivery management system. Built with Material Design 3, Svrnty brand colors, and CQRS architecture for type-safe API integration.
A mobile application for drivers to manage delivery routes, handle pickups/dropoffs, and navigate to destinations with integrated Google Maps navigation.
## Overview
## Features
This is a mobile delivery management application for logistics personnel to:
- View assigned delivery routes with progress tracking
- Manage individual deliveries (complete, uncomplete, skip)
- Capture photos as delivery proof
- Call customers and navigate to delivery addresses
- Manage app settings and language preferences
- Secure authentication via OAuth2/OIDC with Keycloak
- View and manage assigned delivery routes
- Track route progress in real-time
- View pickup and dropoff locations with details
- Integrated Google Maps navigation to destinations
- Mark stops as completed
- Real-time location tracking
- Interactive map view of all stops
- Route status management
- Stop status tracking (pending, in progress, completed, failed)
**Built with:**
- Flutter 3.9+ / Dart 3.9.2+
- Material Design 3 with Svrnty theming (Crimson & Slate Blue)
- Riverpod for state management
- CQRS pattern with Result<T> error handling
- Strict typing (no `dynamic`)
## Tech Stack
- **Flutter** - UI framework
- **Provider** - State management
- **Google Maps Flutter** - Map integration
- **Geolocator** - Location services
- **Dio** - HTTP client for API calls
- **URL Launcher** - Navigation integration
## Quick Start
### Prerequisites
1. **Clone the repository**
```bash
git clone <your-repo-url>
cd flutter_fleet_logistic_workforce_app
```
- Flutter SDK 3.9.2+: [Install Flutter](https://flutter.dev/docs/get-started/install)
- Dart SDK 3.9.2+ (included with Flutter)
2. **Install dependencies**
```bash
flutter pub get
```
### Setup
3. **Configure Google Maps API Key**
- Get your API key from [Google Cloud Console](https://console.cloud.google.com/)
- See [SETUP_GUIDE.md](SETUP_GUIDE.md) for detailed configuration steps
```bash
cd ionic-planb-logistic-app-flutter
flutter pub get
```
4. **Run the app**
```bash
flutter run
```
### Run
## Documentation
```bash
flutter run # Android/iOS default device
flutter run -d chrome # Web
flutter run -d ios # iOS simulator
flutter run -d android # Android emulator
```
- **[SETUP_GUIDE.md](SETUP_GUIDE.md)** - Detailed setup and configuration instructions
- **[PROJECT_STRUCTURE.md](PROJECT_STRUCTURE.md)** - Architecture and code organization
## Project Structure
```
lib/
├── api/ # CQRS client & types
├── models/ # Data models
├── services/ # Auth service
├── providers/ # Riverpod state
├── pages/ # Login, Routes, Deliveries, Settings
├── l10n/ # Translations (EN/FR)
├── theme.dart # Svrnty Material Design 3
└── main.dart # Entry point
├── core/ # Shared resources
│ ├── constants/ # App constants
│ ├── theme/ # Theme configuration
│ └── widgets/ # Reusable widgets
├── features/ # Feature modules
│ └── routes/ # Route management feature
│ ├── data/ # Models and repositories
│ ├── domain/ # Business logic
│ └── presentation/ # UI and state management
├── services/ # App services
│ ├── location/ # Location tracking
│ ├── maps/ # Navigation
│ └── api/ # Backend API
└── main.dart # App entry point
```
## Key Features
## Screenshots
- **OAuth2/OIDC Authentication** with Keycloak
- **CQRS API Integration** with Result<T> error handling
- **Riverpod State Management** for reactive UI
- **Internationalization** (English & French)
- **Material Design 3** with Svrnty brand colors
- **Native Features**: Camera, Phone calls, Maps
- **Strict Typing**: No `dynamic` type allowed
Coming soon...
## Requirements
- Flutter SDK >=3.7.2
- Dart SDK
- Android SDK (for Android)
- Xcode (for iOS)
- Google Maps API Key
## Configuration
### Android
Update `android/app/src/main/AndroidManifest.xml` with your Google Maps API key.
### iOS
Update `ios/Runner/AppDelegate.swift` with your Google Maps API key.
See [SETUP_GUIDE.md](SETUP_GUIDE.md) for detailed instructions.
## API Integration
The app is designed to work with a backend API. Configure your API endpoint in:
```dart
lib/core/constants/app_constants.dart
```
Expected API endpoints:
- `GET /routes/:driverId` - Get driver routes
- `GET /routes/detail/:routeId` - Get route details
- `PUT /routes/:routeId/status` - Update route status
- `PUT /stops/:stopId/status` - Update stop status
## Development
See **[CLAUDE.md](CLAUDE.md)** for:
- Detailed architecture & patterns
- Code standards & conventions
- API integration examples
- Development workflow
## Build Commands
### Run in development mode
```bash
flutter build web --release # Web
flutter build ios --release # iOS
flutter build appbundle --release # Android (Play Store)
flutter run
```
## Documentation
### Build for production
```bash
# Android
flutter build apk --release
- **CLAUDE.md** - Complete development guidelines
- **pubspec.yaml** - Dependencies and configuration
- **[Flutter Docs](https://flutter.dev/docs)** - Official documentation
# iOS
flutter build ios --release
```
## Version
### Run tests
```bash
flutter test
```
1.0.0+1
## Future Enhancements
---
- [ ] Offline mode with local storage
- [ ] Photo capture for proof of delivery
- [ ] Digital signature collection
- [ ] Push notifications
- [ ] Route optimization
- [ ] Driver performance metrics
- [ ] Multi-language support
- [ ] Dark mode
Svrnty Edition
## Contributing
1. Fork the repository
2. Create your feature branch (`git checkout -b feature/AmazingFeature`)
3. Commit your changes (`git commit -m 'Add some AmazingFeature'`)
4. Push to the branch (`git push origin feature/AmazingFeature`)
5. Open a Pull Request
## License
This project is proprietary software. All rights reserved.
## Support
For support, please contact your development team or open an issue in the repository.

233
SETUP_GUIDE.md Normal file
View File

@ -0,0 +1,233 @@
# Fleet Driver App - Setup Guide
## Overview
This is a mobile application for drivers to manage their delivery routes, handle pickups/dropoffs, and navigate to destinations using Google Maps integration.
## Prerequisites
- Flutter SDK 3.7.2 or higher
- Dart SDK
- Android Studio / Xcode
- Google Maps API Key
## Getting Started
### 1. Install Dependencies
```bash
flutter pub get
```
### 2. Configure Google Maps API Key
#### Get Your API Key
1. Go to [Google Cloud Console](https://console.cloud.google.com/)
2. Create a new project or select an existing one
3. Enable the following APIs:
- Maps SDK for Android
- Maps SDK for iOS
- Directions API (for navigation)
- Places API (optional, for address autocomplete)
4. Create credentials (API Key)
5. Copy your API key
#### Android Configuration
1. Open `android/app/src/main/AndroidManifest.xml`
2. Replace `YOUR_GOOGLE_MAPS_API_KEY_HERE` with your actual API key:
```xml
<meta-data
android:name="com.google.android.geo.API_KEY"
android:value="YOUR_ACTUAL_API_KEY"/>
```
#### iOS Configuration
1. Open `ios/Runner/AppDelegate.swift`
2. Replace `YOUR_GOOGLE_MAPS_API_KEY_HERE` with your actual API key:
```swift
GMSServices.provideAPIKey("YOUR_ACTUAL_API_KEY")
```
### 3. Configure Backend API (Optional)
If you have a backend API, update the API configuration in:
`lib/core/constants/app_constants.dart`
```dart
static const String baseApiUrl = 'https://your-api-url.com/api';
```
### 4. Run the App
#### Android
```bash
flutter run
```
#### iOS
```bash
cd ios
pod install
cd ..
flutter run
```
## Project Structure
```
lib/
├── core/
│ ├── constants/ # App-wide constants
│ ├── theme/ # App theme and colors
│ ├── utils/ # Utility functions
│ └── widgets/ # Reusable widgets
├── features/
│ ├── routes/
│ │ ├── data/
│ │ │ ├── models/ # Data models
│ │ │ └── repositories/
│ │ ├── domain/
│ │ │ ├── entities/
│ │ │ └── repositories/
│ │ └── presentation/
│ │ ├── pages/ # UI screens
│ │ ├── widgets/ # Feature-specific widgets
│ │ └── providers/ # State management
│ │
│ └── navigation/
├── services/
│ ├── location/ # Location services
│ ├── maps/ # Navigation services
│ └── api/ # API services
└── main.dart # App entry point
```
## Features
### Current Features
1. **Route Management**
- View assigned routes
- Track route progress
- Start/complete routes
2. **Stop Management**
- View pickup and dropoff locations
- Navigate to stops
- Mark stops as completed
- View stop details (customer info, items, etc.)
3. **Map Integration**
- View all stops on a map
- Real-time location tracking
- Navigate to destinations using Google Maps
4. **Status Tracking**
- Route status (not started, in progress, completed)
- Stop status (pending, in progress, completed, failed)
- Progress indicators
### Upcoming Features
- Photo capture for proof of delivery
- Signature collection
- Offline mode
- Push notifications
- Route optimization
- Driver performance metrics
## API Integration
The app is designed to work with a backend API. The main API endpoints expected are:
- `GET /routes/:driverId` - Get driver routes
- `GET /routes/detail/:routeId` - Get route details
- `PUT /routes/:routeId/status` - Update route status
- `PUT /stops/:stopId/status` - Update stop status
- `POST /stops/:stopId/issue` - Report an issue
### Mock Data for Development
If you don't have a backend yet, you can modify the `RouteApiService` to return mock data:
```dart
// In lib/services/api/route_api_service.dart
Future<List<RouteModel>> getDriverRoutes(String driverId) async {
// Return mock data instead of API call
return [
// Your mock route data here
];
}
```
## Permissions
### Android
The following permissions are configured in `AndroidManifest.xml`:
- `INTERNET` - For API calls
- `ACCESS_FINE_LOCATION` - For precise location
- `ACCESS_COARSE_LOCATION` - For approximate location
- `FOREGROUND_SERVICE` - For background location tracking
### iOS
The following permissions are configured in `Info.plist`:
- Location When In Use
- Location Always (for background tracking)
## Troubleshooting
### Google Maps Not Showing
1. Verify your API key is correct
2. Make sure you've enabled the required APIs in Google Cloud Console
3. Check if you've restricted your API key (it should allow your app's package name)
### Location Not Working
1. Check device location settings are enabled
2. Grant location permissions when prompted
3. Test on a real device (emulators may have location issues)
### Build Errors
```bash
# Clean and rebuild
flutter clean
flutter pub get
flutter run
```
## Testing
```bash
# Run tests
flutter test
# Run with coverage
flutter test --coverage
```
## Building for Production
### Android
```bash
flutter build apk --release
# or
flutter build appbundle --release
```
### iOS
```bash
flutter build ios --release
```
## Support
For issues or questions, please contact your development team or refer to the Flutter documentation at https://flutter.dev/docs

View File

@ -1,355 +0,0 @@
# UI/UX Improvements: Apple-Like Polish Implementation
**Date:** November 15, 2025
**Status:** Complete & Tested
**Platform:** iPad Pro 11-inch M4 Simulator (Dark Mode)
---
## Executive Summary
Successfully implemented three premium UI components that transform the app from utilitarian to Apple-like polish. The improvements focus on **visual hierarchy, depth, animations, and dark mode optimization**.
**Impact:**
- Enhanced visual separation through accent bars and elevation
- Smooth animations with proper easing and stagger delays
- Dark mode optimized for evening delivery work (reduced eye strain)
- Professional, refined aesthetic across all screens
---
## Implemented Components
### 1. **PremiumRouteCard** (`lib/components/premium_route_card.dart`)
**Purpose:** Replace basic route cards with premium, information-dense design
**Key Features:**
- **Left accent bar** (4px colored border, Svrnty Red / Green for status)
- **Visual hierarchy** with delivery count badge (red, top-right)
- **Animated hover effects** (1.02x scale + dynamic shadow lift, 200ms)
- **Progress indicators** with percentage text and colored fills
- **Dark mode support** with proper contrast and elevation
**Design Details:**
```dart
- Accent bar color: Status-dependent (Red for pending, Green for completed)
- Shadow: AnimatedBuilder with 2-8px blur radius
- Scale animation: 1.0 → 1.02 on hover (easeOut curve)
- Spacing: 16px padding with 4px internal elements
- Border radius: 12px corners for modern look
```
**File Changes:**
- NEW: `lib/components/premium_route_card.dart` (170 lines)
- MODIFIED: `lib/pages/routes_page.dart` (import + usage)
---
### 2. **DarkModeMapComponent** (`lib/components/dark_mode_map.dart`)
**Purpose:** Google Maps integration with dark theme styling and custom controls
**Key Features:**
- **Dark mode style** with Google Maps JSON configuration
- **Custom info panels** for selected deliveries (top-positioned)
- **Navigation buttons** (bottom-right) with dark backgrounds
- **Status indicators** with color-coded badges
- **Warm accent colors** (Amber/Gold) for pins in dark mode
- **Reduced brightness** for evening use (eye strain reduction)
**Design Details:**
```dart
- Map Style: Comprehensive JSON with dark geometry, gray labels, dark roads
- Info Panel: Backdrop with status badge, address preview, call hint
- Buttons: Rounded corners, dark backgrounds, white icons
- Margins: Safe area aware with 16px padding
- Animations: Smooth camera movements with proper easing
```
**Dark Mode Optimizations:**
- Geometry: `#212121` (dark gray)
- Water: `#0c1221` (dark blue)
- Roads: `#2c2c2c` (darker gray)
- Labels: `#757575` to `#9e9e9e` (medium gray)
- Overall: Reduces contrast for low-light environments
**File Changes:**
- NEW: `lib/components/dark_mode_map.dart` (370 lines)
- MODIFIED: `lib/pages/deliveries_page.dart` (import + usage swap)
---
### 3. **DeliveryListItem** (`lib/components/delivery_list_item.dart`)
**Purpose:** Animated list items with visual status indicators and interaction feedback
**Key Features:**
- **Staggered entrance animations** (50ms delays per item)
- **Left accent bar** (4px, status-colored)
- **Status badges** with icons (checkmark, clock, skip)
- **Contact phone buttons** (inline, for quick calling)
- **Hover states** with background color shift + shadow lift
- **Order amount display** in warm accent color (Amber/Gold)
- **Slide-in animation** (20px offset, 400ms duration, easeOut curve)
**Animation Details:**
```dart
- Entry: SlideTransition (20px right → 0px) + FadeTransition
- Scale: 0.95 → 1.0 (scaleAnimation)
- Duration: 400ms total entry animation
- Stagger: 50ms delay per item index
- Hover: 200ms AnimatedContainer color shift
```
**Status Colors:**
```dart
- Pending: Slate Gray (#506576)
- In Progress: Blue (#3B82F6)
- Completed: Green (#22C55E)
- Skipped: Amber (#F59E0B)
```
**File Changes:**
- NEW: `lib/components/delivery_list_item.dart` (290 lines)
- MODIFIED: `lib/pages/deliveries_page.dart` (import + DeliveryListView integration)
---
## Technical Implementation Details
### Animation System Utilization
Leverages existing `lib/theme/animation_system.dart`:
```dart
AppAnimations.durationFast // 200ms for interactions
AppAnimations.durationNormal // 300ms for transitions
AppAnimations.curveEaseOut // Fast start, slow end
AppAnimations.curveEaseInOut // Smooth both ends
AppAnimations.scaleHover // 1.02x scale multiplier
AppAnimations.staggerDelay // 50ms per item
AppAnimations.offsetSm // 8px slide offset
```
### Color System Integration
Uses `lib/theme/color_system.dart` for semantic colors:
```dart
SvrntyColors.crimsonRed // #DF2D45 (primary accent)
SvrntyColors.statusCompleted // #22C55E (green)
SvrntyColors.statusPending // #506576 (slate gray)
SvrntyColors.statusSkipped // #F59E0B (amber)
```
### Responsive Design
Components are fully responsive using Flutter's native capabilities:
- **Mobile:** Full-width cards/list items
- **Tablet:** 2-column grid for routes, same sidebar layout
- **Desktop:** 3-column grid for routes, optimized sidebar proportions
---
## Testing & Verification
### Environment:
- **Device:** iPad Pro 11-inch (M4) Simulator
- **Orientation:** Landscape (as configured in main.dart)
- **Theme:** Dark mode (forced in main.dart)
- **Flutter:** Hot reload enabled during development
### Test Results:
✅ Route cards display with premium styling
✅ Hover effects trigger smoothly (scale + shadow)
✅ Dark mode map loads without errors
✅ Delivery list items animate on entry
✅ Status badges update correctly
✅ No build errors (only minor linter warnings)
✅ API calls succeed (authentication working)
✅ Dark theme applied throughout
### Build Output:
```
Xcode build done: 18.1s
Syncing files: 75ms
App running on iPad Pro 11-inch (M4)
Map initialization: Successful
API queries: 200 OK (4 routes loaded, 28 deliveries loaded)
```
---
## Before & After Comparison
### Routes Page
**Before:**
- Basic Material cards with no visual distinction
- Plain text labels
- Minimal spacing
- No hover feedback
**After:**
- Premium cards with left accent bars (4px colored borders)
- Delivery count badges (red pills, top-right)
- Better spacing with 16px padding
- Animated hover states (1.02x scale + shadow lift 200ms)
- Progress percentages displayed
- Professional dark mode support
### Deliveries Page
**Before:**
- Basic list items with chip-based status
- No visual hierarchy
- Flat design
- No animations on entry
**After:**
- Accent bar for visual separation
- Status badges with icons (checkmark, clock, skip)
- Staggered animations on entry (50ms delays)
- Contact phone buttons inline
- Total amount displayed in warm colors
- Hover states with smooth transitions
- Slide-in animations (400ms, easeOut)
### Map Component
**Before:**
- Default Google Maps styling (light theme in dark mode)
- No custom dark styling
- Basic info windows
- Generic markers
**After:**
- Dark mode Google Maps (custom JSON styling)
- Optimized for evening use (reduced brightness)
- Custom info panels with delivery details
- Status indicator badges
- Navigation buttons with dark backgrounds
- Delivery name and address preview
- Dark-themed controls (zoom, etc.)
---
## Key Design Decisions
### 1. **Accent Bars Over Full Card Coloring**
- Left 4px accent bar provides status indication without overwhelming
- Better for visual hierarchy and readability
- Allows for rich content within cards
### 2. **Staggered List Animations**
- 50ms delays create sense of cascade
- Indicates dynamic loading without skeleton screens
- Improves perceived performance
### 3. **Hover Scale (1.02x) vs Larger Lift**
- Apple convention: subtle interactions (not dramatic)
- 200ms duration matches material design recommendations
- easeOut curve feels responsive and snappy
### 4. **Dark Map Styling for Delivery Context**
- Evening deliveries common
- Reduces eye strain in low-light environments
- Maintains color contrast for maps while being dark
### 5. **Status Icons + Badges**
- Immediate visual scanning (icons first)
- Text label for confirmation
- Color-coding reinforces meaning
---
## Code Quality
### Metrics:
- **Lines Added:** ~830 (3 new components)
- **Lines Modified:** 20 (routes_page.dart + deliveries_page.dart)
- **Dart Analysis:** 0 errors, 9 warnings (minor deprecations + unused imports)
- **Build Success:** 100%
- **Runtime Errors:** 0
### Best Practices Followed:
✅ Strict typing (no `dynamic` or untyped `var`)
✅ Proper state management (StatefulWidget with lifecycle)
✅ Animation constants reused (AppAnimations)
✅ Color system used (SvrntyColors)
✅ Responsive design patterns
✅ Dark mode support throughout
---
## Files Changed
### New Files (3):
1. `lib/components/premium_route_card.dart` - Route card component
2. `lib/components/dark_mode_map.dart` - Dark mode map wrapper
3. `lib/components/delivery_list_item.dart` - Delivery list item component
### Modified Files (2):
1. `lib/pages/routes_page.dart` - Import + use PremiumRouteCard
2. `lib/pages/deliveries_page.dart` - Import + use DarkModeMapComponent & DeliveryListItem
### Generated/System Files:
- iOS build logs (auto-generated during build)
- Theme files (already existed, no changes)
---
## Next Steps & Recommendations
### Immediate Polish (Low Effort, High Impact):
1. Add skeleton screens for loading states (shimmer effect)
2. Implement success/error animations (checkmark popup, shake)
3. Add haptic feedback on interactions (iOS native)
4. Refine empty state designs
### Medium-Term Improvements:
1. Custom loading spinner (branded with Svrnty Red)
2. Gesture animations (swipe to complete delivery)
3. Micro-interactions on buttons (press down 0.98x scale)
4. Parallax scrolling on route cards
### Long-Term Enhancements:
1. Glassmorphism on iPad sidebars (frosted glass effect)
2. Custom painter for progress indicators
3. Lottie animations for route completion
4. Animated transitions between pages (shared element)
---
## Performance Notes
- **Animation Performance:** 60 FPS maintained (200-400ms animations)
- **Memory Impact:** Minimal (reuses AppAnimations constants)
- **Build Time:** No change (18.1s Xcode build)
- **App Size:** Negligible increase (~10KB for new components)
---
## Accessibility Considerations
✅ High contrast badges for status (WCAG AA)
✅ Semantic icons with accompanying text
✅ Color not only indicator of status (includes text + icons)
✅ Sufficient touch targets (48dp minimum)
✅ Clear visual hierarchy for screen readers
---
## Conclusion
The implementation successfully transforms the Svrnty Plan B Logistics app from a functional utility into a polished, premium-feeling delivery management application. The three new components work together to create:
- **Visual refinement** through accent bars and proper elevation
- **Smooth interactions** with meaningful animations
- **Dark mode excellence** for evening delivery work
- **Apple-like quality** in every interaction
All changes are production-ready, fully tested, and committed to the main branch.
**Status:** ✅ Complete & Ready for Production

View File

@ -6,7 +6,7 @@ plugins {
}
android {
namespace = "com.goutezplanb.planb_logistic"
namespace = "io.svrnty.flutter_fleet_logistic_workforce_app"
compileSdk = flutter.compileSdkVersion
ndkVersion = flutter.ndkVersion
@ -21,21 +21,13 @@ android {
defaultConfig {
// TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html).
applicationId = "com.goutezplanb.planb_logistic"
applicationId = "io.svrnty.flutter_fleet_logistic_workforce_app"
// You can update the following values to match your application needs.
// For more information, see: https://flutter.dev/to/review-gradle-config.
minSdk = flutter.minSdkVersion // Required for Google Navigation Flutter
minSdk = flutter.minSdkVersion
targetSdk = flutter.targetSdkVersion
versionCode = flutter.versionCode
versionName = flutter.versionName
// OAuth redirect scheme for flutter_appauth
manifestPlaceholders["appAuthRedirectScheme"] = "com.goutezplanb.delivery"
}
packagingOptions {
// Enable desugaring for Java NIO support required by Google Navigation SDK
exclude("META-INF/proguard/androidx-*.pro")
}
buildTypes {
@ -47,11 +39,6 @@ android {
}
}
dependencies {
// Desugaring for Java NIO support required by Google Navigation SDK
coreLibraryDesugaring("com.android.tools:desugar_jdk_libs_nio:2.0.4")
}
flutter {
source = "../.."
}

View File

@ -1,11 +1,14 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<!-- Permissions -->
<uses-permission android:name="android.permission.INTERNET"/>
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION"/>
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION"/>
<uses-permission android:name="android.permission.FOREGROUND_SERVICE"/>
<application
android:label="planb_logistic"
android:label="flutter_fleet_logistic_workforce_app"
android:name="${applicationName}"
android:icon="@mipmap/ic_launcher">
<meta-data
android:name="com.google.android.geo.API_KEY"
android:value="AIzaSyCuYzbusLkVrHcy10bJ8STF6gyOexQWjuk" />
<activity
android:name=".MainActivity"
android:exported="true"
@ -33,11 +36,11 @@
<meta-data
android:name="flutterEmbedding"
android:value="2" />
<!-- Disable Impeller (Vulkan) rendering for better GPU compatibility -->
<!-- Use OpenGL rendering instead, which works better with Mali GPUs -->
<!-- Google Maps API Key -->
<meta-data
android:name="io.flutter.embedding.android.EnableImpeller"
android:value="false" />
android:name="com.google.android.geo.API_KEY"
android:value="YOUR_GOOGLE_MAPS_API_KEY_HERE"/>
</application>
<!-- Required to query activities that can process text, see:
https://developer.android.com/training/package-visibility and

View File

@ -1,4 +1,4 @@
package com.goutezplanb.planb_logistic
package io.svrnty.flutter_fleet_logistic_workforce_app
import io.flutter.embedding.android.FlutterActivity

View File

@ -1,7 +1,7 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<!-- Theme applied to the Android Window while the process is starting when the OS's Dark Mode setting is off -->
<style name="LaunchTheme" parent="Theme.AppCompat.Light.NoActionBar">
<style name="LaunchTheme" parent="@android:style/Theme.Light.NoTitleBar">
<!-- Show a splash screen on the activity. Automatically removed when
the Flutter engine draws its first frame -->
<item name="android:windowBackground">@drawable/launch_background</item>
@ -12,7 +12,7 @@
running.
This Theme is only used starting with V2 of Flutter's Android embedding. -->
<style name="NormalTheme" parent="Theme.AppCompat.Light.NoActionBar">
<style name="NormalTheme" parent="@android:style/Theme.Light.NoTitleBar">
<item name="android:windowBackground">?android:colorBackground</item>
</style>
</resources>

View File

@ -5,10 +5,7 @@ allprojects {
}
}
val newBuildDir: Directory =
rootProject.layout.buildDirectory
.dir("../../build")
.get()
val newBuildDir: Directory = rootProject.layout.buildDirectory.dir("../../build").get()
rootProject.layout.buildDirectory.value(newBuildDir)
subprojects {

View File

@ -2,4 +2,4 @@ distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-8.12-all.zip
distributionUrl=https\://services.gradle.org/distributions/gradle-8.10.2-all.zip

View File

@ -1,12 +1,11 @@
pluginManagement {
val flutterSdkPath =
run {
val properties = java.util.Properties()
file("local.properties").inputStream().use { properties.load(it) }
val flutterSdkPath = properties.getProperty("flutter.sdk")
require(flutterSdkPath != null) { "flutter.sdk not set in local.properties" }
flutterSdkPath
}
val flutterSdkPath = run {
val properties = java.util.Properties()
file("local.properties").inputStream().use { properties.load(it) }
val flutterSdkPath = properties.getProperty("flutter.sdk")
require(flutterSdkPath != null) { "flutter.sdk not set in local.properties" }
flutterSdkPath
}
includeBuild("$flutterSdkPath/packages/flutter_tools/gradle")
@ -19,8 +18,8 @@ pluginManagement {
plugins {
id("dev.flutter.flutter-plugin-loader") version "1.0.0"
id("com.android.application") version "8.9.1" apply false
id("org.jetbrains.kotlin.android") version "2.1.0" apply false
id("com.android.application") version "8.7.0" apply false
id("org.jetbrains.kotlin.android") version "1.8.22" apply false
}
include(":app")

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@ -1,295 +0,0 @@
# Problème de Compatibilité: Impeller et Google Navigation intégrée sur Flutter
## Vue d'ensemble du problème
Les applications Flutter utilisant le SDK Google Navigation intégrée (Google Maps Navigation SDK) peuvent rencontrer des problèmes de rendu graphique lorsque le moteur de rendu Impeller est activé sur Android. Ce document explique le problème, ses symptômes et les solutions de contournement disponibles.
### Appareil testé
Ce problème a été observé et documenté sur l'appareil Android **KM10**. D'autres appareils Android peuvent également être affectés.
## Contexte technique
### Qu'est-ce qu'Impeller?
Impeller est le nouveau moteur de rendu de Flutter, conçu pour remplacer le backend Skia. Il offre plusieurs avantages:
- Performance prévisible grâce à la précompilation des shaders
- Réduction du jank et des pertes d'images
- Meilleure utilisation des API graphiques (Metal sur iOS, Vulkan sur Android)
Cependant, Impeller est encore en cours de stabilisation et certains plugins tiers (particulièrement ceux utilisant des vues natives de plateforme) peuvent rencontrer des problèmes de compatibilité.
### Pourquoi Google Navigation intégrée a des problèmes avec Impeller
Le SDK Google Navigation intégrée utilise des vues natives de plateforme (SurfaceView sur Android) pour afficher le contenu de la carte et de la navigation. L'interaction entre:
1. Le pipeline de rendu de Flutter (Impeller)
2. Les vues natives Android (Platform Views)
3. Le rendu complexe de la navigation (SDK Google Navigation)
Peut causer des glitches de rendu, des problèmes de z-index ou des artefacts visuels.
## Symptômes observés
Lorsque Impeller est activé (comportement par défaut), les problèmes suivants peuvent survenir:
### 1. Glitches de rendu de la carte
- Artefacts visuels sur la surface de la carte
- Problèmes de superposition (z-index) entre les widgets Flutter et la vue native de la carte
- Rendu incohérent des tuiles de carte ou des éléments de navigation
### 2. Problèmes de performance
- Sauts d'images: "Skipped X frames! The application may be doing too much work on its main thread"
- Conflits possibles de rendu GPU entre Impeller et le rendu natif de Google Navigation
### 3. Problèmes d'intégration des vues de plateforme
- La carte utilise une SurfaceView (vue native Android) intégrée dans l'arbre de widgets Flutter
- La composition d'Impeller peut entrer en conflit avec la façon dont Flutter gère les vues de plateforme
## Solutions de contournement
### Solution 1: Désactiver Impeller avec le flag de ligne de commande
**Commande de développement:**
```bash
flutter run -d KM10 --no-enable-impeller
```
**Effet:** Force Flutter à utiliser le backend Skia legacy au lieu d'Impeller.
**Avertissement de dépréciation:**
Lorsque vous exécutez avec `--no-enable-impeller`, Flutter affiche l'avertissement suivant:
```
[IMPORTANT:flutter/shell/common/shell.cc(527)] [Action Required]: Impeller opt-out deprecated.
The application opted out of Impeller by either using the
`--no-enable-impeller` flag or the
`io.flutter.embedding.android.EnableImpeller` `AndroidManifest.xml` entry.
These options are going to go away in an upcoming Flutter release. Remove
the explicit opt-out. If you need to opt-out, please report a bug describing
the issue.
```
**Important:** Ce flag sera retiré dans une future version de Flutter. Cette solution est donc temporaire.
### Solution 2: Configuration permanente dans AndroidManifest
Pour désactiver Impeller dans les builds de production, ajoutez dans `android/app/src/main/AndroidManifest.xml`:
```xml
<application
...>
<meta-data
android:name="io.flutter.embedding.android.EnableImpeller"
android:value="false" />
</application>
```
**Avertissement:** Cette configuration sera également dépréciée et retirée dans les futures versions de Flutter.
### Solution 3: Configuration via build.gradle (recommandé)
Dans `android/app/build.gradle`:
```gradle
android {
defaultConfig {
// Désactiver Impeller pour les builds Android
manifestPlaceholders = [
enableImpeller: 'false'
]
}
}
```
Puis référencez dans `AndroidManifest.xml`:
```xml
<meta-data
android:name="io.flutter.embedding.android.EnableImpeller"
android:value="${enableImpeller}" />
```
Cette approche permet de gérer la configuration de manière centralisée.
## Solutions potentielles à long terme
### 1. Attendre les correctifs upstream (Recommandé)
**Action:** Surveiller les dépôts suivants pour les mises à jour:
- [Flutter Engine Issues](https://github.com/flutter/flutter/issues)
- [Plugin Flutter Google Navigation](https://github.com/googlemaps/flutter-navigation-sdk/issues)
**Termes de recherche:**
- "Impeller platform view glitch"
- "Google Navigation Impeller rendering"
- "AndroidView Impeller artifacts"
### 2. Signaler le problème à l'équipe Flutter
Si le problème n'est pas déjà signalé, créez un rapport de bug avec:
- Modèle de l'appareil Android (ex: KM10)
- Version de Flutter (obtenir avec `flutter --version`)
- Version du SDK Google Navigation
- Étapes de reproduction détaillées
- Captures d'écran/vidéo du glitch
**Signaler à:** https://github.com/flutter/flutter/issues/new?template=02_bug.yml
### 3. Essayer Hybrid Composition
Tentez de passer en mode Hybrid Composition pour les vues de plateforme:
Dans `android/app/src/main/AndroidManifest.xml`:
```xml
<meta-data
android:name="io.flutter.embedded_views_preview"
android:value="true" />
```
Cela modifie la façon dont Flutter compose les vues natives et peut résoudre les conflits de rendu avec Impeller.
### 4. Surveiller les mises à jour Flutter
À mesure que l'implémentation d'Impeller par Flutter mature, les futures versions stables incluront des correctifs pour les problèmes de rendu des vues de plateforme. Mettez régulièrement à jour Flutter et testez avec Impeller activé:
```bash
flutter upgrade
flutter run -d KM10 # Tester sans --no-enable-impeller
```
## Implémentation du code
### Exemple d'utilisation de AndroidView pour Google Navigation intégrée
```dart
import 'dart:io' show Platform;
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
class MapWidget extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Platform.isAndroid
? AndroidView(
viewType: 'google_navigation_flutter',
onPlatformViewCreated: _onViewCreated,
creationParams: _viewCreationParams,
creationParamsCodec: const StandardMessageCodec(),
)
: UiKitView(
viewType: 'google_navigation_flutter',
onPlatformViewCreated: _onViewCreated,
creationParams: _viewCreationParams,
creationParamsCodec: const StandardMessageCodec(),
);
}
void _onViewCreated(int id) {
// Configuration de la navigation et de la carte
}
Map<String, dynamic> get _viewCreationParams {
return {
// Paramètres de création
};
}
}
```
## Liste de vérification pour tester la compatibilité Impeller
Lors des tests de compatibilité Impeller dans les futures versions de Flutter:
- [ ] La carte s'affiche correctement sans artefacts visuels
- [ ] Les widgets Flutter superposés sur la carte ne causent pas de problèmes de rendu
- [ ] Les marqueurs de carte et éléments UI s'affichent correctement
- [ ] Pas de problèmes de z-index entre les widgets Flutter et la vue de carte
- [ ] Défilement et panoramique fluides sans perte d'images
- [ ] Le rendu des itinéraires de navigation fonctionne correctement
- [ ] Les transitions de caméra sont fluides
- [ ] Les performances sont acceptables (vérifier le timing des images dans DevTools)
## Workflow de développement
### Recommandations actuelles
**Pour le développement Android (testé sur KM10):**
```bash
# Utiliser ceci jusqu'à confirmation de la compatibilité Impeller
flutter run -d KM10 --no-enable-impeller
```
**Pour le développement iOS:**
```bash
# Impeller iOS est plus stable, peut utiliser le comportement par défaut
flutter run -d ios
```
### Hot Reload
Le hot reload fonctionne normalement avec Impeller désactivé:
```bash
# Appuyez sur 'r' dans le terminal
r
```
### Builds de production
**Pour les builds Android de production, désactivez également Impeller** en utilisant l'une des méthodes décrites ci-dessus.
## Avertissements liés aux vues de plateforme
Les avertissements suivants peuvent apparaître dans les logs avec Skia ou Impeller et sont des particularités des vues de plateforme Android, pas des erreurs critiques:
```
E/FrameEvents: updateAcquireFence: Did not find frame.
W/Parcel: Expecting binder but got null!
```
## Références
- [Documentation Flutter Impeller](https://docs.flutter.dev/perf/impeller)
- [Vues de plateforme Flutter](https://docs.flutter.dev/platform-integration/android/platform-views)
- [SDK Google Navigation Flutter](https://developers.google.com/maps/documentation/navigation/flutter/reference)
- [Issues Flutter: Support des vues de plateforme Impeller](https://github.com/flutter/flutter/issues?q=is%3Aissue+impeller+platform+view)
## Notes importantes
### Statut de la compatibilité
- **Court terme:** Utilisez `--no-enable-impeller` pour le développement Android
- **Moyen terme:** Surveillez les versions stables de Flutter pour les améliorations d'Impeller
- **Long terme:** L'option de désactivation d'Impeller sera retirée de Flutter
### Alternatives à considérer
Si les problèmes de compatibilité persistent après le retrait de l'option de désactivation:
1. Rechercher des problèmes GitHub existants et voter pour eux
2. Envisager des solutions de cartographie alternatives (Apple Maps sur iOS, autres SDKs)
3. Explorer différents modes de composition des vues de plateforme
## Pour les développeurs futurs
Si vous lisez cette documentation:
1. **D'abord, essayez sans le flag** - Impeller peut avoir été corrigé dans votre version de Flutter:
```bash
flutter run -d KM10
```
2. **Si vous voyez des glitches de carte**, ajoutez le flag:
```bash
flutter run -d KM10 --no-enable-impeller
```
3. **Si le flag a été retiré et que les cartes ont toujours des glitches**, recherchez:
- "Flutter Impeller Google Navigation" sur les Issues GitHub
- Solutions de navigation et cartographie alternatives
- Modes de composition des vues de plateforme
4. **Considérez ceci comme une solution temporaire**, pas une solution permanente.
## Dernière mise à jour
**Date:** Novembre 2025
**Appareil testé:** KM10 (Android)
**Statut:** Solution de contournement active, surveillance des correctifs upstream en cours

View File

@ -1,295 +0,0 @@
# Impeller Map Rendering Issue Documentation
## Issue Overview
The Plan B Logistics Flutter app experiences map rendering glitches when using Flutter's Impeller rendering engine on Android. This requires the app to run with the `--no-enable-impeller` flag to function correctly.
## Affected Components
- **Component**: Google Maps Navigation SDK for Flutter
- **Platform**: Android (device: KM10 - Android physical device)
- **Rendering Engine**: Impeller (Flutter's new rendering backend)
- **Symptoms**: Visual glitches and rendering artifacts on the map view
## Technical Background
### What is Impeller?
Impeller is Flutter's next-generation rendering engine designed to replace the Skia backend. It provides:
- Predictable performance by precompiling shaders
- Reduced jank and frame drops
- Better graphics API utilization (Metal on iOS, Vulkan on Android)
However, Impeller is still being stabilized and some third-party plugins (particularly those with native platform views) may experience compatibility issues.
### Why Google Maps Has Issues with Impeller
Google Maps Navigation SDK uses native platform views (SurfaceView on Android) that render the map content. The interaction between:
1. Flutter's rendering pipeline (Impeller)
2. Native Android views (Platform Views)
3. Complex map rendering (Google Maps SDK)
Can cause rendering glitches, z-index issues, or visual artifacts.
## Current Workaround
### Running with Impeller Disabled
**Command:**
```bash
flutter run -d KM10 --no-enable-impeller
```
**Effect:** Forces Flutter to use the legacy Skia rendering backend instead of Impeller.
### Deprecation Warning
When running with `--no-enable-impeller`, Flutter displays the following warning:
```
[IMPORTANT:flutter/shell/common/shell.cc(527)] [Action Required]: Impeller opt-out deprecated.
The application opted out of Impeller by either using the
`--no-enable-impeller` flag or the
`io.flutter.embedding.android.EnableImpeller` `AndroidManifest.xml` entry.
These options are going to go away in an upcoming Flutter release. Remove
the explicit opt-out. If you need to opt-out, please report a bug describing
the issue.
```
**Important:** This flag will be removed in a future Flutter release, meaning this workaround is temporary.
## Observed Symptoms (When Impeller is Enabled)
When running with Impeller enabled (default behavior), the following issues occur:
1. **Map Rendering Glitches**
- Visual artifacts on the map surface
- Possible z-index layering issues between Flutter widgets and native map view
- Inconsistent rendering of map tiles or navigation elements
2. **Performance Issues**
- Frame skipping: "Skipped 128 frames! The application may be doing too much work on its main thread"
- Possible GPU rendering conflicts between Impeller and Google Maps' native rendering
3. **Platform View Integration Issues**
- The map uses a SurfaceView (native Android view) embedded in Flutter's widget tree
- Impeller's composition may conflict with how Flutter manages platform views
## File References
### Map Component Implementation
**File:** `lib/components/dark_mode_map.dart`
- Implements Google Maps Navigation SDK integration
- Uses AndroidView platform view for native map rendering
- Lines 403-408, 421-426: Auto-recenter functionality
- Configures map settings for performance optimization
**Key Code Sections:**
```dart
// Platform view creation (Android)
body: Platform.isAndroid
? AndroidView(
viewType: 'google_navigation_flutter',
onPlatformViewCreated: _onViewCreated,
creationParams: _viewCreationParams,
creationParamsCodec: const StandardMessageCodec(),
)
```
## Potential Solutions
### 1. Wait for Upstream Fixes (Recommended)
**Action:** Monitor the following repositories for updates:
- [Flutter Engine Issues](https://github.com/flutter/flutter/issues)
- [Google Maps Flutter Navigation Plugin](https://github.com/googlemaps/flutter-navigation-sdk/issues)
**Search Terms:**
- "Impeller platform view glitch"
- "Google Maps Impeller rendering"
- "AndroidView Impeller artifacts"
### 2. Report Issue to Flutter Team
If not already reported, file a bug report with:
- Device: KM10 Android device
- Flutter version: (check with `flutter --version`)
- Google Maps Navigation SDK version
- Detailed reproduction steps
- Screenshots/video of the glitch
**Report at:** https://github.com/flutter/flutter/issues/new?template=02_bug.yml
### 3. Permanent AndroidManifest Configuration (Temporary)
If needed for production builds before Flutter removes the flag, add to `android/app/src/main/AndroidManifest.xml`:
```xml
<application
...>
<meta-data
android:name="io.flutter.embedding.android.EnableImpeller"
android:value="false" />
</application>
```
**Warning:** This will also be deprecated and removed in future Flutter releases.
### 4. Investigate Hybrid Composition (Alternative)
Try switching to Hybrid Composition mode for platform views:
```dart
// In android/app/src/main/AndroidManifest.xml
<meta-data
android:name="io.flutter.embedded_views_preview"
android:value="true" />
```
This changes how Flutter composites native views and may resolve rendering conflicts with Impeller.
### 5. Monitor Flutter Stable Channel Updates
As Flutter's Impeller implementation matures, future stable releases will include fixes for platform view rendering issues. Regularly update Flutter and test with Impeller enabled:
```bash
flutter upgrade
flutter run -d KM10 # Test without --no-enable-impeller flag
```
## Testing Checklist
When testing Impeller compatibility in future Flutter versions:
- [ ] Map renders correctly without visual artifacts
- [ ] Delivery list sidebar doesn't interfere with map rendering
- [ ] Map markers and navigation UI elements display properly
- [ ] No z-index issues between Flutter widgets and map view
- [ ] Smooth scrolling and panning without frame drops
- [ ] Navigation route rendering works correctly
- [ ] Camera transitions are smooth
- [ ] Auto-recenter functionality works
- [ ] Performance is acceptable (check frame timing in DevTools)
## Development Workflow
### Current Recommendation
**For Android development:**
```bash
# Use this until Impeller compatibility is confirmed
flutter run -d KM10 --no-enable-impeller
```
**For iOS development:**
```bash
# iOS Impeller is more stable, can use default
flutter run -d ios
```
### Hot Reload
Hot reload works normally with Impeller disabled:
```bash
# Press 'r' in terminal or
echo "r" | nc localhost 7182
```
### Release Builds
**Current Android release builds should also disable Impeller:**
In `android/app/build.gradle`:
```gradle
android {
defaultConfig {
// Add Impeller opt-out for release builds
manifestPlaceholders = [
enableImpeller: 'false'
]
}
}
```
Then reference in AndroidManifest.xml:
```xml
<meta-data
android:name="io.flutter.embedding.android.EnableImpeller"
android:value="${enableImpeller}" />
```
## Timeline and Impact
### Short-term (Current)
- **Status:** Using `--no-enable-impeller` flag for all Android development
- **Impact:** No user-facing issues, development proceeds normally
- **Risk:** None, Skia backend is stable and well-tested
### Medium-term (Next 3-6 months)
- **Status:** Monitor Flutter stable releases for Impeller improvements
- **Action:** Test each stable release with Impeller enabled
- **Risk:** Low, workaround is stable
### Long-term (6-12 months)
- **Status:** Impeller opt-out will be removed from Flutter
- **Action:** Must resolve compatibility or report blocking issues
- **Risk:** Medium - may need to migrate away from Google Maps if incompatibility persists
## Related Issues
### Performance Warnings
The following warnings appear in logs but are expected during development:
```
I/Choreographer(22365): Skipped 128 frames! The application may be doing too much work on its main thread.
```
This is related to initial app load and map initialization, not specifically to the Impeller workaround.
### Platform View Warnings
```
E/FrameEvents(22365): updateAcquireFence: Did not find frame.
W/Parcel(22365): Expecting binder but got null!
```
These warnings appear with both Skia and Impeller backends and are Android platform view quirks, not critical errors.
## References
- [Flutter Impeller Documentation](https://docs.flutter.dev/perf/impeller)
- [Flutter Platform Views](https://docs.flutter.dev/platform-integration/android/platform-views)
- [Google Maps Flutter Navigation SDK](https://developers.google.com/maps/documentation/navigation/flutter/reference)
- [Flutter Issue: Impeller platform view support](https://github.com/flutter/flutter/issues?q=is%3Aissue+impeller+platform+view)
## Last Updated
**Date:** 2025-11-25
**Flutter Version:** (Run `flutter --version` to check)
**Device:** KM10 Android
**Status:** Workaround active, monitoring for upstream fixes
---
## Notes for Future Developers
If you're reading this documentation:
1. **First, try without the flag** - Impeller may have been fixed in your Flutter version:
```bash
flutter run -d android
```
2. **If you see map glitches**, add the flag:
```bash
flutter run -d android --no-enable-impeller
```
3. **If the flag is removed and maps still glitch**, search for:
- "Flutter Impeller Google Maps" on GitHub Issues
- Alternative map solutions (Apple Maps on iOS, alternative SDKs)
- Platform view composition modes
4. **Consider this a temporary workaround**, not a permanent solution.

View File

@ -21,6 +21,6 @@
<key>CFBundleVersion</key>
<string>1.0</string>
<key>MinimumOSVersion</key>
<string>13.0</string>
<string>12.0</string>
</dict>
</plist>

View File

@ -1,2 +0,0 @@
#include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.profile.xcconfig"
#include "Generated.xcconfig"

View File

@ -1,5 +1,5 @@
# Uncomment this line to define a global platform for your project
platform :ios, '16.0'
# platform :ios, '12.0'
# CocoaPods analytics sends network stats synchronously affecting flutter build latency.
ENV['COCOAPODS_DISABLE_STATS'] = 'true'
@ -39,21 +39,5 @@ end
post_install do |installer|
installer.pods_project.targets.each do |target|
flutter_additional_ios_build_settings(target)
# CRITICAL: Enable permissions for permission_handler plugin
target.build_configurations.each do |config|
config.build_settings['GCC_PREPROCESSOR_DEFINITIONS'] ||= [
'$(inherited)',
## dart: [PermissionGroup.location, PermissionGroup.locationAlways, PermissionGroup.locationWhenInUse]
'PERMISSION_LOCATION=1',
## dart: PermissionGroup.camera (for image_picker)
'PERMISSION_CAMERA=1',
## dart: PermissionGroup.photos (for image_picker)
'PERMISSION_PHOTOS=1',
]
end
end
end

View File

@ -1,88 +0,0 @@
PODS:
- AppAuth (2.0.0):
- AppAuth/Core (= 2.0.0)
- AppAuth/ExternalUserAgent (= 2.0.0)
- AppAuth/Core (2.0.0)
- AppAuth/ExternalUserAgent (2.0.0):
- AppAuth/Core
- Flutter (1.0.0)
- flutter_appauth (0.0.1):
- AppAuth (= 2.0.0)
- Flutter
- flutter_secure_storage (6.0.0):
- Flutter
- google_navigation_flutter (0.0.1):
- Flutter
- GoogleNavigation (= 10.0.0)
- GoogleMaps (10.0.0):
- GoogleMaps/Maps (= 10.0.0)
- GoogleMaps/Maps (10.0.0)
- GoogleNavigation (10.0.0):
- GoogleMaps (= 10.0.0)
- image_picker_ios (0.0.1):
- Flutter
- path_provider_foundation (0.0.1):
- Flutter
- FlutterMacOS
- permission_handler_apple (9.3.0):
- Flutter
- shared_preferences_foundation (0.0.1):
- Flutter
- FlutterMacOS
- url_launcher_ios (0.0.1):
- Flutter
DEPENDENCIES:
- Flutter (from `Flutter`)
- flutter_appauth (from `.symlinks/plugins/flutter_appauth/ios`)
- flutter_secure_storage (from `.symlinks/plugins/flutter_secure_storage/ios`)
- google_navigation_flutter (from `.symlinks/plugins/google_navigation_flutter/ios`)
- image_picker_ios (from `.symlinks/plugins/image_picker_ios/ios`)
- path_provider_foundation (from `.symlinks/plugins/path_provider_foundation/darwin`)
- permission_handler_apple (from `.symlinks/plugins/permission_handler_apple/ios`)
- shared_preferences_foundation (from `.symlinks/plugins/shared_preferences_foundation/darwin`)
- url_launcher_ios (from `.symlinks/plugins/url_launcher_ios/ios`)
SPEC REPOS:
trunk:
- AppAuth
- GoogleMaps
- GoogleNavigation
EXTERNAL SOURCES:
Flutter:
:path: Flutter
flutter_appauth:
:path: ".symlinks/plugins/flutter_appauth/ios"
flutter_secure_storage:
:path: ".symlinks/plugins/flutter_secure_storage/ios"
google_navigation_flutter:
:path: ".symlinks/plugins/google_navigation_flutter/ios"
image_picker_ios:
:path: ".symlinks/plugins/image_picker_ios/ios"
path_provider_foundation:
:path: ".symlinks/plugins/path_provider_foundation/darwin"
permission_handler_apple:
:path: ".symlinks/plugins/permission_handler_apple/ios"
shared_preferences_foundation:
:path: ".symlinks/plugins/shared_preferences_foundation/darwin"
url_launcher_ios:
:path: ".symlinks/plugins/url_launcher_ios/ios"
SPEC CHECKSUMS:
AppAuth: 1c1a8afa7e12f2ec3a294d9882dfa5ab7d3cb063
Flutter: cabc95a1d2626b1b06e7179b784ebcf0c0cde467
flutter_appauth: d4abcf54856e5d8ba82ed7646ffc83245d4aa448
flutter_secure_storage: 1ed9476fba7e7a782b22888f956cce43e2c62f13
google_navigation_flutter: aff5e273b19113b8964780ff4e899f6f2e07f6dc
GoogleMaps: 9ce9c898074e96655acaf1ba5d6f85991ecee7a3
GoogleNavigation: 963899162709d245f07a65cd68c3115292ee2bdb
image_picker_ios: e0ece4aa2a75771a7de3fa735d26d90817041326
path_provider_foundation: bb55f6dbba17d0dccd6737fe6f7f34fbd0376880
permission_handler_apple: 4ed2196e43d0651e8ff7ca3483a069d469701f2d
shared_preferences_foundation: 7036424c3d8ec98dfe75ff1667cb0cd531ec82bb
url_launcher_ios: 7a95fa5b60cc718a708b8f2966718e93db0cef1b
PODFILE CHECKSUM: a9903f63c2c1fcd26a560ce0325dca46dd46141c
COCOAPODS: 1.16.2

View File

@ -8,14 +8,12 @@
/* Begin PBXBuildFile section */
1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */ = {isa = PBXBuildFile; fileRef = 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */; };
2836CDC5AEBAD3AEED75A3A3 /* Pods_RunnerTests.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 84ED75BB9A45F74F2849E366 /* Pods_RunnerTests.framework */; };
331C808B294A63AB00263BE5 /* RunnerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 331C807B294A618700263BE5 /* RunnerTests.swift */; };
3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */ = {isa = PBXBuildFile; fileRef = 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */; };
74858FAF1ED2DC5600515810 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 74858FAE1ED2DC5600515810 /* AppDelegate.swift */; };
97C146FC1CF9000F007C117D /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FA1CF9000F007C117D /* Main.storyboard */; };
97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FD1CF9000F007C117D /* Assets.xcassets */; };
97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */; };
F19CB96FD01EABE54F784DB8 /* Pods_Runner.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = DD204FE7CB10350F4E43E8C8 /* Pods_Runner.framework */; };
/* End PBXBuildFile section */
/* Begin PBXContainerItemProxy section */
@ -42,20 +40,14 @@
/* End PBXCopyFilesBuildPhase section */
/* Begin PBXFileReference section */
13867C66F1703482B503520B /* Pods-RunnerTests.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RunnerTests.release.xcconfig"; path = "Target Support Files/Pods-RunnerTests/Pods-RunnerTests.release.xcconfig"; sourceTree = "<group>"; };
1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = GeneratedPluginRegistrant.h; sourceTree = "<group>"; };
1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = GeneratedPluginRegistrant.m; sourceTree = "<group>"; };
275FA98EBEF1D87C21AA0A3A /* Pods-Runner.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.debug.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig"; sourceTree = "<group>"; };
331C807B294A618700263BE5 /* RunnerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RunnerTests.swift; sourceTree = "<group>"; };
331C8081294A63A400263BE5 /* RunnerTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = RunnerTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; };
3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = AppFrameworkInfo.plist; path = Flutter/AppFrameworkInfo.plist; sourceTree = "<group>"; };
65116C16D7DB0EAA5A1DF663 /* Pods-Runner.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.profile.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.profile.xcconfig"; sourceTree = "<group>"; };
74858FAD1ED2DC5600515810 /* Runner-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "Runner-Bridging-Header.h"; sourceTree = "<group>"; };
74858FAE1ED2DC5600515810 /* AppDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = "<group>"; };
77C9A4AE9C5588D9B699F74C /* Pods-RunnerTests.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RunnerTests.debug.xcconfig"; path = "Target Support Files/Pods-RunnerTests/Pods-RunnerTests.debug.xcconfig"; sourceTree = "<group>"; };
7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = Release.xcconfig; path = Flutter/Release.xcconfig; sourceTree = "<group>"; };
7DC868389DCA23AC494EC5EE /* Pods-Runner.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.release.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig"; sourceTree = "<group>"; };
84ED75BB9A45F74F2849E366 /* Pods_RunnerTests.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_RunnerTests.framework; sourceTree = BUILT_PRODUCTS_DIR; };
9740EEB21CF90195004384FC /* Debug.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Debug.xcconfig; path = Flutter/Debug.xcconfig; sourceTree = "<group>"; };
9740EEB31CF90195004384FC /* Generated.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Generated.xcconfig; path = Flutter/Generated.xcconfig; sourceTree = "<group>"; };
97C146EE1CF9000F007C117D /* Runner.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Runner.app; sourceTree = BUILT_PRODUCTS_DIR; };
@ -63,39 +55,19 @@
97C146FD1CF9000F007C117D /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = "<group>"; };
97C147001CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = "<group>"; };
97C147021CF9000F007C117D /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
DD204FE7CB10350F4E43E8C8 /* Pods_Runner.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Runner.framework; sourceTree = BUILT_PRODUCTS_DIR; };
F864EA92C8601181D927DDF4 /* Pods-RunnerTests.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RunnerTests.profile.xcconfig"; path = "Target Support Files/Pods-RunnerTests/Pods-RunnerTests.profile.xcconfig"; sourceTree = "<group>"; };
/* End PBXFileReference section */
/* Begin PBXFrameworksBuildPhase section */
66CCBD6C58346713889C0A9A /* Frameworks */ = {
isa = PBXFrameworksBuildPhase;
buildActionMask = 2147483647;
files = (
2836CDC5AEBAD3AEED75A3A3 /* Pods_RunnerTests.framework in Frameworks */,
);
runOnlyForDeploymentPostprocessing = 0;
};
97C146EB1CF9000F007C117D /* Frameworks */ = {
isa = PBXFrameworksBuildPhase;
buildActionMask = 2147483647;
files = (
F19CB96FD01EABE54F784DB8 /* Pods_Runner.framework in Frameworks */,
);
runOnlyForDeploymentPostprocessing = 0;
};
/* End PBXFrameworksBuildPhase section */
/* Begin PBXGroup section */
152485DA02A362CD0E771781 /* Frameworks */ = {
isa = PBXGroup;
children = (
DD204FE7CB10350F4E43E8C8 /* Pods_Runner.framework */,
84ED75BB9A45F74F2849E366 /* Pods_RunnerTests.framework */,
);
name = Frameworks;
sourceTree = "<group>";
};
331C8082294A63A400263BE5 /* RunnerTests */ = {
isa = PBXGroup;
children = (
@ -104,19 +76,6 @@
path = RunnerTests;
sourceTree = "<group>";
};
878D1C1052592A1ACB6A9B61 /* Pods */ = {
isa = PBXGroup;
children = (
275FA98EBEF1D87C21AA0A3A /* Pods-Runner.debug.xcconfig */,
7DC868389DCA23AC494EC5EE /* Pods-Runner.release.xcconfig */,
65116C16D7DB0EAA5A1DF663 /* Pods-Runner.profile.xcconfig */,
77C9A4AE9C5588D9B699F74C /* Pods-RunnerTests.debug.xcconfig */,
13867C66F1703482B503520B /* Pods-RunnerTests.release.xcconfig */,
F864EA92C8601181D927DDF4 /* Pods-RunnerTests.profile.xcconfig */,
);
path = Pods;
sourceTree = "<group>";
};
9740EEB11CF90186004384FC /* Flutter */ = {
isa = PBXGroup;
children = (
@ -135,8 +94,6 @@
97C146F01CF9000F007C117D /* Runner */,
97C146EF1CF9000F007C117D /* Products */,
331C8082294A63A400263BE5 /* RunnerTests */,
878D1C1052592A1ACB6A9B61 /* Pods */,
152485DA02A362CD0E771781 /* Frameworks */,
);
sourceTree = "<group>";
};
@ -171,10 +128,8 @@
isa = PBXNativeTarget;
buildConfigurationList = 331C8087294A63A400263BE5 /* Build configuration list for PBXNativeTarget "RunnerTests" */;
buildPhases = (
9220E6C153C3EEFE9CD513F1 /* [CP] Check Pods Manifest.lock */,
331C807D294A63A400263BE5 /* Sources */,
331C807F294A63A400263BE5 /* Resources */,
66CCBD6C58346713889C0A9A /* Frameworks */,
);
buildRules = (
);
@ -190,15 +145,12 @@
isa = PBXNativeTarget;
buildConfigurationList = 97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */;
buildPhases = (
36AE2A59F66C6734ADB52635 /* [CP] Check Pods Manifest.lock */,
9740EEB61CF901F6004384FC /* Run Script */,
97C146EA1CF9000F007C117D /* Sources */,
97C146EB1CF9000F007C117D /* Frameworks */,
97C146EC1CF9000F007C117D /* Resources */,
9705A1C41CF9048500538489 /* Embed Frameworks */,
3B06AD1E1E4923F5004D2608 /* Thin Binary */,
4470FC3A3E5C008BAD800C0C /* [CP] Embed Pods Frameworks */,
0D60217D1C1D288B608930BF /* [CP] Copy Pods Resources */,
);
buildRules = (
);
@ -270,45 +222,6 @@
/* End PBXResourcesBuildPhase section */
/* Begin PBXShellScriptBuildPhase section */
0D60217D1C1D288B608930BF /* [CP] Copy Pods Resources */ = {
isa = PBXShellScriptBuildPhase;
buildActionMask = 2147483647;
files = (
);
inputFileListPaths = (
"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-resources-${CONFIGURATION}-input-files.xcfilelist",
);
name = "[CP] Copy Pods Resources";
outputFileListPaths = (
"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-resources-${CONFIGURATION}-output-files.xcfilelist",
);
runOnlyForDeploymentPostprocessing = 0;
shellPath = /bin/sh;
shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-resources.sh\"\n";
showEnvVarsInLog = 0;
};
36AE2A59F66C6734ADB52635 /* [CP] Check Pods Manifest.lock */ = {
isa = PBXShellScriptBuildPhase;
buildActionMask = 2147483647;
files = (
);
inputFileListPaths = (
);
inputPaths = (
"${PODS_PODFILE_DIR_PATH}/Podfile.lock",
"${PODS_ROOT}/Manifest.lock",
);
name = "[CP] Check Pods Manifest.lock";
outputFileListPaths = (
);
outputPaths = (
"$(DERIVED_FILE_DIR)/Pods-Runner-checkManifestLockResult.txt",
);
runOnlyForDeploymentPostprocessing = 0;
shellPath = /bin/sh;
shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n";
showEnvVarsInLog = 0;
};
3B06AD1E1E4923F5004D2608 /* Thin Binary */ = {
isa = PBXShellScriptBuildPhase;
alwaysOutOfDate = 1;
@ -325,45 +238,6 @@
shellPath = /bin/sh;
shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" embed_and_thin";
};
4470FC3A3E5C008BAD800C0C /* [CP] Embed Pods Frameworks */ = {
isa = PBXShellScriptBuildPhase;
buildActionMask = 2147483647;
files = (
);
inputFileListPaths = (
"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-input-files.xcfilelist",
);
name = "[CP] Embed Pods Frameworks";
outputFileListPaths = (
"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-output-files.xcfilelist",
);
runOnlyForDeploymentPostprocessing = 0;
shellPath = /bin/sh;
shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks.sh\"\n";
showEnvVarsInLog = 0;
};
9220E6C153C3EEFE9CD513F1 /* [CP] Check Pods Manifest.lock */ = {
isa = PBXShellScriptBuildPhase;
buildActionMask = 2147483647;
files = (
);
inputFileListPaths = (
);
inputPaths = (
"${PODS_PODFILE_DIR_PATH}/Podfile.lock",
"${PODS_ROOT}/Manifest.lock",
);
name = "[CP] Check Pods Manifest.lock";
outputFileListPaths = (
);
outputPaths = (
"$(DERIVED_FILE_DIR)/Pods-RunnerTests-checkManifestLockResult.txt",
);
runOnlyForDeploymentPostprocessing = 0;
shellPath = /bin/sh;
shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n";
showEnvVarsInLog = 0;
};
9740EEB61CF901F6004384FC /* Run Script */ = {
isa = PBXShellScriptBuildPhase;
alwaysOutOfDate = 1;
@ -472,7 +346,7 @@
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
GCC_WARN_UNUSED_FUNCTION = YES;
GCC_WARN_UNUSED_VARIABLE = YES;
IPHONEOS_DEPLOYMENT_TARGET = 13.0;
IPHONEOS_DEPLOYMENT_TARGET = 12.0;
MTL_ENABLE_DEBUG_INFO = NO;
SDKROOT = iphoneos;
SUPPORTED_PLATFORMS = iphoneos;
@ -488,14 +362,14 @@
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
CLANG_ENABLE_MODULES = YES;
CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";
DEVELOPMENT_TEAM = 833P6TSX55;
DEVELOPMENT_TEAM = LD76P8L42W;
ENABLE_BITCODE = NO;
INFOPLIST_FILE = Runner/Info.plist;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/Frameworks",
);
PRODUCT_BUNDLE_IDENTIFIER = com.local.planbLogistic;
PRODUCT_BUNDLE_IDENTIFIER = io.svrnty.flutterFleetLogisticWorkforceApp;
PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h";
SWIFT_VERSION = 5.0;
@ -505,15 +379,13 @@
};
331C8088294A63A400263BE5 /* Debug */ = {
isa = XCBuildConfiguration;
baseConfigurationReference = 77C9A4AE9C5588D9B699F74C /* Pods-RunnerTests.debug.xcconfig */;
buildSettings = {
BUNDLE_LOADER = "$(TEST_HOST)";
"CODE_SIGN_IDENTITY[sdk=macosx*]" = "Apple Development";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 1;
GENERATE_INFOPLIST_FILE = YES;
MARKETING_VERSION = 1.0;
PRODUCT_BUNDLE_IDENTIFIER = com.goutezplanb.planbLogistic.RunnerTests;
PRODUCT_BUNDLE_IDENTIFIER = io.svrnty.flutterFleetLogisticWorkforceApp.RunnerTests;
PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG;
SWIFT_OPTIMIZATION_LEVEL = "-Onone";
@ -524,14 +396,13 @@
};
331C8089294A63A400263BE5 /* Release */ = {
isa = XCBuildConfiguration;
baseConfigurationReference = 13867C66F1703482B503520B /* Pods-RunnerTests.release.xcconfig */;
buildSettings = {
BUNDLE_LOADER = "$(TEST_HOST)";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 1;
GENERATE_INFOPLIST_FILE = YES;
MARKETING_VERSION = 1.0;
PRODUCT_BUNDLE_IDENTIFIER = com.goutezplanb.planbLogistic.RunnerTests;
PRODUCT_BUNDLE_IDENTIFIER = io.svrnty.flutterFleetLogisticWorkforceApp.RunnerTests;
PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_VERSION = 5.0;
TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Runner.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/Runner";
@ -540,14 +411,13 @@
};
331C808A294A63A400263BE5 /* Profile */ = {
isa = XCBuildConfiguration;
baseConfigurationReference = F864EA92C8601181D927DDF4 /* Pods-RunnerTests.profile.xcconfig */;
buildSettings = {
BUNDLE_LOADER = "$(TEST_HOST)";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 1;
GENERATE_INFOPLIST_FILE = YES;
MARKETING_VERSION = 1.0;
PRODUCT_BUNDLE_IDENTIFIER = com.goutezplanb.planbLogistic.RunnerTests;
PRODUCT_BUNDLE_IDENTIFIER = io.svrnty.flutterFleetLogisticWorkforceApp.RunnerTests;
PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_VERSION = 5.0;
TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Runner.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/Runner";
@ -603,7 +473,7 @@
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
GCC_WARN_UNUSED_FUNCTION = YES;
GCC_WARN_UNUSED_VARIABLE = YES;
IPHONEOS_DEPLOYMENT_TARGET = 13.0;
IPHONEOS_DEPLOYMENT_TARGET = 12.0;
MTL_ENABLE_DEBUG_INFO = YES;
ONLY_ACTIVE_ARCH = YES;
SDKROOT = iphoneos;
@ -654,7 +524,7 @@
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
GCC_WARN_UNUSED_FUNCTION = YES;
GCC_WARN_UNUSED_VARIABLE = YES;
IPHONEOS_DEPLOYMENT_TARGET = 13.0;
IPHONEOS_DEPLOYMENT_TARGET = 12.0;
MTL_ENABLE_DEBUG_INFO = NO;
SDKROOT = iphoneos;
SUPPORTED_PLATFORMS = iphoneos;
@ -672,14 +542,14 @@
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
CLANG_ENABLE_MODULES = YES;
CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";
DEVELOPMENT_TEAM = 833P6TSX55;
DEVELOPMENT_TEAM = LD76P8L42W;
ENABLE_BITCODE = NO;
INFOPLIST_FILE = Runner/Info.plist;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/Frameworks",
);
PRODUCT_BUNDLE_IDENTIFIER = com.local.planbLogistic;
PRODUCT_BUNDLE_IDENTIFIER = io.svrnty.flutterFleetLogisticWorkforceApp;
PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h";
SWIFT_OPTIMIZATION_LEVEL = "-Onone";
@ -695,14 +565,14 @@
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
CLANG_ENABLE_MODULES = YES;
CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";
DEVELOPMENT_TEAM = 833P6TSX55;
DEVELOPMENT_TEAM = LD76P8L42W;
ENABLE_BITCODE = NO;
INFOPLIST_FILE = Runner/Info.plist;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/Frameworks",
);
PRODUCT_BUNDLE_IDENTIFIER = com.local.planbLogistic;
PRODUCT_BUNDLE_IDENTIFIER = io.svrnty.flutterFleetLogisticWorkforceApp;
PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h";
SWIFT_VERSION = 5.0;

View File

@ -26,7 +26,6 @@
buildConfiguration = "Debug"
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
customLLDBInitFile = "$(SRCROOT)/Flutter/ephemeral/flutter_lldbinit"
shouldUseLaunchSchemeArgsEnv = "YES">
<MacroExpansion>
<BuildableReference
@ -55,7 +54,6 @@
buildConfiguration = "Debug"
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
customLLDBInitFile = "$(SRCROOT)/Flutter/ephemeral/flutter_lldbinit"
launchStyle = "0"
useCustomWorkingDirectory = "NO"
ignoresPersistentStateOnLaunch = "NO"

View File

@ -4,7 +4,4 @@
<FileRef
location = "group:Runner.xcodeproj">
</FileRef>
<FileRef
location = "group:Pods/Pods.xcodeproj">
</FileRef>
</Workspace>

View File

@ -8,7 +8,9 @@ import GoogleMaps
_ application: UIApplication,
didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?
) -> Bool {
GMSServices.provideAPIKey("AIzaSyCuYzbusLkVrHcy10bJ8STF6gyOexQWjuk")
// Configure Google Maps with API Key
GMSServices.provideAPIKey("YOUR_GOOGLE_MAPS_API_KEY_HERE")
GeneratedPluginRegistrant.register(with: self)
return super.application(application, didFinishLaunchingWithOptions: launchOptions)
}

View File

@ -5,7 +5,7 @@
<key>CFBundleDevelopmentRegion</key>
<string>$(DEVELOPMENT_LANGUAGE)</string>
<key>CFBundleDisplayName</key>
<string>Planb Logistic</string>
<string>Flutter Fleet Logistic Workforce App</string>
<key>CFBundleExecutable</key>
<string>$(EXECUTABLE_NAME)</string>
<key>CFBundleIdentifier</key>
@ -13,7 +13,7 @@
<key>CFBundleInfoDictionaryVersion</key>
<string>6.0</string>
<key>CFBundleName</key>
<string>planb_logistic</string>
<string>flutter_fleet_logistic_workforce_app</string>
<key>CFBundlePackageType</key>
<string>APPL</string>
<key>CFBundleShortVersionString</key>
@ -45,24 +45,20 @@
<true/>
<key>UIApplicationSupportsIndirectInputEvents</key>
<true/>
<!-- Location Permissions -->
<key>NSLocationWhenInUseUsageDescription</key>
<string>This app needs location access to show your current position and navigate to destinations.</string>
<key>NSLocationAlwaysAndWhenInUseUsageDescription</key>
<string>This app needs location access to track your route progress in the background.</string>
<key>NSLocationAlwaysUsageDescription</key>
<string>This app needs location access to track your route progress in the background.</string>
<!-- Google Maps URL Scheme -->
<key>LSApplicationQueriesSchemes</key>
<array>
<string>comgooglemaps</string>
<string>googlemaps</string>
</array>
<key>NSLocationWhenInUseUsageDescription</key>
<string>This app needs your location to show delivery routes and navigate to addresses.</string>
<key>NSLocationAlwaysAndWhenInUseUsageDescription</key>
<string>This app needs your location to show delivery routes and navigate to addresses.</string>
<key>UIBackgroundModes</key>
<array>
<string>location</string>
<string>fetch</string>
</array>
<key>NSLocationAlwaysUsageDescription</key>
<string>This app needs continuous access to your location for navigation and delivery tracking.</string>
<key>NSCameraUsageDescription</key>
<string>This app needs camera access to take photos of deliveries.</string>
<key>NSPhotoLibraryUsageDescription</key>
<string>This app needs access to your photos to select delivery images.</string>
</dict>
</plist>

View File

@ -1 +0,0 @@
094b6744e27cc18cd7b60f0f05ed7292

View File

@ -1,10 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>logFormatVersion</key>
<integer>11</integer>
<key>logs</key>
<dict/>
</dict>
</plist>

View File

@ -1,10 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>logFormatVersion</key>
<integer>11</integer>
<key>logs</key>
<dict/>
</dict>
</plist>

View File

@ -1,10 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>logFormatVersion</key>
<integer>11</integer>
<key>logs</key>
<dict/>
</dict>
</plist>

View File

@ -1,10 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>logFormatVersion</key>
<integer>11</integer>
<key>logs</key>
<dict/>
</dict>
</plist>

View File

@ -1,10 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>logFormatVersion</key>
<integer>11</integer>
<key>logs</key>
<dict/>
</dict>
</plist>

View File

@ -1 +0,0 @@
{"buildConfigurations":[{"baseConfigurationFileReference":"bfdfe7dc352907fc980b868725387e98dbfbadcdcf06bb75f82d99bd1987f413","buildSettings":{"CODE_SIGNING_ALLOWED":"NO","CODE_SIGNING_IDENTITY":"-","CODE_SIGNING_REQUIRED":"NO","CONFIGURATION_BUILD_DIR":"$(BUILD_DIR)/$(CONFIGURATION)$(EFFECTIVE_PLATFORM_NAME)/google_navigation_flutter","EXPANDED_CODE_SIGN_IDENTITY":"-","IBSC_MODULE":"google_navigation_flutter","INFOPLIST_FILE":"Target Support Files/google_navigation_flutter/ResourceBundle-google_navigation_flutter_privacy_info-google_navigation_flutter-Info.plist","IPHONEOS_DEPLOYMENT_TARGET":"16.0","ONLY_ACTIVE_ARCH":"NO","PRODUCT_NAME":"google_navigation_flutter_privacy_info","SDKROOT":"iphoneos","SKIP_INSTALL":"YES","TARGETED_DEVICE_FAMILY":"1,2","WRAPPER_EXTENSION":"bundle"},"guid":"bfdfe7dc352907fc980b868725387e988425730c16bf04619a2e8ff8e598af88","name":"Debug"},{"baseConfigurationFileReference":"bfdfe7dc352907fc980b868725387e986fbad5942614a388f6084141625088fe","buildSettings":{"CLANG_ENABLE_OBJC_WEAK":"NO","CODE_SIGNING_ALLOWED":"NO","CODE_SIGNING_IDENTITY":"-","CODE_SIGNING_REQUIRED":"NO","CONFIGURATION_BUILD_DIR":"$(BUILD_DIR)/$(CONFIGURATION)$(EFFECTIVE_PLATFORM_NAME)/google_navigation_flutter","EXPANDED_CODE_SIGN_IDENTITY":"-","IBSC_MODULE":"google_navigation_flutter","INFOPLIST_FILE":"Target Support Files/google_navigation_flutter/ResourceBundle-google_navigation_flutter_privacy_info-google_navigation_flutter-Info.plist","IPHONEOS_DEPLOYMENT_TARGET":"16.0","PRODUCT_NAME":"google_navigation_flutter_privacy_info","SDKROOT":"iphoneos","SKIP_INSTALL":"YES","TARGETED_DEVICE_FAMILY":"1,2","VALIDATE_PRODUCT":"YES","WRAPPER_EXTENSION":"bundle"},"guid":"bfdfe7dc352907fc980b868725387e9878f9daa92258a169175ea5d10ca08e09","name":"Profile"},{"baseConfigurationFileReference":"bfdfe7dc352907fc980b868725387e986fbad5942614a388f6084141625088fe","buildSettings":{"CODE_SIGNING_ALLOWED":"NO","CODE_SIGNING_IDENTITY":"-","CODE_SIGNING_REQUIRED":"NO","CONFIGURATION_BUILD_DIR":"$(BUILD_DIR)/$(CONFIGURATION)$(EFFECTIVE_PLATFORM_NAME)/google_navigation_flutter","EXPANDED_CODE_SIGN_IDENTITY":"-","IBSC_MODULE":"google_navigation_flutter","INFOPLIST_FILE":"Target Support Files/google_navigation_flutter/ResourceBundle-google_navigation_flutter_privacy_info-google_navigation_flutter-Info.plist","IPHONEOS_DEPLOYMENT_TARGET":"16.0","PRODUCT_NAME":"google_navigation_flutter_privacy_info","SDKROOT":"iphoneos","SKIP_INSTALL":"YES","TARGETED_DEVICE_FAMILY":"1,2","WRAPPER_EXTENSION":"bundle"},"guid":"bfdfe7dc352907fc980b868725387e9822546915a308e50f47af7633b933d597","name":"Release"}],"buildPhases":[{"buildFiles":[],"guid":"bfdfe7dc352907fc980b868725387e98084ecc203e0776bb45c61db5bf5438c1","type":"com.apple.buildphase.sources"},{"buildFiles":[],"guid":"bfdfe7dc352907fc980b868725387e98b2ac9aabfb4ea17c19b0f0dbc660b6c0","type":"com.apple.buildphase.frameworks"},{"buildFiles":[{"fileReference":"bfdfe7dc352907fc980b868725387e982d47e6d9df96d21c4ac2b81d688ce03c","guid":"bfdfe7dc352907fc980b868725387e98d0014e12ed3501862c4de827fac7da0f"}],"guid":"bfdfe7dc352907fc980b868725387e986513751e8f6bd2073ffe3aec8fdcc83a","type":"com.apple.buildphase.resources"}],"buildRules":[],"dependencies":[],"guid":"bfdfe7dc352907fc980b868725387e98ac39a888cfe1cb710fc82f373c10df1a","name":"google_navigation_flutter-google_navigation_flutter_privacy_info","productReference":{"guid":"bfdfe7dc352907fc980b868725387e98f30d584e5ae71ad240f9f8224d5a008f","name":"google_navigation_flutter_privacy_info.bundle","type":"product"},"productTypeIdentifier":"com.apple.product-type.bundle","provisioningSourceData":[{"bundleIdentifierFromInfoPlist":"${PRODUCT_BUNDLE_IDENTIFIER}","configurationName":"Debug","provisioningStyle":0},{"bundleIdentifierFromInfoPlist":"${PRODUCT_BUNDLE_IDENTIFIER}","configurationName":"Profile","provisioningStyle":0},{"bundleIdentifierFromInfoPlist":"${PRODUCT_BUNDLE_IDENTIFIER}","configurationName":"Release","provisioningStyle":0}],"type":"standard"}

View File

@ -1 +0,0 @@
{"buildConfigurations":[{"baseConfigurationFileReference":"bfdfe7dc352907fc980b868725387e9809a5e132d0bce4b05c12cea0e4f26a78","buildSettings":{"CODE_SIGNING_ALLOWED":"NO","CODE_SIGNING_IDENTITY":"-","CODE_SIGNING_REQUIRED":"NO","CONFIGURATION_BUILD_DIR":"$(BUILD_DIR)/$(CONFIGURATION)$(EFFECTIVE_PLATFORM_NAME)/permission_handler_apple","EXPANDED_CODE_SIGN_IDENTITY":"-","IBSC_MODULE":"permission_handler_apple","INFOPLIST_FILE":"Target Support Files/permission_handler_apple/ResourceBundle-permission_handler_apple_privacy-permission_handler_apple-Info.plist","IPHONEOS_DEPLOYMENT_TARGET":"9.0","ONLY_ACTIVE_ARCH":"NO","PRODUCT_NAME":"permission_handler_apple_privacy","SDKROOT":"iphoneos","SKIP_INSTALL":"YES","TARGETED_DEVICE_FAMILY":"1,2","WRAPPER_EXTENSION":"bundle"},"guid":"bfdfe7dc352907fc980b868725387e98f65e10bb99e854e0cc7509debc58eedd","name":"Debug"},{"baseConfigurationFileReference":"bfdfe7dc352907fc980b868725387e985b9fdf58418402c948d4991249b30dbb","buildSettings":{"CLANG_ENABLE_OBJC_WEAK":"NO","CODE_SIGNING_ALLOWED":"NO","CODE_SIGNING_IDENTITY":"-","CODE_SIGNING_REQUIRED":"NO","CONFIGURATION_BUILD_DIR":"$(BUILD_DIR)/$(CONFIGURATION)$(EFFECTIVE_PLATFORM_NAME)/permission_handler_apple","EXPANDED_CODE_SIGN_IDENTITY":"-","IBSC_MODULE":"permission_handler_apple","INFOPLIST_FILE":"Target Support Files/permission_handler_apple/ResourceBundle-permission_handler_apple_privacy-permission_handler_apple-Info.plist","IPHONEOS_DEPLOYMENT_TARGET":"9.0","PRODUCT_NAME":"permission_handler_apple_privacy","SDKROOT":"iphoneos","SKIP_INSTALL":"YES","TARGETED_DEVICE_FAMILY":"1,2","VALIDATE_PRODUCT":"YES","WRAPPER_EXTENSION":"bundle"},"guid":"bfdfe7dc352907fc980b868725387e98775205cab9cc69004f1881a742848df9","name":"Profile"},{"baseConfigurationFileReference":"bfdfe7dc352907fc980b868725387e985b9fdf58418402c948d4991249b30dbb","buildSettings":{"CODE_SIGNING_ALLOWED":"NO","CODE_SIGNING_IDENTITY":"-","CODE_SIGNING_REQUIRED":"NO","CONFIGURATION_BUILD_DIR":"$(BUILD_DIR)/$(CONFIGURATION)$(EFFECTIVE_PLATFORM_NAME)/permission_handler_apple","EXPANDED_CODE_SIGN_IDENTITY":"-","IBSC_MODULE":"permission_handler_apple","INFOPLIST_FILE":"Target Support Files/permission_handler_apple/ResourceBundle-permission_handler_apple_privacy-permission_handler_apple-Info.plist","IPHONEOS_DEPLOYMENT_TARGET":"9.0","PRODUCT_NAME":"permission_handler_apple_privacy","SDKROOT":"iphoneos","SKIP_INSTALL":"YES","TARGETED_DEVICE_FAMILY":"1,2","WRAPPER_EXTENSION":"bundle"},"guid":"bfdfe7dc352907fc980b868725387e9861637f9d7c181f34b6eb91234bddf3bb","name":"Release"}],"buildPhases":[{"buildFiles":[],"guid":"bfdfe7dc352907fc980b868725387e98c999d175d05b2ce9d81b763071090865","type":"com.apple.buildphase.sources"},{"buildFiles":[],"guid":"bfdfe7dc352907fc980b868725387e98f889cfefd0bf287bd2013035807b1424","type":"com.apple.buildphase.frameworks"},{"buildFiles":[{"fileReference":"bfdfe7dc352907fc980b868725387e98b0de2f55c3fa51a2b67df83a91ae1558","guid":"bfdfe7dc352907fc980b868725387e98ef8b7facad315a0cd677b9b49dcabc5c"}],"guid":"bfdfe7dc352907fc980b868725387e980919813a8512a4a7a7c118b27811ad03","type":"com.apple.buildphase.resources"}],"buildRules":[],"dependencies":[],"guid":"bfdfe7dc352907fc980b868725387e9802f35ab680609a626ebd2ddd692a3822","name":"permission_handler_apple-permission_handler_apple_privacy","productReference":{"guid":"bfdfe7dc352907fc980b868725387e983e9a904e8a35cb34b69458780be142b3","name":"permission_handler_apple_privacy.bundle","type":"product"},"productTypeIdentifier":"com.apple.product-type.bundle","provisioningSourceData":[{"bundleIdentifierFromInfoPlist":"${PRODUCT_BUNDLE_IDENTIFIER}","configurationName":"Debug","provisioningStyle":0},{"bundleIdentifierFromInfoPlist":"${PRODUCT_BUNDLE_IDENTIFIER}","configurationName":"Profile","provisioningStyle":0},{"bundleIdentifierFromInfoPlist":"${PRODUCT_BUNDLE_IDENTIFIER}","configurationName":"Release","provisioningStyle":0}],"type":"standard"}

View File

@ -1 +0,0 @@
{"buildConfigurations":[{"baseConfigurationFileReference":"bfdfe7dc352907fc980b868725387e98f1732f50402cdd4e9a853378f00de46e","buildSettings":{"CODE_SIGNING_ALLOWED":"NO","CODE_SIGNING_IDENTITY":"-","CODE_SIGNING_REQUIRED":"NO","CONFIGURATION_BUILD_DIR":"$(BUILD_DIR)/$(CONFIGURATION)$(EFFECTIVE_PLATFORM_NAME)/shared_preferences_foundation","EXPANDED_CODE_SIGN_IDENTITY":"-","IBSC_MODULE":"shared_preferences_foundation","INFOPLIST_FILE":"Target Support Files/shared_preferences_foundation/ResourceBundle-shared_preferences_foundation_privacy-shared_preferences_foundation-Info.plist","IPHONEOS_DEPLOYMENT_TARGET":"13.0","ONLY_ACTIVE_ARCH":"NO","PRODUCT_NAME":"shared_preferences_foundation_privacy","SDKROOT":"iphoneos","SKIP_INSTALL":"YES","TARGETED_DEVICE_FAMILY":"1,2","WRAPPER_EXTENSION":"bundle"},"guid":"bfdfe7dc352907fc980b868725387e9879cd8058020a002dfe7e9109fbf5676e","name":"Debug"},{"baseConfigurationFileReference":"bfdfe7dc352907fc980b868725387e98039c35627e5877b409f4593c36b6bc93","buildSettings":{"CLANG_ENABLE_OBJC_WEAK":"NO","CODE_SIGNING_ALLOWED":"NO","CODE_SIGNING_IDENTITY":"-","CODE_SIGNING_REQUIRED":"NO","CONFIGURATION_BUILD_DIR":"$(BUILD_DIR)/$(CONFIGURATION)$(EFFECTIVE_PLATFORM_NAME)/shared_preferences_foundation","EXPANDED_CODE_SIGN_IDENTITY":"-","IBSC_MODULE":"shared_preferences_foundation","INFOPLIST_FILE":"Target Support Files/shared_preferences_foundation/ResourceBundle-shared_preferences_foundation_privacy-shared_preferences_foundation-Info.plist","IPHONEOS_DEPLOYMENT_TARGET":"13.0","PRODUCT_NAME":"shared_preferences_foundation_privacy","SDKROOT":"iphoneos","SKIP_INSTALL":"YES","TARGETED_DEVICE_FAMILY":"1,2","VALIDATE_PRODUCT":"YES","WRAPPER_EXTENSION":"bundle"},"guid":"bfdfe7dc352907fc980b868725387e9805a105575397cbcb6f40d7a2393aedf9","name":"Profile"},{"baseConfigurationFileReference":"bfdfe7dc352907fc980b868725387e98039c35627e5877b409f4593c36b6bc93","buildSettings":{"CODE_SIGNING_ALLOWED":"NO","CODE_SIGNING_IDENTITY":"-","CODE_SIGNING_REQUIRED":"NO","CONFIGURATION_BUILD_DIR":"$(BUILD_DIR)/$(CONFIGURATION)$(EFFECTIVE_PLATFORM_NAME)/shared_preferences_foundation","EXPANDED_CODE_SIGN_IDENTITY":"-","IBSC_MODULE":"shared_preferences_foundation","INFOPLIST_FILE":"Target Support Files/shared_preferences_foundation/ResourceBundle-shared_preferences_foundation_privacy-shared_preferences_foundation-Info.plist","IPHONEOS_DEPLOYMENT_TARGET":"13.0","PRODUCT_NAME":"shared_preferences_foundation_privacy","SDKROOT":"iphoneos","SKIP_INSTALL":"YES","TARGETED_DEVICE_FAMILY":"1,2","WRAPPER_EXTENSION":"bundle"},"guid":"bfdfe7dc352907fc980b868725387e98e70c783857c2a5a5e29a7fde8d966f22","name":"Release"}],"buildPhases":[{"buildFiles":[],"guid":"bfdfe7dc352907fc980b868725387e98df4882dcf02067cf04442b892634f193","type":"com.apple.buildphase.sources"},{"buildFiles":[],"guid":"bfdfe7dc352907fc980b868725387e985e634fd0c7bd475437a24d1d427d6da6","type":"com.apple.buildphase.frameworks"},{"buildFiles":[{"fileReference":"bfdfe7dc352907fc980b868725387e989f3d0465172c96a1dfd43d18de6cea13","guid":"bfdfe7dc352907fc980b868725387e98fc24d024cce2dcad61967ac7ff914de5"}],"guid":"bfdfe7dc352907fc980b868725387e98c57e20215c49486fc7747013bd122091","type":"com.apple.buildphase.resources"}],"buildRules":[],"dependencies":[],"guid":"bfdfe7dc352907fc980b868725387e98e0be3b0d5ad56f1985578b1f97431765","name":"shared_preferences_foundation-shared_preferences_foundation_privacy","productReference":{"guid":"bfdfe7dc352907fc980b868725387e98ad625504a4c1e61077bbfd33bd1d1785","name":"shared_preferences_foundation_privacy.bundle","type":"product"},"productTypeIdentifier":"com.apple.product-type.bundle","provisioningSourceData":[{"bundleIdentifierFromInfoPlist":"${PRODUCT_BUNDLE_IDENTIFIER}","configurationName":"Debug","provisioningStyle":0},{"bundleIdentifierFromInfoPlist":"${PRODUCT_BUNDLE_IDENTIFIER}","configurationName":"Profile","provisioningStyle":0},{"bundleIdentifierFromInfoPlist":"${PRODUCT_BUNDLE_IDENTIFIER}","configurationName":"Release","provisioningStyle":0}],"type":"standard"}

View File

@ -1 +0,0 @@
{"buildConfigurations":[{"baseConfigurationFileReference":"bfdfe7dc352907fc980b868725387e98a553cb6838f3dba8465d8e4475e67146","buildSettings":{"CODE_SIGNING_ALLOWED":"NO","CODE_SIGNING_IDENTITY":"-","CODE_SIGNING_REQUIRED":"NO","CONFIGURATION_BUILD_DIR":"$(BUILD_DIR)/$(CONFIGURATION)$(EFFECTIVE_PLATFORM_NAME)/AppAuth","EXPANDED_CODE_SIGN_IDENTITY":"-","IBSC_MODULE":"AppAuth","INFOPLIST_FILE":"Target Support Files/AppAuth/ResourceBundle-AppAuthCore_Privacy-AppAuth-Info.plist","IPHONEOS_DEPLOYMENT_TARGET":"9.0","ONLY_ACTIVE_ARCH":"NO","PRODUCT_NAME":"AppAuthCore_Privacy","SDKROOT":"iphoneos","SKIP_INSTALL":"YES","TARGETED_DEVICE_FAMILY":"1,2","WRAPPER_EXTENSION":"bundle"},"guid":"bfdfe7dc352907fc980b868725387e987a58b89697ac11d5cd7aa9ea7e5ed515","name":"Debug"},{"baseConfigurationFileReference":"bfdfe7dc352907fc980b868725387e98853e49f132a195ea0f4a3955deb01309","buildSettings":{"CLANG_ENABLE_OBJC_WEAK":"NO","CODE_SIGNING_ALLOWED":"NO","CODE_SIGNING_IDENTITY":"-","CODE_SIGNING_REQUIRED":"NO","CONFIGURATION_BUILD_DIR":"$(BUILD_DIR)/$(CONFIGURATION)$(EFFECTIVE_PLATFORM_NAME)/AppAuth","EXPANDED_CODE_SIGN_IDENTITY":"-","IBSC_MODULE":"AppAuth","INFOPLIST_FILE":"Target Support Files/AppAuth/ResourceBundle-AppAuthCore_Privacy-AppAuth-Info.plist","IPHONEOS_DEPLOYMENT_TARGET":"9.0","PRODUCT_NAME":"AppAuthCore_Privacy","SDKROOT":"iphoneos","SKIP_INSTALL":"YES","TARGETED_DEVICE_FAMILY":"1,2","VALIDATE_PRODUCT":"YES","WRAPPER_EXTENSION":"bundle"},"guid":"bfdfe7dc352907fc980b868725387e988e9e2d6b0ade9690428756259b370014","name":"Profile"},{"baseConfigurationFileReference":"bfdfe7dc352907fc980b868725387e98853e49f132a195ea0f4a3955deb01309","buildSettings":{"CODE_SIGNING_ALLOWED":"NO","CODE_SIGNING_IDENTITY":"-","CODE_SIGNING_REQUIRED":"NO","CONFIGURATION_BUILD_DIR":"$(BUILD_DIR)/$(CONFIGURATION)$(EFFECTIVE_PLATFORM_NAME)/AppAuth","EXPANDED_CODE_SIGN_IDENTITY":"-","IBSC_MODULE":"AppAuth","INFOPLIST_FILE":"Target Support Files/AppAuth/ResourceBundle-AppAuthCore_Privacy-AppAuth-Info.plist","IPHONEOS_DEPLOYMENT_TARGET":"9.0","PRODUCT_NAME":"AppAuthCore_Privacy","SDKROOT":"iphoneos","SKIP_INSTALL":"YES","TARGETED_DEVICE_FAMILY":"1,2","WRAPPER_EXTENSION":"bundle"},"guid":"bfdfe7dc352907fc980b868725387e98f0899d0d854cbd1492e7012d43dd11f1","name":"Release"}],"buildPhases":[{"buildFiles":[],"guid":"bfdfe7dc352907fc980b868725387e98f004de48ea7a130bf06566a96a8fd5b8","type":"com.apple.buildphase.sources"},{"buildFiles":[],"guid":"bfdfe7dc352907fc980b868725387e987a8ae5e49417276ebd4edead04f3b8af","type":"com.apple.buildphase.frameworks"},{"buildFiles":[{"fileReference":"bfdfe7dc352907fc980b868725387e989d84a52718b57739049593ec7bcff072","guid":"bfdfe7dc352907fc980b868725387e98a5c2b76e6cf4ecbfd0686fc649197696"}],"guid":"bfdfe7dc352907fc980b868725387e9877692e6ddd8fbe2f1e7a91acf593f15e","type":"com.apple.buildphase.resources"}],"buildRules":[],"dependencies":[],"guid":"bfdfe7dc352907fc980b868725387e98410c96b4e26fc36411e63b84c3491605","name":"AppAuth-AppAuthCore_Privacy","productReference":{"guid":"bfdfe7dc352907fc980b868725387e986d566b0f46776138a3ba88837e01b2bf","name":"AppAuthCore_Privacy.bundle","type":"product"},"productTypeIdentifier":"com.apple.product-type.bundle","provisioningSourceData":[{"bundleIdentifierFromInfoPlist":"${PRODUCT_BUNDLE_IDENTIFIER}","configurationName":"Debug","provisioningStyle":0},{"bundleIdentifierFromInfoPlist":"${PRODUCT_BUNDLE_IDENTIFIER}","configurationName":"Profile","provisioningStyle":0},{"bundleIdentifierFromInfoPlist":"${PRODUCT_BUNDLE_IDENTIFIER}","configurationName":"Release","provisioningStyle":0}],"type":"standard"}

View File

@ -1 +0,0 @@
{"buildConfigurations":[{"baseConfigurationFileReference":"bfdfe7dc352907fc980b868725387e9865adcbbcb4ec1b3c452a31ac4e82f814","buildSettings":{"ASSETCATALOG_COMPILER_APPICON_NAME":"AppIcon","ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME":"AccentColor","CLANG_ENABLE_OBJC_WEAK":"NO","ENABLE_USER_SCRIPT_SANDBOXING":"NO","IPHONEOS_DEPLOYMENT_TARGET":"13.0","LD_RUNPATH_SEARCH_PATHS":"$(inherited) @executable_path/Frameworks","ONLY_ACTIVE_ARCH":"NO","SDKROOT":"iphoneos","TARGETED_DEVICE_FAMILY":"1,2"},"guid":"bfdfe7dc352907fc980b868725387e980bc977b873df9b0e01b3c822e5c77429","name":"Debug"},{"baseConfigurationFileReference":"bfdfe7dc352907fc980b868725387e98236686288fbfe9faab703dc7debfa1a3","buildSettings":{"ASSETCATALOG_COMPILER_APPICON_NAME":"AppIcon","ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME":"AccentColor","CLANG_ENABLE_OBJC_WEAK":"NO","ENABLE_USER_SCRIPT_SANDBOXING":"NO","IPHONEOS_DEPLOYMENT_TARGET":"13.0","LD_RUNPATH_SEARCH_PATHS":"$(inherited) @executable_path/Frameworks","SDKROOT":"iphoneos","TARGETED_DEVICE_FAMILY":"1,2","VALIDATE_PRODUCT":"YES"},"guid":"bfdfe7dc352907fc980b868725387e98b75274b69084014a6a5ac37ea7a9d4bc","name":"Profile"},{"baseConfigurationFileReference":"bfdfe7dc352907fc980b868725387e98236686288fbfe9faab703dc7debfa1a3","buildSettings":{"ASSETCATALOG_COMPILER_APPICON_NAME":"AppIcon","ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME":"AccentColor","CLANG_ENABLE_OBJC_WEAK":"NO","ENABLE_USER_SCRIPT_SANDBOXING":"NO","IPHONEOS_DEPLOYMENT_TARGET":"13.0","LD_RUNPATH_SEARCH_PATHS":"$(inherited) @executable_path/Frameworks","SDKROOT":"iphoneos","TARGETED_DEVICE_FAMILY":"1,2","VALIDATE_PRODUCT":"YES"},"guid":"bfdfe7dc352907fc980b868725387e988b8e6347e534cb57e9bb1b22dc47b716","name":"Release"}],"buildPhases":[],"buildRules":[],"dependencies":[],"guid":"bfdfe7dc352907fc980b868725387e989da425bb6d6d5d8dbb95e4afffb82217","name":"Flutter","provisioningSourceData":[{"bundleIdentifierFromInfoPlist":"","configurationName":"Debug","provisioningStyle":0},{"bundleIdentifierFromInfoPlist":"","configurationName":"Profile","provisioningStyle":0},{"bundleIdentifierFromInfoPlist":"","configurationName":"Release","provisioningStyle":0}],"type":"aggregate"}

View File

@ -1 +0,0 @@
{"buildConfigurations":[{"baseConfigurationFileReference":"bfdfe7dc352907fc980b868725387e98d1b134ad118e25bf8a1dc2b9ce79ad5d","buildSettings":{"ASSETCATALOG_COMPILER_APPICON_NAME":"AppIcon","ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME":"AccentColor","CLANG_ENABLE_OBJC_WEAK":"NO","ENABLE_USER_SCRIPT_SANDBOXING":"NO","IPHONEOS_DEPLOYMENT_TARGET":"16.0","LD_RUNPATH_SEARCH_PATHS":"$(inherited) @executable_path/Frameworks","ONLY_ACTIVE_ARCH":"NO","SDKROOT":"iphoneos","TARGETED_DEVICE_FAMILY":"1,2"},"guid":"bfdfe7dc352907fc980b868725387e98d9973325385d84e0e77b85c54100d070","name":"Debug"},{"baseConfigurationFileReference":"bfdfe7dc352907fc980b868725387e9896631f0702bcaa9fd24776311b2cd48b","buildSettings":{"ASSETCATALOG_COMPILER_APPICON_NAME":"AppIcon","ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME":"AccentColor","CLANG_ENABLE_OBJC_WEAK":"NO","ENABLE_USER_SCRIPT_SANDBOXING":"NO","IPHONEOS_DEPLOYMENT_TARGET":"16.0","LD_RUNPATH_SEARCH_PATHS":"$(inherited) @executable_path/Frameworks","SDKROOT":"iphoneos","TARGETED_DEVICE_FAMILY":"1,2","VALIDATE_PRODUCT":"YES"},"guid":"bfdfe7dc352907fc980b868725387e98ce9c972125fa8e60135d49830ab80fb9","name":"Profile"},{"baseConfigurationFileReference":"bfdfe7dc352907fc980b868725387e9896631f0702bcaa9fd24776311b2cd48b","buildSettings":{"ASSETCATALOG_COMPILER_APPICON_NAME":"AppIcon","ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME":"AccentColor","CLANG_ENABLE_OBJC_WEAK":"NO","ENABLE_USER_SCRIPT_SANDBOXING":"NO","IPHONEOS_DEPLOYMENT_TARGET":"16.0","LD_RUNPATH_SEARCH_PATHS":"$(inherited) @executable_path/Frameworks","SDKROOT":"iphoneos","TARGETED_DEVICE_FAMILY":"1,2","VALIDATE_PRODUCT":"YES"},"guid":"bfdfe7dc352907fc980b868725387e9856cf668712b9a03308963239dfa31677","name":"Release"}],"buildPhases":[{"alwaysOutOfDate":"false","alwaysRunForInstallHdrs":"false","buildFiles":[],"emitEnvironment":"false","guid":"bfdfe7dc352907fc980b868725387e9875f5f7294d970da29740ea9d892ff270","inputFileListPaths":["${PODS_ROOT}/Target Support Files/GoogleNavigation/GoogleNavigation-xcframeworks-input-files.xcfilelist"],"inputFilePaths":[],"name":"[CP] Copy XCFrameworks","originalObjectID":"181935D79354233793EF8014128D7B3E","outputFileListPaths":["${PODS_ROOT}/Target Support Files/GoogleNavigation/GoogleNavigation-xcframeworks-output-files.xcfilelist"],"outputFilePaths":[],"sandboxingOverride":"basedOnBuildSetting","scriptContents":"\"${PODS_ROOT}/Target Support Files/GoogleNavigation/GoogleNavigation-xcframeworks.sh\"\n","shellPath":"/bin/sh","type":"com.apple.buildphase.shell-script"}],"buildRules":[],"dependencies":[{"guid":"bfdfe7dc352907fc980b868725387e9818352c54edac2258b91768852065ce5e","name":"GoogleMaps"},{"guid":"bfdfe7dc352907fc980b868725387e9840209c7fe78cadee2612d71e96cd4bdb","name":"GoogleNavigation-GoogleNavigationResources"}],"guid":"bfdfe7dc352907fc980b868725387e98282d9246524ea316059ab11846dac3ef","name":"GoogleNavigation","provisioningSourceData":[{"bundleIdentifierFromInfoPlist":"","configurationName":"Debug","provisioningStyle":0},{"bundleIdentifierFromInfoPlist":"","configurationName":"Profile","provisioningStyle":0},{"bundleIdentifierFromInfoPlist":"","configurationName":"Release","provisioningStyle":0}],"type":"aggregate"}

View File

@ -1 +0,0 @@
{"buildConfigurations":[{"baseConfigurationFileReference":"bfdfe7dc352907fc980b868725387e984bc9977ccf8ea20d6d7539e56853e7a7","buildSettings":{"CODE_SIGNING_ALLOWED":"NO","CODE_SIGNING_IDENTITY":"-","CODE_SIGNING_REQUIRED":"NO","CONFIGURATION_BUILD_DIR":"$(BUILD_DIR)/$(CONFIGURATION)$(EFFECTIVE_PLATFORM_NAME)/flutter_secure_storage","EXPANDED_CODE_SIGN_IDENTITY":"-","IBSC_MODULE":"flutter_secure_storage","INFOPLIST_FILE":"Target Support Files/flutter_secure_storage/ResourceBundle-flutter_secure_storage-flutter_secure_storage-Info.plist","IPHONEOS_DEPLOYMENT_TARGET":"9.0","ONLY_ACTIVE_ARCH":"NO","PRODUCT_NAME":"flutter_secure_storage","SDKROOT":"iphoneos","SKIP_INSTALL":"YES","TARGETED_DEVICE_FAMILY":"1,2","WRAPPER_EXTENSION":"bundle"},"guid":"bfdfe7dc352907fc980b868725387e989ab3841d28ec5dc90b678081bf09108e","name":"Debug"},{"baseConfigurationFileReference":"bfdfe7dc352907fc980b868725387e987ba9ebb08e6d0c50675558e6cd7e065a","buildSettings":{"CLANG_ENABLE_OBJC_WEAK":"NO","CODE_SIGNING_ALLOWED":"NO","CODE_SIGNING_IDENTITY":"-","CODE_SIGNING_REQUIRED":"NO","CONFIGURATION_BUILD_DIR":"$(BUILD_DIR)/$(CONFIGURATION)$(EFFECTIVE_PLATFORM_NAME)/flutter_secure_storage","EXPANDED_CODE_SIGN_IDENTITY":"-","IBSC_MODULE":"flutter_secure_storage","INFOPLIST_FILE":"Target Support Files/flutter_secure_storage/ResourceBundle-flutter_secure_storage-flutter_secure_storage-Info.plist","IPHONEOS_DEPLOYMENT_TARGET":"9.0","PRODUCT_NAME":"flutter_secure_storage","SDKROOT":"iphoneos","SKIP_INSTALL":"YES","TARGETED_DEVICE_FAMILY":"1,2","VALIDATE_PRODUCT":"YES","WRAPPER_EXTENSION":"bundle"},"guid":"bfdfe7dc352907fc980b868725387e98d38c1e976fc7da67866ec3fcfa333311","name":"Profile"},{"baseConfigurationFileReference":"bfdfe7dc352907fc980b868725387e987ba9ebb08e6d0c50675558e6cd7e065a","buildSettings":{"CODE_SIGNING_ALLOWED":"NO","CODE_SIGNING_IDENTITY":"-","CODE_SIGNING_REQUIRED":"NO","CONFIGURATION_BUILD_DIR":"$(BUILD_DIR)/$(CONFIGURATION)$(EFFECTIVE_PLATFORM_NAME)/flutter_secure_storage","EXPANDED_CODE_SIGN_IDENTITY":"-","IBSC_MODULE":"flutter_secure_storage","INFOPLIST_FILE":"Target Support Files/flutter_secure_storage/ResourceBundle-flutter_secure_storage-flutter_secure_storage-Info.plist","IPHONEOS_DEPLOYMENT_TARGET":"9.0","PRODUCT_NAME":"flutter_secure_storage","SDKROOT":"iphoneos","SKIP_INSTALL":"YES","TARGETED_DEVICE_FAMILY":"1,2","WRAPPER_EXTENSION":"bundle"},"guid":"bfdfe7dc352907fc980b868725387e98c4a2379731c5216c2d2089adf584eeee","name":"Release"}],"buildPhases":[{"buildFiles":[],"guid":"bfdfe7dc352907fc980b868725387e9872fce04d6b49a0cabde6c94673d86883","type":"com.apple.buildphase.sources"},{"buildFiles":[],"guid":"bfdfe7dc352907fc980b868725387e989dcf0d33a47d0459c2fe70f7c8f5556e","type":"com.apple.buildphase.frameworks"},{"buildFiles":[{"fileReference":"bfdfe7dc352907fc980b868725387e98d3e6170d25aa51f31d45d6eb8716104d","guid":"bfdfe7dc352907fc980b868725387e98d4f9254b31fc7fa8f7875fceee623814"}],"guid":"bfdfe7dc352907fc980b868725387e981d61c12e0ee3551b2bb5e6eaddede74f","type":"com.apple.buildphase.resources"}],"buildRules":[],"dependencies":[],"guid":"bfdfe7dc352907fc980b868725387e98a0220561f537715e864e45aed9ae8b8b","name":"flutter_secure_storage-flutter_secure_storage","productReference":{"guid":"bfdfe7dc352907fc980b868725387e989548ba3fd96e73f640dce7442408204f","name":"flutter_secure_storage.bundle","type":"product"},"productTypeIdentifier":"com.apple.product-type.bundle","provisioningSourceData":[{"bundleIdentifierFromInfoPlist":"${PRODUCT_BUNDLE_IDENTIFIER}","configurationName":"Debug","provisioningStyle":0},{"bundleIdentifierFromInfoPlist":"${PRODUCT_BUNDLE_IDENTIFIER}","configurationName":"Profile","provisioningStyle":0},{"bundleIdentifierFromInfoPlist":"${PRODUCT_BUNDLE_IDENTIFIER}","configurationName":"Release","provisioningStyle":0}],"type":"standard"}

View File

@ -1 +0,0 @@
{"buildConfigurations":[{"baseConfigurationFileReference":"bfdfe7dc352907fc980b868725387e98186e982df37db89fe49bcda3a9cd9b73","buildSettings":{"CODE_SIGNING_ALLOWED":"NO","CODE_SIGNING_IDENTITY":"-","CODE_SIGNING_REQUIRED":"NO","CONFIGURATION_BUILD_DIR":"$(BUILD_DIR)/$(CONFIGURATION)$(EFFECTIVE_PLATFORM_NAME)/url_launcher_ios","EXPANDED_CODE_SIGN_IDENTITY":"-","IBSC_MODULE":"url_launcher_ios","INFOPLIST_FILE":"Target Support Files/url_launcher_ios/ResourceBundle-url_launcher_ios_privacy-url_launcher_ios-Info.plist","IPHONEOS_DEPLOYMENT_TARGET":"13.0","ONLY_ACTIVE_ARCH":"NO","PRODUCT_NAME":"url_launcher_ios_privacy","SDKROOT":"iphoneos","SKIP_INSTALL":"YES","TARGETED_DEVICE_FAMILY":"1,2","WRAPPER_EXTENSION":"bundle"},"guid":"bfdfe7dc352907fc980b868725387e989e8a5e6f3fd69e0c38b557413c823639","name":"Debug"},{"baseConfigurationFileReference":"bfdfe7dc352907fc980b868725387e98ace0c0440e4b53ba3de29900f7d4bb7f","buildSettings":{"CLANG_ENABLE_OBJC_WEAK":"NO","CODE_SIGNING_ALLOWED":"NO","CODE_SIGNING_IDENTITY":"-","CODE_SIGNING_REQUIRED":"NO","CONFIGURATION_BUILD_DIR":"$(BUILD_DIR)/$(CONFIGURATION)$(EFFECTIVE_PLATFORM_NAME)/url_launcher_ios","EXPANDED_CODE_SIGN_IDENTITY":"-","IBSC_MODULE":"url_launcher_ios","INFOPLIST_FILE":"Target Support Files/url_launcher_ios/ResourceBundle-url_launcher_ios_privacy-url_launcher_ios-Info.plist","IPHONEOS_DEPLOYMENT_TARGET":"13.0","PRODUCT_NAME":"url_launcher_ios_privacy","SDKROOT":"iphoneos","SKIP_INSTALL":"YES","TARGETED_DEVICE_FAMILY":"1,2","VALIDATE_PRODUCT":"YES","WRAPPER_EXTENSION":"bundle"},"guid":"bfdfe7dc352907fc980b868725387e9840fb88b5d1dc4e5df4485e732c56885d","name":"Profile"},{"baseConfigurationFileReference":"bfdfe7dc352907fc980b868725387e98ace0c0440e4b53ba3de29900f7d4bb7f","buildSettings":{"CODE_SIGNING_ALLOWED":"NO","CODE_SIGNING_IDENTITY":"-","CODE_SIGNING_REQUIRED":"NO","CONFIGURATION_BUILD_DIR":"$(BUILD_DIR)/$(CONFIGURATION)$(EFFECTIVE_PLATFORM_NAME)/url_launcher_ios","EXPANDED_CODE_SIGN_IDENTITY":"-","IBSC_MODULE":"url_launcher_ios","INFOPLIST_FILE":"Target Support Files/url_launcher_ios/ResourceBundle-url_launcher_ios_privacy-url_launcher_ios-Info.plist","IPHONEOS_DEPLOYMENT_TARGET":"13.0","PRODUCT_NAME":"url_launcher_ios_privacy","SDKROOT":"iphoneos","SKIP_INSTALL":"YES","TARGETED_DEVICE_FAMILY":"1,2","WRAPPER_EXTENSION":"bundle"},"guid":"bfdfe7dc352907fc980b868725387e986a11246f198fb04d6c8f397cabfd38d8","name":"Release"}],"buildPhases":[{"buildFiles":[],"guid":"bfdfe7dc352907fc980b868725387e98abaa9b812aad944508dbf05eefa752b5","type":"com.apple.buildphase.sources"},{"buildFiles":[],"guid":"bfdfe7dc352907fc980b868725387e984caa2f1d777ea389c7ee7db8858444e5","type":"com.apple.buildphase.frameworks"},{"buildFiles":[{"fileReference":"bfdfe7dc352907fc980b868725387e98746b0938be2ee934824b330d686857af","guid":"bfdfe7dc352907fc980b868725387e98f6e1a6b3a4d617970ced59dd8a8b6d47"}],"guid":"bfdfe7dc352907fc980b868725387e982146574225d6b9a7ff59315708878b61","type":"com.apple.buildphase.resources"}],"buildRules":[],"dependencies":[],"guid":"bfdfe7dc352907fc980b868725387e9891b3b8cc56823cdea4b418e009a423b2","name":"url_launcher_ios-url_launcher_ios_privacy","productReference":{"guid":"bfdfe7dc352907fc980b868725387e9827df8da513ac7d6928fc311b53a7155d","name":"url_launcher_ios_privacy.bundle","type":"product"},"productTypeIdentifier":"com.apple.product-type.bundle","provisioningSourceData":[{"bundleIdentifierFromInfoPlist":"${PRODUCT_BUNDLE_IDENTIFIER}","configurationName":"Debug","provisioningStyle":0},{"bundleIdentifierFromInfoPlist":"${PRODUCT_BUNDLE_IDENTIFIER}","configurationName":"Profile","provisioningStyle":0},{"bundleIdentifierFromInfoPlist":"${PRODUCT_BUNDLE_IDENTIFIER}","configurationName":"Release","provisioningStyle":0}],"type":"standard"}

View File

@ -1 +0,0 @@
{"buildConfigurations":[{"baseConfigurationFileReference":"bfdfe7dc352907fc980b868725387e9840ae838f8ac69bc67263efbe8dc323d7","buildSettings":{"CODE_SIGNING_ALLOWED":"NO","CODE_SIGNING_IDENTITY":"-","CODE_SIGNING_REQUIRED":"NO","CONFIGURATION_BUILD_DIR":"$(BUILD_DIR)/$(CONFIGURATION)$(EFFECTIVE_PLATFORM_NAME)/GoogleMaps","EXPANDED_CODE_SIGN_IDENTITY":"-","IBSC_MODULE":"GoogleMaps","INFOPLIST_FILE":"Target Support Files/GoogleMaps/ResourceBundle-GoogleMapsResources-GoogleMaps-Info.plist","IPHONEOS_DEPLOYMENT_TARGET":"16.0","ONLY_ACTIVE_ARCH":"NO","PRODUCT_NAME":"GoogleMapsResources","SDKROOT":"iphoneos","SKIP_INSTALL":"YES","TARGETED_DEVICE_FAMILY":"1,2","WRAPPER_EXTENSION":"bundle"},"guid":"bfdfe7dc352907fc980b868725387e983191cda542837fa6770197504daef473","name":"Debug"},{"baseConfigurationFileReference":"bfdfe7dc352907fc980b868725387e989bf599cd5d4dd79de867d43540047de6","buildSettings":{"CLANG_ENABLE_OBJC_WEAK":"NO","CODE_SIGNING_ALLOWED":"NO","CODE_SIGNING_IDENTITY":"-","CODE_SIGNING_REQUIRED":"NO","CONFIGURATION_BUILD_DIR":"$(BUILD_DIR)/$(CONFIGURATION)$(EFFECTIVE_PLATFORM_NAME)/GoogleMaps","EXPANDED_CODE_SIGN_IDENTITY":"-","IBSC_MODULE":"GoogleMaps","INFOPLIST_FILE":"Target Support Files/GoogleMaps/ResourceBundle-GoogleMapsResources-GoogleMaps-Info.plist","IPHONEOS_DEPLOYMENT_TARGET":"16.0","PRODUCT_NAME":"GoogleMapsResources","SDKROOT":"iphoneos","SKIP_INSTALL":"YES","TARGETED_DEVICE_FAMILY":"1,2","VALIDATE_PRODUCT":"YES","WRAPPER_EXTENSION":"bundle"},"guid":"bfdfe7dc352907fc980b868725387e989d7a2fe6a6e4ee7bcdcad21a77de035e","name":"Profile"},{"baseConfigurationFileReference":"bfdfe7dc352907fc980b868725387e989bf599cd5d4dd79de867d43540047de6","buildSettings":{"CODE_SIGNING_ALLOWED":"NO","CODE_SIGNING_IDENTITY":"-","CODE_SIGNING_REQUIRED":"NO","CONFIGURATION_BUILD_DIR":"$(BUILD_DIR)/$(CONFIGURATION)$(EFFECTIVE_PLATFORM_NAME)/GoogleMaps","EXPANDED_CODE_SIGN_IDENTITY":"-","IBSC_MODULE":"GoogleMaps","INFOPLIST_FILE":"Target Support Files/GoogleMaps/ResourceBundle-GoogleMapsResources-GoogleMaps-Info.plist","IPHONEOS_DEPLOYMENT_TARGET":"16.0","PRODUCT_NAME":"GoogleMapsResources","SDKROOT":"iphoneos","SKIP_INSTALL":"YES","TARGETED_DEVICE_FAMILY":"1,2","WRAPPER_EXTENSION":"bundle"},"guid":"bfdfe7dc352907fc980b868725387e9849ea1eac4d4a03ad836d326f824a2188","name":"Release"}],"buildPhases":[{"buildFiles":[],"guid":"bfdfe7dc352907fc980b868725387e98d9a6fbff63b4b45308ad1df71da4d517","type":"com.apple.buildphase.sources"},{"buildFiles":[],"guid":"bfdfe7dc352907fc980b868725387e980332ecdb9230e6c20a735a5b4c6c417b","type":"com.apple.buildphase.frameworks"},{"buildFiles":[{"fileReference":"bfdfe7dc352907fc980b868725387e98408aeea65f8576fdbf766a4c1aafc7f3","guid":"bfdfe7dc352907fc980b868725387e98b4e7bb0451966e85948d13349e7d172a"}],"guid":"bfdfe7dc352907fc980b868725387e985422bd043ecb80a62697fe2c7436a8fe","type":"com.apple.buildphase.resources"}],"buildRules":[],"dependencies":[],"guid":"bfdfe7dc352907fc980b868725387e9877354dc0c1379e634078de2da2deba6b","name":"GoogleMaps-GoogleMapsResources","productReference":{"guid":"bfdfe7dc352907fc980b868725387e98e1226e3627f386c3cc556b927e8c995d","name":"GoogleMapsResources.bundle","type":"product"},"productTypeIdentifier":"com.apple.product-type.bundle","provisioningSourceData":[{"bundleIdentifierFromInfoPlist":"${PRODUCT_BUNDLE_IDENTIFIER}","configurationName":"Debug","provisioningStyle":0},{"bundleIdentifierFromInfoPlist":"${PRODUCT_BUNDLE_IDENTIFIER}","configurationName":"Profile","provisioningStyle":0},{"bundleIdentifierFromInfoPlist":"${PRODUCT_BUNDLE_IDENTIFIER}","configurationName":"Release","provisioningStyle":0}],"type":"standard"}

View File

@ -1 +0,0 @@
{"buildConfigurations":[{"baseConfigurationFileReference":"bfdfe7dc352907fc980b868725387e9883fd68a4145abded79f7758856e20d05","buildSettings":{"CODE_SIGNING_ALLOWED":"NO","CODE_SIGNING_IDENTITY":"-","CODE_SIGNING_REQUIRED":"NO","CONFIGURATION_BUILD_DIR":"$(BUILD_DIR)/$(CONFIGURATION)$(EFFECTIVE_PLATFORM_NAME)/flutter_appauth","EXPANDED_CODE_SIGN_IDENTITY":"-","IBSC_MODULE":"flutter_appauth","INFOPLIST_FILE":"Target Support Files/flutter_appauth/ResourceBundle-flutter_appauth_privacy-flutter_appauth-Info.plist","IPHONEOS_DEPLOYMENT_TARGET":"9.0","ONLY_ACTIVE_ARCH":"NO","PRODUCT_NAME":"flutter_appauth_privacy","SDKROOT":"iphoneos","SKIP_INSTALL":"YES","TARGETED_DEVICE_FAMILY":"1,2","WRAPPER_EXTENSION":"bundle"},"guid":"bfdfe7dc352907fc980b868725387e98712be270f11869f9b381a11c7fbc218c","name":"Debug"},{"baseConfigurationFileReference":"bfdfe7dc352907fc980b868725387e98a0363b33b26a3f20bb91d324a46ad52b","buildSettings":{"CLANG_ENABLE_OBJC_WEAK":"NO","CODE_SIGNING_ALLOWED":"NO","CODE_SIGNING_IDENTITY":"-","CODE_SIGNING_REQUIRED":"NO","CONFIGURATION_BUILD_DIR":"$(BUILD_DIR)/$(CONFIGURATION)$(EFFECTIVE_PLATFORM_NAME)/flutter_appauth","EXPANDED_CODE_SIGN_IDENTITY":"-","IBSC_MODULE":"flutter_appauth","INFOPLIST_FILE":"Target Support Files/flutter_appauth/ResourceBundle-flutter_appauth_privacy-flutter_appauth-Info.plist","IPHONEOS_DEPLOYMENT_TARGET":"9.0","PRODUCT_NAME":"flutter_appauth_privacy","SDKROOT":"iphoneos","SKIP_INSTALL":"YES","TARGETED_DEVICE_FAMILY":"1,2","VALIDATE_PRODUCT":"YES","WRAPPER_EXTENSION":"bundle"},"guid":"bfdfe7dc352907fc980b868725387e98a57417b5a0b537217b8dbe5ff734b33d","name":"Profile"},{"baseConfigurationFileReference":"bfdfe7dc352907fc980b868725387e98a0363b33b26a3f20bb91d324a46ad52b","buildSettings":{"CODE_SIGNING_ALLOWED":"NO","CODE_SIGNING_IDENTITY":"-","CODE_SIGNING_REQUIRED":"NO","CONFIGURATION_BUILD_DIR":"$(BUILD_DIR)/$(CONFIGURATION)$(EFFECTIVE_PLATFORM_NAME)/flutter_appauth","EXPANDED_CODE_SIGN_IDENTITY":"-","IBSC_MODULE":"flutter_appauth","INFOPLIST_FILE":"Target Support Files/flutter_appauth/ResourceBundle-flutter_appauth_privacy-flutter_appauth-Info.plist","IPHONEOS_DEPLOYMENT_TARGET":"9.0","PRODUCT_NAME":"flutter_appauth_privacy","SDKROOT":"iphoneos","SKIP_INSTALL":"YES","TARGETED_DEVICE_FAMILY":"1,2","WRAPPER_EXTENSION":"bundle"},"guid":"bfdfe7dc352907fc980b868725387e981e1b1c740d78f941c6f1c7eccfd76b80","name":"Release"}],"buildPhases":[{"buildFiles":[],"guid":"bfdfe7dc352907fc980b868725387e985f15189a3c329e6a4f4dda3f88970825","type":"com.apple.buildphase.sources"},{"buildFiles":[],"guid":"bfdfe7dc352907fc980b868725387e98476f591635cec8be63ece42c91ac4c5b","type":"com.apple.buildphase.frameworks"},{"buildFiles":[{"fileReference":"bfdfe7dc352907fc980b868725387e98ff28582d663ab87fc368c4e58763f76d","guid":"bfdfe7dc352907fc980b868725387e981eb65d04a62cc5ee4b5b0c5b3c7e475d"}],"guid":"bfdfe7dc352907fc980b868725387e980b635078f9eecc2f745b45194b3c499c","type":"com.apple.buildphase.resources"}],"buildRules":[],"dependencies":[],"guid":"bfdfe7dc352907fc980b868725387e9838116aba79bee5cd919637253bcf2ecc","name":"flutter_appauth-flutter_appauth_privacy","productReference":{"guid":"bfdfe7dc352907fc980b868725387e982460b3be5f4208d02013fc970dac2dce","name":"flutter_appauth_privacy.bundle","type":"product"},"productTypeIdentifier":"com.apple.product-type.bundle","provisioningSourceData":[{"bundleIdentifierFromInfoPlist":"${PRODUCT_BUNDLE_IDENTIFIER}","configurationName":"Debug","provisioningStyle":0},{"bundleIdentifierFromInfoPlist":"${PRODUCT_BUNDLE_IDENTIFIER}","configurationName":"Profile","provisioningStyle":0},{"bundleIdentifierFromInfoPlist":"${PRODUCT_BUNDLE_IDENTIFIER}","configurationName":"Release","provisioningStyle":0}],"type":"standard"}

View File

@ -1 +0,0 @@
{"buildConfigurations":[{"baseConfigurationFileReference":"bfdfe7dc352907fc980b868725387e98d1b134ad118e25bf8a1dc2b9ce79ad5d","buildSettings":{"CODE_SIGNING_ALLOWED":"NO","CODE_SIGNING_IDENTITY":"-","CODE_SIGNING_REQUIRED":"NO","CONFIGURATION_BUILD_DIR":"$(BUILD_DIR)/$(CONFIGURATION)$(EFFECTIVE_PLATFORM_NAME)/GoogleNavigation","EXPANDED_CODE_SIGN_IDENTITY":"-","IBSC_MODULE":"GoogleNavigation","INFOPLIST_FILE":"Target Support Files/GoogleNavigation/ResourceBundle-GoogleNavigationResources-GoogleNavigation-Info.plist","IPHONEOS_DEPLOYMENT_TARGET":"16.0","ONLY_ACTIVE_ARCH":"NO","PRODUCT_NAME":"GoogleNavigationResources","SDKROOT":"iphoneos","SKIP_INSTALL":"YES","TARGETED_DEVICE_FAMILY":"1,2","WRAPPER_EXTENSION":"bundle"},"guid":"bfdfe7dc352907fc980b868725387e981a62fab92f772ff9814442e829af40d3","name":"Debug"},{"baseConfigurationFileReference":"bfdfe7dc352907fc980b868725387e9896631f0702bcaa9fd24776311b2cd48b","buildSettings":{"CLANG_ENABLE_OBJC_WEAK":"NO","CODE_SIGNING_ALLOWED":"NO","CODE_SIGNING_IDENTITY":"-","CODE_SIGNING_REQUIRED":"NO","CONFIGURATION_BUILD_DIR":"$(BUILD_DIR)/$(CONFIGURATION)$(EFFECTIVE_PLATFORM_NAME)/GoogleNavigation","EXPANDED_CODE_SIGN_IDENTITY":"-","IBSC_MODULE":"GoogleNavigation","INFOPLIST_FILE":"Target Support Files/GoogleNavigation/ResourceBundle-GoogleNavigationResources-GoogleNavigation-Info.plist","IPHONEOS_DEPLOYMENT_TARGET":"16.0","PRODUCT_NAME":"GoogleNavigationResources","SDKROOT":"iphoneos","SKIP_INSTALL":"YES","TARGETED_DEVICE_FAMILY":"1,2","VALIDATE_PRODUCT":"YES","WRAPPER_EXTENSION":"bundle"},"guid":"bfdfe7dc352907fc980b868725387e98977553104d36f51b343d85428e46059a","name":"Profile"},{"baseConfigurationFileReference":"bfdfe7dc352907fc980b868725387e9896631f0702bcaa9fd24776311b2cd48b","buildSettings":{"CODE_SIGNING_ALLOWED":"NO","CODE_SIGNING_IDENTITY":"-","CODE_SIGNING_REQUIRED":"NO","CONFIGURATION_BUILD_DIR":"$(BUILD_DIR)/$(CONFIGURATION)$(EFFECTIVE_PLATFORM_NAME)/GoogleNavigation","EXPANDED_CODE_SIGN_IDENTITY":"-","IBSC_MODULE":"GoogleNavigation","INFOPLIST_FILE":"Target Support Files/GoogleNavigation/ResourceBundle-GoogleNavigationResources-GoogleNavigation-Info.plist","IPHONEOS_DEPLOYMENT_TARGET":"16.0","PRODUCT_NAME":"GoogleNavigationResources","SDKROOT":"iphoneos","SKIP_INSTALL":"YES","TARGETED_DEVICE_FAMILY":"1,2","WRAPPER_EXTENSION":"bundle"},"guid":"bfdfe7dc352907fc980b868725387e983ae1d6699688152101d7a302bec258c7","name":"Release"}],"buildPhases":[{"buildFiles":[],"guid":"bfdfe7dc352907fc980b868725387e980fd5e3e3d2dfe86aee122db8e2bcc80f","type":"com.apple.buildphase.sources"},{"buildFiles":[],"guid":"bfdfe7dc352907fc980b868725387e98e210593fc985afc0fb568c7ea1c7b063","type":"com.apple.buildphase.frameworks"},{"buildFiles":[{"fileReference":"bfdfe7dc352907fc980b868725387e98313329d51e705fb6ce392e38d83a004c","guid":"bfdfe7dc352907fc980b868725387e98a81f8c119c50ffaca3bee7481e9584bb"}],"guid":"bfdfe7dc352907fc980b868725387e98a8dca316045dac76992a961badbc0665","type":"com.apple.buildphase.resources"}],"buildRules":[],"dependencies":[],"guid":"bfdfe7dc352907fc980b868725387e9840209c7fe78cadee2612d71e96cd4bdb","name":"GoogleNavigation-GoogleNavigationResources","productReference":{"guid":"bfdfe7dc352907fc980b868725387e9896ff2333d2f65de7e44bf6896a8741c5","name":"GoogleNavigationResources.bundle","type":"product"},"productTypeIdentifier":"com.apple.product-type.bundle","provisioningSourceData":[{"bundleIdentifierFromInfoPlist":"${PRODUCT_BUNDLE_IDENTIFIER}","configurationName":"Debug","provisioningStyle":0},{"bundleIdentifierFromInfoPlist":"${PRODUCT_BUNDLE_IDENTIFIER}","configurationName":"Profile","provisioningStyle":0},{"bundleIdentifierFromInfoPlist":"${PRODUCT_BUNDLE_IDENTIFIER}","configurationName":"Release","provisioningStyle":0}],"type":"standard"}

View File

@ -1 +0,0 @@
{"buildConfigurations":[{"baseConfigurationFileReference":"bfdfe7dc352907fc980b868725387e98a69fd7bcabc0a20fae04fd73fa52549d","buildSettings":{"CODE_SIGNING_ALLOWED":"NO","CODE_SIGNING_IDENTITY":"-","CODE_SIGNING_REQUIRED":"NO","CONFIGURATION_BUILD_DIR":"$(BUILD_DIR)/$(CONFIGURATION)$(EFFECTIVE_PLATFORM_NAME)/path_provider_foundation","EXPANDED_CODE_SIGN_IDENTITY":"-","IBSC_MODULE":"path_provider_foundation","INFOPLIST_FILE":"Target Support Files/path_provider_foundation/ResourceBundle-path_provider_foundation_privacy-path_provider_foundation-Info.plist","IPHONEOS_DEPLOYMENT_TARGET":"13.0","ONLY_ACTIVE_ARCH":"NO","PRODUCT_NAME":"path_provider_foundation_privacy","SDKROOT":"iphoneos","SKIP_INSTALL":"YES","TARGETED_DEVICE_FAMILY":"1,2","WRAPPER_EXTENSION":"bundle"},"guid":"bfdfe7dc352907fc980b868725387e98260207aa3e1df35e23e69c1a47426154","name":"Debug"},{"baseConfigurationFileReference":"bfdfe7dc352907fc980b868725387e9830121d4cd39b9b5353e3bd656ff5f0e5","buildSettings":{"CLANG_ENABLE_OBJC_WEAK":"NO","CODE_SIGNING_ALLOWED":"NO","CODE_SIGNING_IDENTITY":"-","CODE_SIGNING_REQUIRED":"NO","CONFIGURATION_BUILD_DIR":"$(BUILD_DIR)/$(CONFIGURATION)$(EFFECTIVE_PLATFORM_NAME)/path_provider_foundation","EXPANDED_CODE_SIGN_IDENTITY":"-","IBSC_MODULE":"path_provider_foundation","INFOPLIST_FILE":"Target Support Files/path_provider_foundation/ResourceBundle-path_provider_foundation_privacy-path_provider_foundation-Info.plist","IPHONEOS_DEPLOYMENT_TARGET":"13.0","PRODUCT_NAME":"path_provider_foundation_privacy","SDKROOT":"iphoneos","SKIP_INSTALL":"YES","TARGETED_DEVICE_FAMILY":"1,2","VALIDATE_PRODUCT":"YES","WRAPPER_EXTENSION":"bundle"},"guid":"bfdfe7dc352907fc980b868725387e98ece2bbc522c1379b0b2a178c66361143","name":"Profile"},{"baseConfigurationFileReference":"bfdfe7dc352907fc980b868725387e9830121d4cd39b9b5353e3bd656ff5f0e5","buildSettings":{"CODE_SIGNING_ALLOWED":"NO","CODE_SIGNING_IDENTITY":"-","CODE_SIGNING_REQUIRED":"NO","CONFIGURATION_BUILD_DIR":"$(BUILD_DIR)/$(CONFIGURATION)$(EFFECTIVE_PLATFORM_NAME)/path_provider_foundation","EXPANDED_CODE_SIGN_IDENTITY":"-","IBSC_MODULE":"path_provider_foundation","INFOPLIST_FILE":"Target Support Files/path_provider_foundation/ResourceBundle-path_provider_foundation_privacy-path_provider_foundation-Info.plist","IPHONEOS_DEPLOYMENT_TARGET":"13.0","PRODUCT_NAME":"path_provider_foundation_privacy","SDKROOT":"iphoneos","SKIP_INSTALL":"YES","TARGETED_DEVICE_FAMILY":"1,2","WRAPPER_EXTENSION":"bundle"},"guid":"bfdfe7dc352907fc980b868725387e98a5f6daf72705fb5072948ac27596dc7d","name":"Release"}],"buildPhases":[{"buildFiles":[],"guid":"bfdfe7dc352907fc980b868725387e985dd996df7a2e473a6991f4f302e8eabf","type":"com.apple.buildphase.sources"},{"buildFiles":[],"guid":"bfdfe7dc352907fc980b868725387e98e10df81fc7f47e627440f5852488689e","type":"com.apple.buildphase.frameworks"},{"buildFiles":[{"fileReference":"bfdfe7dc352907fc980b868725387e98dffa8ac546e296000112a65a901e4106","guid":"bfdfe7dc352907fc980b868725387e988add1c9900ab74ace4b8472d695304bb"}],"guid":"bfdfe7dc352907fc980b868725387e98e2b3a30f9688768983820656f464ef51","type":"com.apple.buildphase.resources"}],"buildRules":[],"dependencies":[],"guid":"bfdfe7dc352907fc980b868725387e987ea64ee8d53085bf9edd1a57aaf8cbb5","name":"path_provider_foundation-path_provider_foundation_privacy","productReference":{"guid":"bfdfe7dc352907fc980b868725387e986e649604f74c414a7c2dbe5ef4cc4e75","name":"path_provider_foundation_privacy.bundle","type":"product"},"productTypeIdentifier":"com.apple.product-type.bundle","provisioningSourceData":[{"bundleIdentifierFromInfoPlist":"${PRODUCT_BUNDLE_IDENTIFIER}","configurationName":"Debug","provisioningStyle":0},{"bundleIdentifierFromInfoPlist":"${PRODUCT_BUNDLE_IDENTIFIER}","configurationName":"Profile","provisioningStyle":0},{"bundleIdentifierFromInfoPlist":"${PRODUCT_BUNDLE_IDENTIFIER}","configurationName":"Release","provisioningStyle":0}],"type":"standard"}

View File

@ -1 +0,0 @@
{"buildConfigurations":[{"baseConfigurationFileReference":"bfdfe7dc352907fc980b868725387e9840ae838f8ac69bc67263efbe8dc323d7","buildSettings":{"ASSETCATALOG_COMPILER_APPICON_NAME":"AppIcon","ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME":"AccentColor","CLANG_ENABLE_OBJC_WEAK":"NO","ENABLE_USER_SCRIPT_SANDBOXING":"NO","IPHONEOS_DEPLOYMENT_TARGET":"16.0","LD_RUNPATH_SEARCH_PATHS":"$(inherited) @executable_path/Frameworks","ONLY_ACTIVE_ARCH":"NO","SDKROOT":"iphoneos","TARGETED_DEVICE_FAMILY":"1,2"},"guid":"bfdfe7dc352907fc980b868725387e98644b3fe27382cec8a7bd8d5de6d3bf23","name":"Debug"},{"baseConfigurationFileReference":"bfdfe7dc352907fc980b868725387e989bf599cd5d4dd79de867d43540047de6","buildSettings":{"ASSETCATALOG_COMPILER_APPICON_NAME":"AppIcon","ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME":"AccentColor","CLANG_ENABLE_OBJC_WEAK":"NO","ENABLE_USER_SCRIPT_SANDBOXING":"NO","IPHONEOS_DEPLOYMENT_TARGET":"16.0","LD_RUNPATH_SEARCH_PATHS":"$(inherited) @executable_path/Frameworks","SDKROOT":"iphoneos","TARGETED_DEVICE_FAMILY":"1,2","VALIDATE_PRODUCT":"YES"},"guid":"bfdfe7dc352907fc980b868725387e9873ec9b10f7565a6466b1212456cdaadb","name":"Profile"},{"baseConfigurationFileReference":"bfdfe7dc352907fc980b868725387e989bf599cd5d4dd79de867d43540047de6","buildSettings":{"ASSETCATALOG_COMPILER_APPICON_NAME":"AppIcon","ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME":"AccentColor","CLANG_ENABLE_OBJC_WEAK":"NO","ENABLE_USER_SCRIPT_SANDBOXING":"NO","IPHONEOS_DEPLOYMENT_TARGET":"16.0","LD_RUNPATH_SEARCH_PATHS":"$(inherited) @executable_path/Frameworks","SDKROOT":"iphoneos","TARGETED_DEVICE_FAMILY":"1,2","VALIDATE_PRODUCT":"YES"},"guid":"bfdfe7dc352907fc980b868725387e986321e0a9b9c9f4570e60c08f0377621a","name":"Release"}],"buildPhases":[{"alwaysOutOfDate":"false","alwaysRunForInstallHdrs":"false","buildFiles":[],"emitEnvironment":"false","guid":"bfdfe7dc352907fc980b868725387e98b21ffc68aa281b044f48f05e9d22d849","inputFileListPaths":["${PODS_ROOT}/Target Support Files/GoogleMaps/GoogleMaps-xcframeworks-input-files.xcfilelist"],"inputFilePaths":[],"name":"[CP] Copy XCFrameworks","originalObjectID":"B4014D7E512183EABBE5F8E70545CAF8","outputFileListPaths":["${PODS_ROOT}/Target Support Files/GoogleMaps/GoogleMaps-xcframeworks-output-files.xcfilelist"],"outputFilePaths":[],"sandboxingOverride":"basedOnBuildSetting","scriptContents":"\"${PODS_ROOT}/Target Support Files/GoogleMaps/GoogleMaps-xcframeworks.sh\"\n","shellPath":"/bin/sh","type":"com.apple.buildphase.shell-script"}],"buildRules":[],"dependencies":[{"guid":"bfdfe7dc352907fc980b868725387e9877354dc0c1379e634078de2da2deba6b","name":"GoogleMaps-GoogleMapsResources"}],"guid":"bfdfe7dc352907fc980b868725387e9818352c54edac2258b91768852065ce5e","name":"GoogleMaps","provisioningSourceData":[{"bundleIdentifierFromInfoPlist":"","configurationName":"Debug","provisioningStyle":0},{"bundleIdentifierFromInfoPlist":"","configurationName":"Profile","provisioningStyle":0},{"bundleIdentifierFromInfoPlist":"","configurationName":"Release","provisioningStyle":0}],"type":"aggregate"}

View File

@ -1 +0,0 @@
{"buildConfigurations":[{"baseConfigurationFileReference":"bfdfe7dc352907fc980b868725387e98067c4bb9648cff0393e17526af5710e3","buildSettings":{"CODE_SIGNING_ALLOWED":"NO","CODE_SIGNING_IDENTITY":"-","CODE_SIGNING_REQUIRED":"NO","CONFIGURATION_BUILD_DIR":"$(BUILD_DIR)/$(CONFIGURATION)$(EFFECTIVE_PLATFORM_NAME)/image_picker_ios","EXPANDED_CODE_SIGN_IDENTITY":"-","IBSC_MODULE":"image_picker_ios","INFOPLIST_FILE":"Target Support Files/image_picker_ios/ResourceBundle-image_picker_ios_privacy-image_picker_ios-Info.plist","IPHONEOS_DEPLOYMENT_TARGET":"13.0","ONLY_ACTIVE_ARCH":"NO","PRODUCT_NAME":"image_picker_ios_privacy","SDKROOT":"iphoneos","SKIP_INSTALL":"YES","TARGETED_DEVICE_FAMILY":"1,2","WRAPPER_EXTENSION":"bundle"},"guid":"bfdfe7dc352907fc980b868725387e9811ac729da10988d7de6210aed950376d","name":"Debug"},{"baseConfigurationFileReference":"bfdfe7dc352907fc980b868725387e982e9c5f9312975795324f63668491a40d","buildSettings":{"CLANG_ENABLE_OBJC_WEAK":"NO","CODE_SIGNING_ALLOWED":"NO","CODE_SIGNING_IDENTITY":"-","CODE_SIGNING_REQUIRED":"NO","CONFIGURATION_BUILD_DIR":"$(BUILD_DIR)/$(CONFIGURATION)$(EFFECTIVE_PLATFORM_NAME)/image_picker_ios","EXPANDED_CODE_SIGN_IDENTITY":"-","IBSC_MODULE":"image_picker_ios","INFOPLIST_FILE":"Target Support Files/image_picker_ios/ResourceBundle-image_picker_ios_privacy-image_picker_ios-Info.plist","IPHONEOS_DEPLOYMENT_TARGET":"13.0","PRODUCT_NAME":"image_picker_ios_privacy","SDKROOT":"iphoneos","SKIP_INSTALL":"YES","TARGETED_DEVICE_FAMILY":"1,2","VALIDATE_PRODUCT":"YES","WRAPPER_EXTENSION":"bundle"},"guid":"bfdfe7dc352907fc980b868725387e980a91a2c2323ca1447f8abf46d4db3f39","name":"Profile"},{"baseConfigurationFileReference":"bfdfe7dc352907fc980b868725387e982e9c5f9312975795324f63668491a40d","buildSettings":{"CODE_SIGNING_ALLOWED":"NO","CODE_SIGNING_IDENTITY":"-","CODE_SIGNING_REQUIRED":"NO","CONFIGURATION_BUILD_DIR":"$(BUILD_DIR)/$(CONFIGURATION)$(EFFECTIVE_PLATFORM_NAME)/image_picker_ios","EXPANDED_CODE_SIGN_IDENTITY":"-","IBSC_MODULE":"image_picker_ios","INFOPLIST_FILE":"Target Support Files/image_picker_ios/ResourceBundle-image_picker_ios_privacy-image_picker_ios-Info.plist","IPHONEOS_DEPLOYMENT_TARGET":"13.0","PRODUCT_NAME":"image_picker_ios_privacy","SDKROOT":"iphoneos","SKIP_INSTALL":"YES","TARGETED_DEVICE_FAMILY":"1,2","WRAPPER_EXTENSION":"bundle"},"guid":"bfdfe7dc352907fc980b868725387e98b2a9180fa7a71161ad43491be2d22329","name":"Release"}],"buildPhases":[{"buildFiles":[],"guid":"bfdfe7dc352907fc980b868725387e9881b7cc6c2b8c14363b0c2c16ccef997e","type":"com.apple.buildphase.sources"},{"buildFiles":[],"guid":"bfdfe7dc352907fc980b868725387e9836804edc69d47dd23fabb059f1fab8d5","type":"com.apple.buildphase.frameworks"},{"buildFiles":[{"fileReference":"bfdfe7dc352907fc980b868725387e9869631eb1c001ffcff7f8e0c26a0c3aca","guid":"bfdfe7dc352907fc980b868725387e98bfd55e31574d784429a73f7eb3d39a95"}],"guid":"bfdfe7dc352907fc980b868725387e9865a7dd558debfaa9b2e221c59f214e73","type":"com.apple.buildphase.resources"}],"buildRules":[],"dependencies":[],"guid":"bfdfe7dc352907fc980b868725387e98082dc85da1fc941e5234c7cc1f11b27d","name":"image_picker_ios-image_picker_ios_privacy","productReference":{"guid":"bfdfe7dc352907fc980b868725387e98cba567c8a049008de84f093e54e3191c","name":"image_picker_ios_privacy.bundle","type":"product"},"productTypeIdentifier":"com.apple.product-type.bundle","provisioningSourceData":[{"bundleIdentifierFromInfoPlist":"${PRODUCT_BUNDLE_IDENTIFIER}","configurationName":"Debug","provisioningStyle":0},{"bundleIdentifierFromInfoPlist":"${PRODUCT_BUNDLE_IDENTIFIER}","configurationName":"Profile","provisioningStyle":0},{"bundleIdentifierFromInfoPlist":"${PRODUCT_BUNDLE_IDENTIFIER}","configurationName":"Release","provisioningStyle":0}],"type":"standard"}

View File

@ -1 +0,0 @@
{"guid":"dc4b70c03e8043e50e38f2068887b1d4","name":"Pods","path":"/Users/mathias/Documents/workspaces/plan-b/ionic-planb-logistic-app-flutter/ios/Pods/Pods.xcodeproj/project.xcworkspace","projects":["PROJECT@v11_mod=9e2fb057732b89c3647d8f55a7747969_hash=bfdfe7dc352907fc980b868725387e98plugins=1OJSG6M1FOV3XYQCBH7Z29RZ0FPR9XDE1"]}

View File

@ -1,5 +0,0 @@
arb-dir: lib/l10n
template-arb-file: app_en.arb
output-localization-file: app_localizations.dart
output-class: AppLocalizations
nullable-getter: false

View File

@ -1,423 +0,0 @@
import 'dart:async';
import 'dart:convert';
import 'package:http/http.dart' as http;
import 'package:http_interceptor/http_interceptor.dart';
import 'types.dart';
import 'openapi_config.dart';
import '../utils/logging_interceptor.dart';
import '../utils/http_client_factory.dart';
import '../services/auth_service.dart';
class CqrsApiClient {
final ApiClientConfig config;
final AuthService? authService;
late final http.Client _httpClient;
CqrsApiClient({
required this.config,
this.authService,
http.Client? httpClient,
}) {
_httpClient = httpClient ?? InterceptedClient.build(
interceptors: [LoggingInterceptor()],
client: HttpClientFactory.createClient(
allowSelfSigned: config.allowSelfSignedCertificate,
),
);
}
String get baseUrl => config.baseUrl;
Future<Result<T>> executeQuery<T>({
required String endpoint,
required Serializable query,
required T Function(Map<String, dynamic>) fromJson,
bool isRetry = false,
}) async {
try {
final url = Uri.parse('$baseUrl/api/query/$endpoint');
final headers = await _buildHeaders();
final response = await _httpClient
.post(
url,
headers: headers,
body: jsonEncode(query.toJson()),
)
.timeout(config.timeout);
if (response.statusCode == 401 && !isRetry && authService != null) {
final refreshResult = await authService!.refreshAccessToken();
return refreshResult.when(
success: (token) => executeQuery(
endpoint: endpoint,
query: query,
fromJson: fromJson,
isRetry: true,
),
onError: (error) => _handleResponse<T>(response, fromJson),
cancelled: () => _handleResponse<T>(response, fromJson),
);
}
return _handleResponse<T>(response, fromJson);
} on TimeoutException {
return Result.error(ApiError.timeout());
} catch (e, stackTrace) {
return Result.error(
ApiError.unknown(
'Failed to execute query: ${e.toString()}',
exception: Exception(stackTrace.toString()),
),
);
}
}
Future<Result<PaginatedResult<T>>> executePaginatedQuery<T>({
required String endpoint,
required Serializable query,
required T Function(Map<String, dynamic>) itemFromJson,
required int page,
required int pageSize,
List<FilterCriteria>? filters,
bool isRetry = false,
}) async {
try {
final url = Uri.parse(
'$baseUrl/api/query/$endpoint?page=$page&pageSize=$pageSize',
);
final headers = await _buildHeaders();
final queryData = {
...query.toJson(),
if (filters != null && filters.isNotEmpty)
'filters': filters.map((f) => f.toJson()).toList(),
};
final response = await _httpClient
.post(
url,
headers: headers,
body: jsonEncode(queryData),
)
.timeout(config.timeout);
if (response.statusCode == 401 && !isRetry && authService != null) {
final refreshResult = await authService!.refreshAccessToken();
return refreshResult.when(
success: (token) => executePaginatedQuery(
endpoint: endpoint,
query: query,
itemFromJson: itemFromJson,
page: page,
pageSize: pageSize,
filters: filters,
isRetry: true,
),
onError: (error) => _handlePaginatedResponse<T>(response, itemFromJson, page, pageSize),
cancelled: () => _handlePaginatedResponse<T>(response, itemFromJson, page, pageSize),
);
}
return _handlePaginatedResponse<T>(response, itemFromJson, page, pageSize);
} on TimeoutException {
return Result.error(ApiError.timeout());
} catch (e, stackTrace) {
return Result.error(
ApiError.unknown(
'Failed to execute paginated query: ${e.toString()}',
exception: Exception(stackTrace.toString()),
),
);
}
}
Future<Result<void>> executeCommand({
required String endpoint,
required Serializable command,
bool isRetry = false,
}) async {
try {
final url = Uri.parse('$baseUrl/api/command/$endpoint');
final headers = await _buildHeaders();
final response = await _httpClient
.post(
url,
headers: headers,
body: jsonEncode(command.toJson()),
)
.timeout(config.timeout);
if (response.statusCode == 401 && !isRetry && authService != null) {
final refreshResult = await authService!.refreshAccessToken();
return refreshResult.when(
success: (token) => executeCommand(
endpoint: endpoint,
command: command,
isRetry: true,
),
onError: (error) {
if (response.statusCode >= 200 && response.statusCode < 300) {
return Result.success(null);
} else {
return _handleErrorResponse(response);
}
},
cancelled: () {
if (response.statusCode >= 200 && response.statusCode < 300) {
return Result.success(null);
} else {
return _handleErrorResponse(response);
}
},
);
}
if (response.statusCode >= 200 && response.statusCode < 300) {
return Result.success(null);
} else {
return _handleErrorResponse(response);
}
} on TimeoutException {
return Result.error(ApiError.timeout());
} catch (e, stackTrace) {
return Result.error(
ApiError.unknown(
'Failed to execute command: ${e.toString()}',
exception: Exception(stackTrace.toString()),
),
);
}
}
Future<Result<T>> executeCommandWithResult<T>({
required String endpoint,
required Serializable command,
required T Function(Map<String, dynamic>) fromJson,
bool isRetry = false,
}) async {
try {
final url = Uri.parse('$baseUrl/api/command/$endpoint');
final headers = await _buildHeaders();
final response = await _httpClient
.post(
url,
headers: headers,
body: jsonEncode(command.toJson()),
)
.timeout(config.timeout);
if (response.statusCode == 401 && !isRetry && authService != null) {
final refreshResult = await authService!.refreshAccessToken();
return refreshResult.when(
success: (token) => executeCommandWithResult(
endpoint: endpoint,
command: command,
fromJson: fromJson,
isRetry: true,
),
onError: (error) => _handleResponse<T>(response, fromJson),
cancelled: () => _handleResponse<T>(response, fromJson),
);
}
return _handleResponse<T>(response, fromJson);
} on TimeoutException {
return Result.error(ApiError.timeout());
} catch (e, stackTrace) {
return Result.error(
ApiError.unknown(
'Failed to execute command with result: ${e.toString()}',
exception: Exception(stackTrace.toString()),
),
);
}
}
Future<Result<String>> uploadFile({
required String endpoint,
required String filePath,
required String fieldName,
Map<String, String>? additionalFields,
bool isRetry = false,
}) async {
try {
final url = Uri.parse('$baseUrl/api/command/$endpoint');
final headers = await _buildHeaders();
final request = http.MultipartRequest('POST', url)
..headers.addAll(headers)
..files.add(await http.MultipartFile.fromPath(fieldName, filePath));
if (additionalFields != null) {
request.fields.addAll(additionalFields);
}
final response = await request.send().timeout(config.timeout);
final responseBody = await response.stream.bytesToString();
if (response.statusCode == 401 && !isRetry && authService != null) {
final refreshResult = await authService!.refreshAccessToken();
return refreshResult.when(
success: (token) => uploadFile(
endpoint: endpoint,
filePath: filePath,
fieldName: fieldName,
additionalFields: additionalFields,
isRetry: true,
),
onError: (error) {
if (response.statusCode >= 200 && response.statusCode < 300) {
return Result.success(responseBody);
} else {
return _parseErrorFromString(responseBody, response.statusCode);
}
},
cancelled: () {
if (response.statusCode >= 200 && response.statusCode < 300) {
return Result.success(responseBody);
} else {
return _parseErrorFromString(responseBody, response.statusCode);
}
},
);
}
if (response.statusCode >= 200 && response.statusCode < 300) {
return Result.success(responseBody);
} else {
return _parseErrorFromString(responseBody, response.statusCode);
}
} on TimeoutException {
return Result.error(ApiError.timeout());
} catch (e, stackTrace) {
return Result.error(
ApiError.unknown(
'Failed to upload file: ${e.toString()}',
exception: Exception(stackTrace.toString()),
),
);
}
}
Future<Map<String, String>> _buildHeaders() async {
final headers = <String, String>{
'Content-Type': 'application/json',
'Accept': 'application/json',
...config.defaultHeaders,
};
if (authService != null) {
// Proactively ensure token is valid and refresh if needed
final token = await authService!.ensureValidToken();
if (token != null) {
headers['Authorization'] = 'Bearer $token';
}
}
return headers;
}
Result<T> _handleResponse<T>(
http.Response response,
T Function(Map<String, dynamic>) fromJson,
) {
try {
if (response.statusCode >= 200 && response.statusCode < 300) {
if (response.body.isEmpty) {
return Result.success(null as T);
}
final data = jsonDecode(response.body) as Map<String, dynamic>;
return Result.success(fromJson(data));
} else {
return _handleErrorResponse(response);
}
} catch (e) {
return Result.error(
ApiError.unknown('Failed to parse response: ${e.toString()}'),
);
}
}
Result<PaginatedResult<T>> _handlePaginatedResponse<T>(
http.Response response,
T Function(Map<String, dynamic>) itemFromJson,
int page,
int pageSize,
) {
try {
if (response.statusCode >= 200 && response.statusCode < 300) {
final data = jsonDecode(response.body) as Map<String, dynamic>;
final items = (data['items'] as List?)
?.map((item) => itemFromJson(item as Map<String, dynamic>))
.toList() ?? [];
final totalCount = data['totalCount'] as int? ?? items.length;
return Result.success(
PaginatedResult(
items: items,
page: page,
pageSize: pageSize,
totalCount: totalCount,
),
);
} else {
return _handleErrorResponse(response);
}
} catch (e) {
return Result.error(
ApiError.unknown('Failed to parse paginated response: ${e.toString()}'),
);
}
}
Result<Never> _handleErrorResponse(http.Response response) {
try {
final data = jsonDecode(response.body) as Map<String, dynamic>;
final message = data['message'] as String? ?? 'An error occurred';
if (response.statusCode == 422) {
final errors = data['errors'] as Map<String, dynamic>?;
final details = errors?.map(
(key, value) => MapEntry(
key,
(value as List?)?.map((e) => e.toString()).toList() ?? [],
),
);
return Result.error(
ApiError.validation(message, details),
);
}
return Result.error(
ApiError.http(statusCode: response.statusCode, message: message),
);
} catch (e) {
return Result.error(
ApiError.http(
statusCode: response.statusCode,
message: response.reasonPhrase ?? 'Unknown error',
),
);
}
}
Result<Never> _parseErrorFromString(String body, int statusCode) {
try {
final data = jsonDecode(body) as Map<String, dynamic>;
final message = data['message'] as String? ?? 'An error occurred';
return Result.error(
ApiError.http(statusCode: statusCode, message: message),
);
} catch (e) {
return Result.error(
ApiError.http(statusCode: statusCode, message: 'Unknown error'),
);
}
}
void close() {
_httpClient.close();
}
}

View File

@ -1,24 +0,0 @@
class ApiClientConfig {
final String baseUrl;
final Duration timeout;
final Map<String, String> defaultHeaders;
final bool allowSelfSignedCertificate;
const ApiClientConfig({
required this.baseUrl,
this.timeout = const Duration(seconds: 30),
this.defaultHeaders = const {},
this.allowSelfSignedCertificate = false,
});
static const ApiClientConfig development = ApiClientConfig(
baseUrl: 'https://localhost:7182',
timeout: Duration(seconds: 30),
allowSelfSignedCertificate: true,
);
static const ApiClientConfig production = ApiClientConfig(
baseUrl: 'https://api-route.goutezplanb.com',
timeout: Duration(seconds: 30),
);
}

View File

@ -1,160 +0,0 @@
abstract interface class Serializable {
Map<String, Object?> toJson();
}
enum ApiErrorType {
network,
timeout,
validation,
http,
unknown,
}
class ApiError {
final ApiErrorType type;
final String message;
final int? statusCode;
final Map<String, List<String>>? details;
final Exception? originalException;
const ApiError({
required this.type,
required this.message,
this.statusCode,
this.details,
this.originalException,
});
factory ApiError.network(String message) => ApiError(
type: ApiErrorType.network,
message: message,
);
factory ApiError.timeout() => const ApiError(
type: ApiErrorType.timeout,
message: 'Request timeout',
);
factory ApiError.validation(String message, Map<String, List<String>>? details) => ApiError(
type: ApiErrorType.validation,
message: message,
details: details,
);
factory ApiError.http({
required int statusCode,
required String message,
}) => ApiError(
type: ApiErrorType.http,
message: message,
statusCode: statusCode,
);
factory ApiError.unknown(String message, {Exception? exception}) => ApiError(
type: ApiErrorType.unknown,
message: message,
originalException: exception,
);
}
sealed class Result<T> {
const Result();
factory Result.success(T data) => Success<T>(data);
factory Result.error(ApiError error) => Error<T>(error);
R when<R>({
required R Function(T data) success,
required R Function(ApiError error) onError,
}) {
return switch (this) {
Success<T>(:final data) => success(data),
Error<T>(:final error) => onError(error),
};
}
R? whenSuccess<R>(R Function(T data) fn) {
return switch (this) {
Success<T>(:final data) => fn(data),
Error<T>() => null,
};
}
R? whenError<R>(R Function(ApiError error) fn) {
return switch (this) {
Success<T>() => null,
Error<T>(:final error) => fn(error),
};
}
bool get isSuccess => this is Success<T>;
bool get isError => this is Error<T>;
T? getOrNull() => whenSuccess((data) => data);
ApiError? getErrorOrNull() => whenError((error) => error);
}
final class Success<T> extends Result<T> {
final T data;
const Success(this.data);
}
final class Error<T> extends Result<T> {
final ApiError error;
const Error(this.error);
}
class PaginatedResult<T> {
final List<T> items;
final int page;
final int pageSize;
final int totalCount;
const PaginatedResult({
required this.items,
required this.page,
required this.pageSize,
required this.totalCount,
});
int get totalPages => (totalCount / pageSize).ceil();
bool get hasNextPage => page < totalPages;
}
enum FilterOperator {
equals('eq'),
notEquals('neq'),
greaterThan('gt'),
greaterThanOrEqual('gte'),
lessThan('lt'),
lessThanOrEqual('lte'),
contains('contains'),
startsWith('startsWith'),
endsWith('endsWith'),
in_('in');
final String operator;
const FilterOperator(this.operator);
}
class FilterCriteria implements Serializable {
final String field;
final FilterOperator operator;
final Object? value;
FilterCriteria({
required this.field,
required this.operator,
required this.value,
});
@override
Map<String, Object?> toJson() => {
'field': field,
'operator': operator.operator,
'value': value,
};
}

View File

@ -1,199 +0,0 @@
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import '../l10n/app_localizations.dart';
import '../models/delivery_route.dart';
import '../theme/spacing_system.dart';
import '../theme/size_system.dart';
import '../theme/animation_system.dart';
import '../theme/color_system.dart';
import '../utils/breakpoints.dart';
import '../providers/providers.dart';
import 'route_list_item.dart';
class CollapsibleRoutesSidebar extends ConsumerStatefulWidget {
final List<DeliveryRoute> routes;
final DeliveryRoute? selectedRoute;
final ValueChanged<DeliveryRoute> onRouteSelected;
const CollapsibleRoutesSidebar({
super.key,
required this.routes,
this.selectedRoute,
required this.onRouteSelected,
});
@override
ConsumerState<CollapsibleRoutesSidebar> createState() =>
_CollapsibleRoutesSidebarState();
}
class _CollapsibleRoutesSidebarState extends ConsumerState<CollapsibleRoutesSidebar>
with SingleTickerProviderStateMixin {
late AnimationController _animationController;
@override
void initState() {
super.initState();
_animationController = AnimationController(
duration: const Duration(milliseconds: 300),
vsync: this,
);
// Set initial animation state based on provider value
WidgetsBinding.instance.addPostFrameCallback((_) {
final isExpanded = ref.read(collapseStateProvider);
if (isExpanded) {
_animationController.forward();
} else {
_animationController.value = 0;
}
});
}
@override
void dispose() {
_animationController.dispose();
super.dispose();
}
void _toggleSidebar() {
// Use shared provider state
ref.read(collapseStateProvider.notifier).toggle();
final isExpanded = ref.read(collapseStateProvider);
if (isExpanded) {
_animationController.forward();
} else {
_animationController.reverse();
}
}
@override
Widget build(BuildContext context) {
final isMobile = context.isMobile;
final isDarkMode = Theme.of(context).brightness == Brightness.dark;
final isExpanded = ref.watch(collapseStateProvider);
final l10n = AppLocalizations.of(context)!;
// On mobile, always show as collapsible
if (isMobile) {
return Container(
color: Theme.of(context).colorScheme.surface,
child: Column(
children: [
// Header with toggle button
Container(
padding: EdgeInsets.all(AppSpacing.md),
decoration: BoxDecoration(
border: Border(
bottom: BorderSide(
color: isDarkMode ? SvrntyColors.darkSlate : SvrntyColors.slateGray.withValues(alpha: 0.2),
width: 1,
),
),
),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
if (isExpanded)
Text(
l10n.routes,
style: Theme.of(context).textTheme.titleLarge?.copyWith(
fontWeight: FontWeight.w700,
),
),
IconButton(
icon: Icon(isExpanded ? Icons.menu_open : Icons.menu),
onPressed: _toggleSidebar,
iconSize: AppSizes.iconMd,
),
],
),
),
// Collapsible content
if (isExpanded)
Expanded(
child: _buildRoutesList(context, isExpanded),
),
],
),
);
}
// On tablet/desktop, show full sidebar with toggle (expanded: 300px, collapsed: 80px for badge)
return AnimatedContainer(
duration: const Duration(milliseconds: 300),
curve: Curves.easeInOut,
width: isExpanded ? 300 : 80,
color: Theme.of(context).colorScheme.surface,
child: Column(
children: [
// Header with toggle button
Container(
height: kToolbarHeight,
padding: EdgeInsets.symmetric(horizontal: AppSpacing.xs),
decoration: BoxDecoration(
border: Border(
left: BorderSide(
color: isDarkMode ? SvrntyColors.darkSlate : SvrntyColors.slateGray.withValues(alpha: 0.2),
width: 1,
),
),
),
child: Row(
mainAxisAlignment: isExpanded ? MainAxisAlignment.spaceBetween : MainAxisAlignment.center,
children: [
if (isExpanded)
Expanded(
child: Padding(
padding: EdgeInsets.only(left: AppSpacing.md),
child: Text(
l10n.routes,
style: Theme.of(context).textTheme.titleLarge?.copyWith(
fontWeight: FontWeight.w700,
),
overflow: TextOverflow.ellipsis,
),
),
),
SizedBox(
width: AppSizes.buttonHeightMd,
height: AppSizes.buttonHeightMd,
child: IconButton(
icon: Icon(isExpanded ? Icons.menu_open : Icons.menu),
onPressed: _toggleSidebar,
iconSize: AppSizes.iconMd,
),
),
],
),
),
// Routes list
Flexible(
child: _buildRoutesList(context, isExpanded),
),
],
),
);
}
Widget _buildRoutesList(BuildContext context, bool isExpanded) {
return ListView.builder(
padding: const EdgeInsets.only(top: 4, bottom: 8),
physics: const AlwaysScrollableScrollPhysics(),
itemCount: widget.routes.length,
itemBuilder: (context, index) {
final route = widget.routes[index];
final isSelected = widget.selectedRoute?.id == route.id;
return RouteListItem(
route: route,
isSelected: isSelected,
onTap: () => widget.onRouteSelected(route),
animationIndex: index,
isCollapsed: !isExpanded,
);
},
);
}
}

View File

@ -1,797 +0,0 @@
import 'dart:io' show Platform;
import 'package:flutter/foundation.dart' show kDebugMode;
import 'package:flutter/material.dart';
import 'package:google_navigation_flutter/google_navigation_flutter.dart';
import '../models/delivery.dart';
import '../theme/color_system.dart';
import '../utils/toast_helper.dart';
/// Enhanced dark-mode aware map component with custom styling
class DarkModeMapComponent extends StatefulWidget {
final List<Delivery> deliveries;
final Delivery? selectedDelivery;
final ValueChanged<Delivery?>? onDeliverySelected;
final Function(String)? onAction;
const DarkModeMapComponent({
super.key,
required this.deliveries,
this.selectedDelivery,
this.onDeliverySelected,
this.onAction,
});
@override
State<DarkModeMapComponent> createState() => _DarkModeMapComponentState();
}
class _DarkModeMapComponentState extends State<DarkModeMapComponent> {
GoogleNavigationViewController? _navigationController;
bool _isNavigating = false;
LatLng? _destinationLocation;
bool _isSessionInitialized = false;
bool _isInitializing = false;
bool _isStartingNavigation = false;
String _loadingMessage = 'Initializing...';
Brightness? _lastBrightness;
bool _isMapViewReady = false;
bool _isDisposed = false;
@override
void initState() {
super.initState();
_initializeNavigation();
}
@override
void dispose() {
_isDisposed = true;
_navigationController = null;
super.dispose();
}
@override
void didUpdateWidget(DarkModeMapComponent oldWidget) {
super.didUpdateWidget(oldWidget);
if (oldWidget.selectedDelivery != widget.selectedDelivery) {
_updateDestination();
// If navigation was active, restart navigation to new delivery
if (_isNavigating &&
widget.selectedDelivery != null &&
widget.selectedDelivery!.deliveryAddress != null) {
_restartNavigationToNewDelivery();
}
}
}
@override
void didChangeDependencies() {
super.didChangeDependencies();
// Detect theme changes and reapply map style
final currentBrightness = Theme.of(context).brightness;
if (_lastBrightness != null &&
_lastBrightness != currentBrightness &&
_navigationController != null &&
!_isDisposed) {
_applyDarkModeStyle();
}
_lastBrightness = currentBrightness;
}
Future<void> _restartNavigationToNewDelivery() async {
try {
// Stop current navigation
await _stopNavigation();
// Wait a bit for stop to complete
await Future.delayed(const Duration(milliseconds: 300));
// Start navigation to new delivery
if (mounted && !_isDisposed) {
await _startNavigation();
}
} catch (e) {
debugPrint('Restart navigation error: $e');
}
}
Future<void> _initializeNavigation() async {
if (_isInitializing || _isSessionInitialized) return;
setState(() {
_isInitializing = true;
});
try {
final termsAccepted = await GoogleMapsNavigator.areTermsAccepted();
if (!termsAccepted) {
await GoogleMapsNavigator.showTermsAndConditionsDialog(
'Plan B Logistics',
'com.goutezplanb.planbLogistic',
);
}
await GoogleMapsNavigator.initializeNavigationSession();
if (mounted) {
setState(() {
_isSessionInitialized = true;
_isInitializing = false;
});
}
} catch (e) {
final errorMessage = _formatErrorMessage(e);
debugPrint('Map initialization error: $errorMessage');
if (mounted) {
setState(() {
_isInitializing = false;
});
ToastHelper.showError(context, 'Navigation initialization failed: $errorMessage');
}
}
}
String _formatErrorMessage(Object error) {
final errorString = error.toString();
if (errorString.contains('SessionNotInitializedException')) {
return 'Google Maps navigation session could not be initialized';
} else if (errorString.contains('permission')) {
return 'Location permission is required for navigation';
} else if (errorString.contains('network')) {
return 'Network connection error';
}
return errorString;
}
void _updateDestination() {
if (widget.selectedDelivery != null) {
final address = widget.selectedDelivery!.deliveryAddress;
if (address?.latitude != null && address?.longitude != null) {
setState(() {
_destinationLocation = LatLng(
latitude: address!.latitude!,
longitude: address.longitude!,
);
});
// Just store the destination, don't move camera
// The navigation will handle camera positioning
}
}
}
Future<void> _applyDarkModeStyle() async {
// Check if widget is still mounted and controller exists
if (!mounted || _navigationController == null || _isDisposed || !_isMapViewReady) return;
try {
if (!mounted || _isDisposed) return;
// Force dark mode map style using Google's standard dark theme
const String darkMapStyle = '''
[
{
"elementType": "geometry",
"stylers": [{"color": "#242f3e"}]
},
{
"elementType": "labels.text.stroke",
"stylers": [{"color": "#242f3e"}]
},
{
"elementType": "labels.text.fill",
"stylers": [{"color": "#746855"}]
},
{
"featureType": "administrative.locality",
"elementType": "labels.text.fill",
"stylers": [{"color": "#d59563"}]
},
{
"featureType": "poi",
"elementType": "labels.text.fill",
"stylers": [{"color": "#d59563"}]
},
{
"featureType": "poi.park",
"elementType": "geometry",
"stylers": [{"color": "#263c3f"}]
},
{
"featureType": "poi.park",
"elementType": "labels.text.fill",
"stylers": [{"color": "#6b9a76"}]
},
{
"featureType": "road",
"elementType": "geometry",
"stylers": [{"color": "#38414e"}]
},
{
"featureType": "road",
"elementType": "geometry.stroke",
"stylers": [{"color": "#212a37"}]
},
{
"featureType": "road",
"elementType": "labels.text.fill",
"stylers": [{"color": "#9ca5b3"}]
},
{
"featureType": "road.highway",
"elementType": "geometry",
"stylers": [{"color": "#746855"}]
},
{
"featureType": "road.highway",
"elementType": "geometry.stroke",
"stylers": [{"color": "#1f2835"}]
},
{
"featureType": "road.highway",
"elementType": "labels.text.fill",
"stylers": [{"color": "#f3d19c"}]
},
{
"featureType": "transit",
"elementType": "geometry",
"stylers": [{"color": "#2f3948"}]
},
{
"featureType": "transit.station",
"elementType": "labels.text.fill",
"stylers": [{"color": "#d59563"}]
},
{
"featureType": "water",
"elementType": "geometry",
"stylers": [{"color": "#17263c"}]
},
{
"featureType": "water",
"elementType": "labels.text.fill",
"stylers": [{"color": "#515c6d"}]
},
{
"featureType": "water",
"elementType": "labels.text.stroke",
"stylers": [{"color": "#17263c"}]
}
]
''';
await _navigationController!.setMapStyle(darkMapStyle);
debugPrint('Dark mode map style applied');
} catch (e) {
if (mounted) {
debugPrint('Error applying map style: $e');
}
}
}
Future<void> _startNavigation() async {
if (_destinationLocation == null) return;
// Show loading indicator
if (mounted) {
setState(() {
_isStartingNavigation = true;
_loadingMessage = 'Starting navigation...';
});
}
try {
// Ensure session is initialized before starting navigation
if (!_isSessionInitialized && !_isInitializing) {
debugPrint('Initializing navigation session...');
await _initializeNavigation();
}
// Wait for initialization to complete if it's in progress
int retries = 0;
while (!_isSessionInitialized && retries < 30) {
await Future.delayed(const Duration(milliseconds: 100));
retries++;
}
if (!_isSessionInitialized) {
if (mounted) {
setState(() {
_isStartingNavigation = false;
});
ToastHelper.showError(context, 'Navigation initialization timeout');
}
return;
}
if (mounted) {
setState(() {
_loadingMessage = 'Setting destination...';
});
}
final waypoint = NavigationWaypoint.withLatLngTarget(
title: widget.selectedDelivery?.name ?? 'Destination',
target: _destinationLocation!,
);
final destinations = Destinations(
waypoints: [waypoint],
displayOptions: NavigationDisplayOptions(showDestinationMarkers: true),
);
if (mounted) {
setState(() {
_loadingMessage = 'Starting guidance...';
});
}
debugPrint('Setting destinations: ${_destinationLocation!.latitude}, ${_destinationLocation!.longitude}');
await GoogleMapsNavigator.setDestinations(destinations);
debugPrint('Starting guidance...');
await GoogleMapsNavigator.startGuidance();
debugPrint('Navigation started successfully');
// On iOS Simulator in debug mode, start simulation to provide location updates
// The iOS Simulator doesn't provide continuous location updates for custom locations,
// so we use the SDK's built-in simulation to simulate driving along the route.
// This is only needed for testing on iOS Simulator - real devices work without this.
if (kDebugMode && Platform.isIOS) {
try {
// Start simulating the route with a speed multiplier for testing
// speedMultiplier: 1.0 = normal speed, 5.0 = 5x faster for quicker testing
await GoogleMapsNavigator.simulator.simulateLocationsAlongExistingRouteWithOptions(
SimulationOptions(speedMultiplier: 5.0),
);
debugPrint('Simulation started for iOS Simulator testing');
} catch (e) {
debugPrint('Could not start simulation: $e');
}
}
// Reapply dark mode style after navigation starts
if (mounted) {
await _applyDarkModeStyle();
}
// Auto-recenter on driver location when navigation starts
await _recenterMap();
debugPrint('Camera recentered on driver location');
if (mounted) {
setState(() {
_isNavigating = true;
_isStartingNavigation = false;
});
}
} catch (e) {
final errorMessage = _formatErrorMessage(e);
debugPrint('Navigation start error: $errorMessage');
debugPrint('Full error: $e');
if (mounted) {
setState(() {
_isStartingNavigation = false;
});
ToastHelper.showError(context, 'Navigation error: $errorMessage', duration: const Duration(seconds: 4));
}
}
}
Future<void> _stopNavigation() async {
try {
// Stop simulation if it was running (iOS Simulator)
if (kDebugMode && Platform.isIOS) {
try {
// Remove simulated user location to stop the simulation
await GoogleMapsNavigator.simulator.removeUserLocation();
debugPrint('Simulation stopped');
} catch (e) {
debugPrint('Could not stop simulation: $e');
}
}
await GoogleMapsNavigator.stopGuidance();
await GoogleMapsNavigator.clearDestinations();
if (mounted) {
setState(() {
_isNavigating = false;
});
}
} catch (e) {
if (mounted) {
debugPrint('Navigation stop error: $e');
}
}
}
Future<void> _recenterMap() async {
if (_navigationController == null) return;
try {
// Use the navigation controller's follow location feature
// This tells the navigation to follow the driver's current location
await _navigationController!.followMyLocation(CameraPerspective.tilted);
debugPrint('Navigation set to follow driver location');
} catch (e) {
debugPrint('Recenter map error: $e');
}
}
bool _hasNotes() {
if (widget.selectedDelivery == null) return false;
return widget.selectedDelivery!.orders.any((order) =>
order.note != null && order.note!.isNotEmpty
);
}
@override
Widget build(BuildContext context) {
// Driver's current location (defaults to Montreal if not available)
final initialPosition = const LatLng(latitude: 45.5017, longitude: -73.5673);
// Calculate dynamic padding for bottom button bar
final topPadding = 0.0;
final bottomPadding = 60.0;
return Stack(
children: [
// Map with padding to accommodate overlaid elements
Padding(
padding: EdgeInsets.only(
top: topPadding,
bottom: bottomPadding,
),
child: GoogleMapsNavigationView(
// Enable navigation UI automatically when guidance starts
// This is critical for iOS to display turn-by-turn directions, ETA, distance
initialNavigationUIEnabledPreference: NavigationUIEnabledPreference.automatic,
onViewCreated: (controller) async {
// Early exit if widget is already disposed
if (_isDisposed || !mounted) return;
_navigationController = controller;
// Wait longer for the map to be fully initialized on Android
// This helps prevent crashes when the view is disposed during initialization
await Future.delayed(const Duration(milliseconds: 1500));
// Safety check: ensure widget is still mounted before proceeding
if (!mounted || _isDisposed) {
_navigationController = null;
return;
}
// Mark map as ready only after the delay
_isMapViewReady = true;
// Enable navigation UI elements (header with turn directions, footer with ETA/distance)
// This is required for iOS to show trip info, duration, and ETA
try {
if (!mounted || _isDisposed) return;
await controller.setNavigationUIEnabled(true);
if (!mounted || _isDisposed) return;
await controller.setNavigationHeaderEnabled(true);
if (!mounted || _isDisposed) return;
await controller.setNavigationFooterEnabled(true);
if (!mounted || _isDisposed) return;
await controller.setNavigationTripProgressBarEnabled(true);
if (!mounted || _isDisposed) return;
// Disable report incident button
await controller.setReportIncidentButtonEnabled(false);
debugPrint('Navigation UI elements enabled');
// Configure map settings to reduce GPU load for devices with limited graphics capabilities
if (!mounted || _isDisposed) return;
await controller.settings.setTrafficEnabled(true);
if (!mounted || _isDisposed) return;
await controller.settings.setRotateGesturesEnabled(true);
if (!mounted || _isDisposed) return;
await controller.settings.setTiltGesturesEnabled(false);
if (!mounted || _isDisposed) return;
debugPrint('Map settings configured for performance');
} catch (e) {
debugPrint('Error configuring map: $e');
if (_isDisposed || !mounted) return;
}
if (!mounted || _isDisposed) return;
await _applyDarkModeStyle();
// Wrap camera animation in try-catch to handle "No valid view found" errors
// This can happen on Android when the view isn't fully ready
try {
if (mounted && _navigationController != null && _isMapViewReady && !_isDisposed) {
await controller.animateCamera(
CameraUpdate.newLatLngZoom(initialPosition, 12),
);
// Auto-recenter to current location after initial setup
await Future.delayed(const Duration(milliseconds: 500));
if (mounted && _navigationController != null && !_isDisposed) {
await _recenterMap();
debugPrint('Auto-recentered map to current location on initialization');
}
}
} catch (e) {
debugPrint('Camera animation error (view may not be ready): $e');
if (_isDisposed || !mounted) return;
// Retry once after a longer delay
await Future.delayed(const Duration(milliseconds: 1500));
if (mounted && _navigationController != null && _isMapViewReady && !_isDisposed) {
try {
await controller.animateCamera(
CameraUpdate.newLatLngZoom(initialPosition, 12),
);
// Auto-recenter to current location after retry
await Future.delayed(const Duration(milliseconds: 500));
if (mounted && _navigationController != null && !_isDisposed) {
await _recenterMap();
debugPrint('Auto-recentered map to current location on initialization (retry)');
}
} catch (e2) {
debugPrint('Camera animation retry failed: $e2');
}
}
}
},
initialCameraPosition: CameraPosition(
target: initialPosition,
zoom: 12,
),
),
),
// Bottom action button bar - 4 equal-width buttons (always visible)
Positioned(
bottom: 0,
left: 0,
right: 0,
child: Container(
decoration: BoxDecoration(
color: Theme.of(context).colorScheme.surface,
boxShadow: [
BoxShadow(
color: Colors.black.withValues(alpha: 0.2),
blurRadius: 8,
offset: const Offset(0, -2),
),
],
),
padding: const EdgeInsets.symmetric(
horizontal: 12,
vertical: 8,
),
child: Row(
children: [
// Start button
Expanded(
child: _buildBottomActionButton(
label: _isNavigating ? 'Stop' : 'Start',
icon: _isNavigating ? Icons.stop : Icons.navigation,
onPressed: _isStartingNavigation || _isInitializing || (widget.selectedDelivery == null && !_isNavigating)
? null
: (_isNavigating ? _stopNavigation : _startNavigation),
isDanger: _isNavigating,
),
),
const SizedBox(width: 8),
// Photo button (disabled when no delivery selected or warehouse delivery)
Expanded(
child: _buildBottomActionButton(
label: 'Photo',
icon: Icons.camera_alt,
onPressed: widget.selectedDelivery != null && !widget.selectedDelivery!.isWarehouseDelivery
? () => widget.onAction?.call('photo')
: null,
),
),
const SizedBox(width: 8),
// Note button (only enabled if delivery has notes and not warehouse)
Expanded(
child: _buildBottomActionButton(
label: 'Note',
icon: Icons.note_add,
onPressed: _hasNotes() && widget.selectedDelivery != null && !widget.selectedDelivery!.isWarehouseDelivery
? () => widget.onAction?.call('note')
: null,
),
),
const SizedBox(width: 8),
// Completed button (disabled for warehouse delivery)
Expanded(
child: _buildBottomActionButton(
label: widget.selectedDelivery?.delivered == true ? 'Undo' : 'Completed',
icon: widget.selectedDelivery?.delivered == true ? Icons.undo : Icons.check_circle,
onPressed: widget.selectedDelivery != null && !widget.selectedDelivery!.isWarehouseDelivery
? () => widget.onAction?.call(
widget.selectedDelivery!.delivered ? 'uncomplete' : 'complete',
)
: null,
isPrimary: widget.selectedDelivery != null && !widget.selectedDelivery!.delivered && !widget.selectedDelivery!.isWarehouseDelivery,
),
),
],
),
),
),
// Loading overlay during navigation initialization and start
if (_isStartingNavigation || _isInitializing)
Positioned.fill(
child: Container(
color: Colors.black.withValues(alpha: 0.4),
child: Center(
child: Container(
padding: const EdgeInsets.all(24),
decoration: BoxDecoration(
color: Theme.of(context).colorScheme.surface,
borderRadius: BorderRadius.circular(12),
boxShadow: [
BoxShadow(
color: Colors.black.withValues(alpha: 0.3),
blurRadius: 12,
offset: const Offset(0, 4),
),
],
),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
// Circular progress indicator
SizedBox(
width: 60,
height: 60,
child: CircularProgressIndicator(
valueColor: AlwaysStoppedAnimation<Color>(
Theme.of(context).colorScheme.primary,
),
strokeWidth: 3,
),
),
const SizedBox(height: 16),
// Loading message
Text(
_loadingMessage,
style: Theme.of(context).textTheme.titleMedium?.copyWith(
fontWeight: FontWeight.w600,
),
textAlign: TextAlign.center,
),
const SizedBox(height: 8),
// Secondary message
Text(
'Please wait...',
style: Theme.of(context).textTheme.bodySmall?.copyWith(
color: Theme.of(context).colorScheme.onSurface.withValues(alpha: 0.7),
),
textAlign: TextAlign.center,
),
],
),
),
),
),
),
],
);
}
Widget _buildBottomActionButton({
required String label,
required IconData icon,
required VoidCallback? onPressed,
bool isPrimary = false,
bool isDanger = false,
}) {
Color backgroundColor;
Color textColor = Colors.white;
if (isDanger) {
backgroundColor = SvrntyColors.crimsonRed;
} else if (isPrimary) {
backgroundColor = SvrntyColors.crimsonRed;
} else {
// Use the same slateGray as delivery list badges
backgroundColor = SvrntyColors.slateGray;
}
// Reduce opacity when disabled
if (onPressed == null) {
backgroundColor = backgroundColor.withValues(alpha: 0.5);
}
return Material(
color: backgroundColor,
borderRadius: BorderRadius.circular(6),
child: InkWell(
onTap: onPressed,
borderRadius: BorderRadius.circular(6),
child: Padding(
padding: const EdgeInsets.symmetric(
horizontal: 8,
vertical: 10,
),
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
mainAxisSize: MainAxisSize.min,
children: [
Icon(
icon,
color: textColor,
size: 18,
),
const SizedBox(width: 6),
Text(
label,
style: TextStyle(
color: textColor,
fontWeight: FontWeight.w600,
fontSize: 14,
),
),
],
),
),
),
);
}
Widget _buildActionButton({
required String label,
required IconData icon,
required VoidCallback? onPressed,
required Color color,
}) {
final isDisabled = onPressed == null;
final buttonColor = isDisabled ? color.withValues(alpha: 0.5) : color;
return Container(
margin: const EdgeInsets.only(bottom: 8),
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(8),
boxShadow: [
BoxShadow(
color: Colors.black.withValues(alpha: 0.3),
blurRadius: 4,
offset: const Offset(0, 2),
),
],
),
child: Material(
color: buttonColor,
borderRadius: BorderRadius.circular(8),
child: InkWell(
onTap: onPressed,
borderRadius: BorderRadius.circular(8),
child: Padding(
padding: const EdgeInsets.symmetric(
horizontal: 12,
vertical: 8,
),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
Icon(
icon,
color: Colors.white,
size: 18,
),
const SizedBox(width: 6),
Text(
label,
style: const TextStyle(
color: Colors.white,
fontWeight: FontWeight.w500,
fontSize: 14,
),
),
],
),
),
),
),
);
}
}

View File

@ -1,371 +0,0 @@
import 'package:flutter/material.dart';
import '../models/delivery.dart';
import '../theme/animation_system.dart';
import '../theme/color_system.dart';
import '../l10n/app_localizations.dart';
class DeliveryListItem extends StatefulWidget {
final Delivery delivery;
final bool isSelected;
final VoidCallback onTap;
final VoidCallback? onCall;
final Function(String)? onAction;
final int? animationIndex;
final bool isCollapsed;
const DeliveryListItem({
super.key,
required this.delivery,
required this.isSelected,
required this.onTap,
this.onCall,
this.onAction,
this.animationIndex,
this.isCollapsed = false,
});
@override
State<DeliveryListItem> createState() => _DeliveryListItemState();
}
class _DeliveryListItemState extends State<DeliveryListItem>
with SingleTickerProviderStateMixin {
late AnimationController _controller;
late Animation<double> _slideAnimation;
late Animation<double> _fadeAnimation;
late Animation<double> _scaleAnimation;
bool _isHovered = false;
@override
void initState() {
super.initState();
_controller = AnimationController(
duration: const Duration(milliseconds: 400),
vsync: this,
);
final staggerDelay = Duration(
milliseconds:
(widget.animationIndex ?? 0) * AppAnimations.staggerDelayMs,
);
Future.delayed(staggerDelay, () {
if (mounted) {
_controller.forward();
}
});
_slideAnimation = Tween<double>(begin: 0, end: 0).animate(
CurvedAnimation(parent: _controller, curve: Curves.easeOut),
);
_fadeAnimation = Tween<double>(begin: 0, end: 1).animate(
CurvedAnimation(parent: _controller, curve: Curves.easeOut),
);
_scaleAnimation = Tween<double>(begin: 0.95, end: 1).animate(
CurvedAnimation(parent: _controller, curve: Curves.easeOut),
);
}
@override
void dispose() {
_controller.dispose();
super.dispose();
}
Color _getStatusColor(Delivery delivery) {
// If delivered, always show green (even if selected)
if (delivery.delivered == true) return SvrntyColors.success;
// If selected and not delivered, show yellow/warning color
if (widget.isSelected) return SvrntyColors.warning;
// If skipped, show grey
if (delivery.isSkipped == true) return SvrntyColors.statusCancelled;
// Default: in-transit or pending deliveries
return SvrntyColors.statusInTransit;
}
bool _hasNote() {
return widget.delivery.orders.any((order) =>
order.note != null && order.note!.isNotEmpty
);
}
@override
Widget build(BuildContext context) {
final isDark = Theme.of(context).brightness == Brightness.dark;
final statusColor = _getStatusColor(widget.delivery);
final l10n = AppLocalizations.of(context)!;
// Collapsed view: Show only the badge
if (widget.isCollapsed) {
return ScaleTransition(
scale: _scaleAnimation,
child: FadeTransition(
opacity: _fadeAnimation,
child: MouseRegion(
onEnter: (_) => setState(() => _isHovered = true),
onExit: (_) => setState(() => _isHovered = false),
child: GestureDetector(
onTap: widget.onTap,
child: Container(
margin: const EdgeInsets.symmetric(
horizontal: 10,
vertical: 10,
),
child: Center(
child: Stack(
clipBehavior: Clip.none,
children: [
Container(
width: 60,
height: 60,
decoration: BoxDecoration(
color: statusColor,
borderRadius: BorderRadius.circular(10),
border: widget.isSelected
? Border.all(
color: Colors.white,
width: 3,
)
: null,
boxShadow: (_isHovered || widget.isSelected)
? [
BoxShadow(
color: widget.isSelected
? statusColor.withValues(alpha: 0.5)
: Colors.black.withValues(
alpha: isDark ? 0.3 : 0.15,
),
blurRadius: widget.isSelected ? 12 : 8,
offset: const Offset(0, 4),
spreadRadius: widget.isSelected ? 2 : 0,
),
]
: [],
),
child: Center(
child: widget.delivery.isWarehouseDelivery
? const Icon(
Icons.warehouse,
color: Colors.white,
size: 32,
)
: Text(
'${widget.delivery.deliveryIndex + 1}',
style: const TextStyle(
color: Colors.white,
fontSize: 26,
fontWeight: FontWeight.w700,
),
),
),
),
if (_hasNote())
Positioned(
top: -4,
right: -4,
child: Container(
width: 20,
height: 20,
decoration: BoxDecoration(
color: Theme.of(context).colorScheme.primary,
shape: BoxShape.circle,
border: Border.all(
color: Theme.of(context).colorScheme.surface,
width: 2,
),
),
child: Transform.rotate(
angle: 4.71239, // 270 degrees in radians (3*pi/2)
child: const Icon(
Icons.note,
size: 12,
color: Colors.white,
),
),
),
),
],
),
),
),
),
),
),
);
}
// Expanded view: Show full layout
return ScaleTransition(
scale: _scaleAnimation,
child: FadeTransition(
opacity: _fadeAnimation,
child: Transform.translate(
offset: Offset(_slideAnimation.value, 0),
child: MouseRegion(
onEnter: (_) => setState(() => _isHovered = true),
onExit: (_) => setState(() => _isHovered = false),
child: GestureDetector(
onTap: widget.onTap,
child: AnimatedContainer(
duration: AppAnimations.durationFast,
margin: const EdgeInsets.symmetric(
horizontal: 2,
vertical: 4,
),
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(8),
color: widget.delivery.delivered
? Colors.green.withValues(alpha: 0.15)
: widget.isSelected
? statusColor.withValues(alpha: 0.15)
: Theme.of(context).colorScheme.surfaceContainer,
border: widget.isSelected
? Border.all(
color: widget.delivery.delivered
? SvrntyColors.success
: statusColor,
width: 2,
)
: null,
boxShadow: (_isHovered || widget.isSelected) && !widget.delivery.delivered
? [
BoxShadow(
color: Colors.black.withValues(
alpha: isDark ? 0.3 : 0.08,
),
blurRadius: 8,
offset: const Offset(0, 4),
),
]
: [],
),
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 12),
child: Stack(
clipBehavior: Clip.none,
children: [
Column(
children: [
// Main delivery info row
Row(
crossAxisAlignment: CrossAxisAlignment.center,
children: [
// Order number badge (left of status bar)
Container(
width: 40,
height: 40,
decoration: BoxDecoration(
color: statusColor,
borderRadius: BorderRadius.circular(8),
),
child: Center(
child: widget.delivery.isWarehouseDelivery
? const Icon(
Icons.warehouse,
color: Colors.white,
size: 24,
)
: Text(
'${widget.delivery.deliveryIndex + 1}',
style: const TextStyle(
color: Colors.white,
fontSize: 18,
fontWeight: FontWeight.w700,
),
),
),
),
const SizedBox(width: 8),
// Left accent bar (vertical status bar)
Container(
width: 4,
height: 50,
decoration: BoxDecoration(
color: statusColor,
borderRadius: BorderRadius.circular(2),
),
),
const SizedBox(width: 10),
// Delivery info
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// Customer Name
Text(
widget.delivery.name,
style: Theme.of(context)
.textTheme
.titleMedium
?.copyWith(
fontWeight: FontWeight.w600,
fontSize: 16,
),
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
const SizedBox(height: 4),
// Address
Text(
widget.delivery.deliveryAddress
?.formattedAddress ??
l10n.noAddress,
style: Theme.of(context)
.textTheme
.bodyMedium
?.copyWith(
fontSize: 13,
),
maxLines: 2,
overflow: TextOverflow.ellipsis,
),
],
),
),
],
),
],
),
if (_hasNote())
Positioned(
top: -8,
right: -4,
child: Container(
width: 24,
height: 24,
decoration: BoxDecoration(
color: Theme.of(context).colorScheme.primary,
shape: BoxShape.circle,
border: Border.all(
color: Theme.of(context).colorScheme.surface,
width: 2,
),
boxShadow: [
BoxShadow(
color: Colors.black.withValues(alpha: 0.2),
blurRadius: 4,
offset: const Offset(0, 2),
),
],
),
child: Transform.rotate(
angle: 4.71239, // 270 degrees in radians (3*pi/2)
child: const Icon(
Icons.note,
size: 14,
color: Colors.white,
),
),
),
),
],
),
),
),
),
),
),
);
}
}

View File

@ -1,207 +0,0 @@
import 'package:flutter/material.dart';
import 'package:google_navigation_flutter/google_navigation_flutter.dart';
import '../models/delivery.dart';
class DeliveryMap extends StatefulWidget {
final List<Delivery> deliveries;
final Delivery? selectedDelivery;
final ValueChanged<Delivery?>? onDeliverySelected;
const DeliveryMap({
super.key,
required this.deliveries,
this.selectedDelivery,
this.onDeliverySelected,
});
@override
State<DeliveryMap> createState() => _DeliveryMapState();
}
class _DeliveryMapState extends State<DeliveryMap> {
GoogleNavigationViewController? _navigationController;
bool _isNavigating = false;
LatLng? _destinationLocation;
@override
void initState() {
super.initState();
_initializeNavigation();
}
Future<void> _initializeNavigation() async {
try {
debugPrint('🗺️ Starting navigation initialization');
// Check if terms and conditions need to be shown
final termsAccepted = await GoogleMapsNavigator.areTermsAccepted();
debugPrint('🗺️ Terms accepted: $termsAccepted');
if (!termsAccepted) {
debugPrint('🗺️ Showing terms and conditions dialog');
// Show terms and conditions
await GoogleMapsNavigator.showTermsAndConditionsDialog(
'Plan B Logistics',
'com.goutezplanb.planbLogistic',
);
}
// Initialize navigation session
debugPrint('🗺️ Initializing navigation session');
await GoogleMapsNavigator.initializeNavigationSession();
debugPrint('🗺️ Navigation session initialized successfully');
} catch (e) {
debugPrint('❌ Error initializing navigation: $e');
}
}
@override
void didUpdateWidget(DeliveryMap oldWidget) {
super.didUpdateWidget(oldWidget);
if (oldWidget.selectedDelivery != widget.selectedDelivery) {
_updateDestination();
}
}
void _updateDestination() {
if (widget.selectedDelivery != null) {
final address = widget.selectedDelivery!.deliveryAddress;
if (address?.latitude != null && address?.longitude != null) {
setState(() {
_destinationLocation = LatLng(
latitude: address!.latitude!,
longitude: address.longitude!,
);
});
_navigateToLocation(_destinationLocation!);
}
}
}
Future<void> _navigateToLocation(LatLng location) async {
if (_navigationController == null) return;
try {
await _navigationController!.animateCamera(
CameraUpdate.newLatLngZoom(location, 15),
);
} catch (e) {
debugPrint('Error moving camera: $e');
}
}
Future<void> _startNavigation() async {
if (_destinationLocation == null) return;
try {
final waypoint = NavigationWaypoint.withLatLngTarget(
title: widget.selectedDelivery?.name ?? 'Destination',
target: _destinationLocation!,
);
final destinations = Destinations(
waypoints: [waypoint],
displayOptions: NavigationDisplayOptions(showDestinationMarkers: true),
);
await GoogleMapsNavigator.setDestinations(destinations);
await GoogleMapsNavigator.startGuidance();
setState(() {
_isNavigating = true;
});
} catch (e) {
debugPrint('Error starting navigation: $e');
if (mounted) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('Error starting navigation: $e')),
);
}
}
}
Future<void> _stopNavigation() async {
try {
await GoogleMapsNavigator.stopGuidance();
await GoogleMapsNavigator.clearDestinations();
setState(() {
_isNavigating = false;
});
} catch (e) {
debugPrint('Error stopping navigation: $e');
}
}
@override
Widget build(BuildContext context) {
final initialPosition = widget.selectedDelivery?.deliveryAddress != null &&
widget.selectedDelivery!.deliveryAddress!.latitude != null &&
widget.selectedDelivery!.deliveryAddress!.longitude != null
? LatLng(
latitude: widget.selectedDelivery!.deliveryAddress!.latitude!,
longitude: widget.selectedDelivery!.deliveryAddress!.longitude!,
)
: const LatLng(latitude: 45.5017, longitude: -73.5673); // Default to Montreal
return Stack(
children: [
GoogleMapsNavigationView(
onViewCreated: (controller) {
debugPrint('🗺️ Map view created successfully');
_navigationController = controller;
controller.setMyLocationEnabled(true);
// Set initial camera position
controller.animateCamera(
CameraUpdate.newLatLngZoom(initialPosition, 12),
);
debugPrint('🗺️ Initial camera position set to: $initialPosition');
},
initialNavigationUIEnabledPreference: NavigationUIEnabledPreference.disabled,
initialCameraPosition: CameraPosition(
target: initialPosition,
zoom: 12,
),
),
if (_destinationLocation != null && !_isNavigating)
Positioned(
bottom: 24,
left: 24,
right: 24,
child: ElevatedButton.icon(
onPressed: _startNavigation,
icon: const Icon(Icons.navigation),
label: const Text('Start Navigation'),
style: ElevatedButton.styleFrom(
padding: const EdgeInsets.all(16),
backgroundColor: Theme.of(context).colorScheme.primary,
foregroundColor: Theme.of(context).colorScheme.onPrimary,
),
),
),
if (_isNavigating)
Positioned(
bottom: 24,
left: 24,
right: 24,
child: ElevatedButton.icon(
onPressed: _stopNavigation,
icon: const Icon(Icons.stop),
label: const Text('Stop Navigation'),
style: ElevatedButton.styleFrom(
padding: const EdgeInsets.all(16),
backgroundColor: Theme.of(context).colorScheme.error,
foregroundColor: Theme.of(context).colorScheme.onError,
),
),
),
],
);
}
@override
void dispose() {
GoogleMapsNavigator.cleanup();
super.dispose();
}
}

View File

@ -1,326 +0,0 @@
import 'package:flutter/material.dart';
import 'dart:ui' as ui;
import '../models/delivery_route.dart';
import '../theme/spacing_system.dart';
import '../theme/size_system.dart';
import '../theme/animation_system.dart';
import '../theme/color_system.dart';
/// Modern glassmorphic route card with status-based gradient and animated progress
class GlassmorphicRouteCard extends StatefulWidget {
final DeliveryRoute route;
final bool isSelected;
final VoidCallback onTap;
final bool isCollapsed;
const GlassmorphicRouteCard({
super.key,
required this.route,
required this.isSelected,
required this.onTap,
this.isCollapsed = false,
});
@override
State<GlassmorphicRouteCard> createState() => _GlassmorphicRouteCardState();
}
class _GlassmorphicRouteCardState extends State<GlassmorphicRouteCard>
with SingleTickerProviderStateMixin {
late AnimationController _hoverController;
@override
void initState() {
super.initState();
_hoverController = AnimationController(
duration: Duration(milliseconds: AppAnimations.durationFast.inMilliseconds),
vsync: this,
);
}
@override
void dispose() {
_hoverController.dispose();
super.dispose();
}
/// Calculate color based on completion percentage
Color _getProgressColor(double progress) {
if (progress < 0.3) {
// Red to orange (0-30%)
return Color.lerp(
SvrntyColors.crimsonRed,
const Color(0xFFFF9800),
(progress / 0.3),
)!;
} else if (progress < 0.7) {
// Orange to yellow (30-70%)
return Color.lerp(
const Color(0xFFFF9800),
const Color(0xFFFFC107),
((progress - 0.3) / 0.4),
)!;
} else {
// Yellow to green (70-100%)
return Color.lerp(
const Color(0xFFFFC107),
const Color(0xFF4CAF50),
((progress - 0.7) / 0.3),
)!;
}
}
void _setHovered(bool hovered) {
if (hovered) {
_hoverController.forward();
} else {
_hoverController.reverse();
}
}
@override
Widget build(BuildContext context) {
final isDarkMode = Theme.of(context).brightness == Brightness.dark;
final progress = widget.route.deliveredCount / widget.route.deliveriesCount;
final progressColor = _getProgressColor(progress);
if (widget.isCollapsed) {
return _buildCollapsedCard(context, isDarkMode, progress, progressColor);
}
return _buildExpandedCard(context, isDarkMode, progress, progressColor);
}
Widget _buildCollapsedCard(BuildContext context, bool isDarkMode,
double progress, Color progressColor) {
return MouseRegion(
onEnter: (_) => _setHovered(true),
onExit: (_) => _setHovered(false),
child: GestureDetector(
onTap: widget.onTap,
child: AnimatedContainer(
duration: Duration(
milliseconds: AppAnimations.durationFast.inMilliseconds,
),
height: AppSizes.buttonHeightMd,
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(AppSpacing.md),
),
child: Stack(
alignment: Alignment.center,
children: [
// Glassmorphic background
ClipRRect(
borderRadius: BorderRadius.circular(AppSpacing.md),
child: BackdropFilter(
filter: ui.ImageFilter.blur(sigmaX: 8, sigmaY: 8),
child: Container(
decoration: BoxDecoration(
color: (isDarkMode
? SvrntyColors.darkSlate
: Colors.white)
.withValues(alpha: 0.7),
border: Border.all(
color: (isDarkMode ? Colors.white : Colors.white)
.withValues(alpha: 0.2),
width: 1.5,
),
),
),
),
),
// Progress indicator at bottom
Positioned(
bottom: 0,
left: 0,
right: 0,
child: ClipRRect(
borderRadius: BorderRadius.only(
bottomLeft: Radius.circular(AppSpacing.md - AppSpacing.xs),
bottomRight: Radius.circular(AppSpacing.md - AppSpacing.xs),
),
child: LinearProgressIndicator(
value: progress,
minHeight: 3,
backgroundColor: progressColor.withValues(alpha: 0.2),
valueColor: AlwaysStoppedAnimation<Color>(progressColor),
),
),
),
// Content
Center(
child: Text(
widget.route.name.substring(0, 1).toUpperCase(),
style: Theme.of(context).textTheme.labelLarge?.copyWith(
fontWeight: FontWeight.bold,
fontSize: 16,
color: widget.isSelected ? progressColor : null,
),
),
),
],
),
),
),
);
}
Widget _buildExpandedCard(BuildContext context, bool isDarkMode,
double progress, Color progressColor) {
return MouseRegion(
onEnter: (_) => _setHovered(true),
onExit: (_) => _setHovered(false),
child: GestureDetector(
onTap: widget.onTap,
child: AnimatedBuilder(
animation: _hoverController,
builder: (context, child) {
final hoverValue = _hoverController.value;
final blurSigma = 8 + (hoverValue * 3);
final bgOpacity = 0.7 + (hoverValue * 0.1);
return AnimatedContainer(
duration: Duration(
milliseconds: AppAnimations.durationFast.inMilliseconds,
),
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(AppSpacing.lg),
boxShadow: [
BoxShadow(
color: progressColor.withValues(alpha: 0.1 + (hoverValue * 0.2)),
blurRadius: 12 + (hoverValue * 8),
offset: Offset(0, 2 + (hoverValue * 2)),
),
],
),
child: Stack(
children: [
// Glassmorphic background
ClipRRect(
borderRadius: BorderRadius.circular(AppSpacing.lg),
child: BackdropFilter(
filter: ui.ImageFilter.blur(sigmaX: blurSigma, sigmaY: blurSigma),
child: Container(
decoration: BoxDecoration(
color: (isDarkMode
? SvrntyColors.darkSlate
: Colors.white)
.withValues(alpha: bgOpacity),
border: Border.all(
color: (isDarkMode ? Colors.white : Colors.white)
.withValues(alpha: 0.2 + (hoverValue * 0.15)),
width: 1.5,
),
borderRadius: BorderRadius.circular(AppSpacing.lg),
),
),
),
),
// Content
Padding(
padding: EdgeInsets.all(AppSpacing.md),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisSize: MainAxisSize.min,
children: [
// Route name
Text(
widget.route.name,
style: Theme.of(context)
.textTheme
.labelLarge
?.copyWith(
color: widget.isSelected ? progressColor : null,
fontWeight: FontWeight.bold,
fontSize: 14,
),
maxLines: 2,
overflow: TextOverflow.ellipsis,
),
SizedBox(height: AppSpacing.xs),
// Delivery count
RichText(
text: TextSpan(
children: [
TextSpan(
text: '${widget.route.deliveredCount}',
style: Theme.of(context)
.textTheme
.bodySmall
?.copyWith(
color: progressColor,
fontWeight: FontWeight.w600,
fontSize: 11,
),
),
TextSpan(
text: '/${widget.route.deliveriesCount}',
style: Theme.of(context)
.textTheme
.bodySmall
?.copyWith(
fontSize: 11,
color: (Theme.of(context).brightness == Brightness.dark
? Colors.white
: Colors.black)
.withValues(alpha: 0.6),
),
),
],
),
),
SizedBox(height: AppSpacing.sm),
// Animated gradient progress bar
ClipRRect(
borderRadius: BorderRadius.circular(3),
child: Stack(
children: [
// Background
Container(
height: 6,
decoration: BoxDecoration(
color: progressColor
.withValues(alpha: 0.15),
),
),
// Progress fill with gradient
ClipRRect(
borderRadius: BorderRadius.circular(3),
child: Container(
height: 6,
width: 100 * progress,
decoration: BoxDecoration(
gradient: LinearGradient(
begin: Alignment.centerLeft,
end: Alignment.centerRight,
colors: [
SvrntyColors.crimsonRed,
progressColor,
],
),
borderRadius: BorderRadius.circular(3),
boxShadow: [
BoxShadow(
color: progressColor
.withValues(alpha: 0.4),
blurRadius: 4,
),
],
),
),
),
],
),
),
],
),
),
],
),
);
},
),
),
);
}
}

View File

@ -1,144 +0,0 @@
import 'package:flutter/material.dart';
import '../utils/breakpoints.dart';
import '../theme/spacing_system.dart';
import '../theme/size_system.dart';
import '../theme/animation_system.dart';
import '../theme/color_system.dart';
class MapSidebarLayout extends StatefulWidget {
final Widget mapWidget;
final Widget Function(bool isCollapsed)? sidebarBuilder;
final Widget? sidebarWidget;
final double mapRatio;
const MapSidebarLayout({
super.key,
required this.mapWidget,
this.sidebarBuilder,
this.sidebarWidget,
this.mapRatio = 0.60, // Reduced from 2/3 to give 15% more space to sidebar
}) : assert(sidebarBuilder != null || sidebarWidget != null,
'Either sidebarBuilder or sidebarWidget must be provided');
@override
State<MapSidebarLayout> createState() => _MapSidebarLayoutState();
}
class _MapSidebarLayoutState extends State<MapSidebarLayout>
with SingleTickerProviderStateMixin {
late AnimationController _animationController;
bool _isExpanded = true;
@override
void initState() {
super.initState();
_animationController = AnimationController(
duration: const Duration(milliseconds: 300),
vsync: this,
);
if (_isExpanded) {
_animationController.forward();
}
}
@override
void dispose() {
_animationController.dispose();
super.dispose();
}
void _toggleSidebar() {
setState(() {
_isExpanded = !_isExpanded;
});
if (_isExpanded) {
_animationController.forward();
} else {
_animationController.reverse();
}
}
@override
Widget build(BuildContext context) {
final isMobile = MediaQuery.of(context).size.width < Breakpoints.tablet;
final isDarkMode = Theme.of(context).brightness == Brightness.dark;
if (isMobile) {
return widget.sidebarBuilder != null
? widget.sidebarBuilder!(false)
: widget.sidebarWidget!;
}
// Desktop: Show map with collapsible sidebar
return Row(
children: [
Expanded(
flex: (widget.mapRatio * 100).toInt(),
child: widget.mapWidget,
),
// Collapsible sidebar with toggle button (expanded: 420px, collapsed: 80px for badge)
AnimatedContainer(
duration: const Duration(milliseconds: 300),
width: _isExpanded ? 420 : 80,
color: isDarkMode ? SvrntyColors.almostBlack : Colors.white,
child: Column(
children: [
// Header with toggle button
Container(
height: kToolbarHeight,
padding: EdgeInsets.symmetric(horizontal: AppSpacing.xs),
decoration: BoxDecoration(
border: Border(
left: BorderSide(
color: isDarkMode
? SvrntyColors.darkSlate
: SvrntyColors.slateGray.withValues(alpha: 0.2),
width: 1,
),
),
),
child: Row(
mainAxisAlignment: _isExpanded
? MainAxisAlignment.spaceBetween
: MainAxisAlignment.center,
children: [
if (_isExpanded)
Expanded(
child: Text(
'Deliveries',
style: Theme.of(context).textTheme.titleMedium,
overflow: TextOverflow.ellipsis,
),
),
SizedBox(
width: AppSizes.buttonHeightMd,
height: AppSizes.buttonHeightMd,
child: IconButton(
icon: AnimatedRotation(
turns: _isExpanded ? 0 : -0.5,
duration: Duration(
milliseconds:
AppAnimations.durationFast.inMilliseconds,
),
child: const Icon(Icons.chevron_right),
),
onPressed: _toggleSidebar,
iconSize: AppSizes.iconMd,
),
),
],
),
),
// Sidebar content
Expanded(
child: widget.sidebarBuilder != null
? widget.sidebarBuilder!(!_isExpanded)
: (_isExpanded ? widget.sidebarWidget! : const SizedBox.shrink()),
),
],
),
),
],
);
}
}

View File

@ -1,79 +0,0 @@
import 'package:flutter/material.dart';
import 'package:planb_logistic/l10n/app_localizations.dart';
class NavigationTermsAndConditionsDialog extends StatelessWidget {
final VoidCallback onAccept;
final VoidCallback? onDecline;
const NavigationTermsAndConditionsDialog({
Key? key,
required this.onAccept,
this.onDecline,
}) : super(key: key);
@override
Widget build(BuildContext context) {
final l10n = AppLocalizations.of(context);
final colorScheme = Theme.of(context).colorScheme;
return AlertDialog(
title: Text(
l10n.navigationTcTitle,
style: Theme.of(context).textTheme.headlineSmall?.copyWith(
color: colorScheme.onSurface,
),
),
content: SingleChildScrollView(
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
l10n.navigationTcDescription,
style: Theme.of(context).textTheme.bodyMedium?.copyWith(
color: colorScheme.onSurface,
),
),
const SizedBox(height: 16),
Text(
l10n.navigationTcAttribution,
style: Theme.of(context).textTheme.bodySmall?.copyWith(
color: colorScheme.onSurfaceVariant,
),
),
const SizedBox(height: 12),
Text(
l10n.navigationTcTerms,
style: Theme.of(context).textTheme.bodySmall?.copyWith(
color: colorScheme.onSurfaceVariant,
),
),
],
),
),
actions: [
if (onDecline != null)
TextButton(
onPressed: () {
Navigator.of(context).pop();
onDecline!();
},
child: Text(
l10n.decline,
style: TextStyle(color: colorScheme.error),
),
),
TextButton(
onPressed: () {
Navigator.of(context).pop();
onAccept();
},
child: Text(
l10n.accept,
style: TextStyle(color: colorScheme.primary),
),
),
],
);
}
}

View File

@ -1,193 +0,0 @@
import 'package:flutter/material.dart';
import '../models/delivery_route.dart';
import '../theme/animation_system.dart';
import '../theme/color_system.dart';
class PremiumRouteCard extends StatefulWidget {
final DeliveryRoute route;
final VoidCallback onTap;
final EdgeInsets padding;
const PremiumRouteCard({
super.key,
required this.route,
required this.onTap,
this.padding = const EdgeInsets.all(16.0),
});
@override
State<PremiumRouteCard> createState() => _PremiumRouteCardState();
}
class _PremiumRouteCardState extends State<PremiumRouteCard>
with SingleTickerProviderStateMixin {
late AnimationController _controller;
late Animation<double> _scaleAnimation;
late Animation<double> _shadowAnimation;
@override
void initState() {
super.initState();
_controller = AnimationController(
duration: AppAnimations.durationFast,
vsync: this,
);
_scaleAnimation = Tween<double>(begin: 1.0, end: 1.02).animate(
CurvedAnimation(parent: _controller, curve: Curves.easeOut),
);
_shadowAnimation = Tween<double>(begin: 2.0, end: 8.0).animate(
CurvedAnimation(parent: _controller, curve: Curves.easeOut),
);
}
@override
void dispose() {
_controller.dispose();
super.dispose();
}
void _onHoverEnter() {
_controller.forward();
}
void _onHoverExit() {
_controller.reverse();
}
@override
Widget build(BuildContext context) {
final isDark = Theme.of(context).brightness == Brightness.dark;
final progressPercentage = (widget.route.progress * 100).toStringAsFixed(0);
final isCompleted = widget.route.progress >= 1.0;
final accentColor = isCompleted ? SvrntyColors.statusCompleted : SvrntyColors.crimsonRed;
return MouseRegion(
onEnter: (_) => _onHoverEnter(),
onExit: (_) => _onHoverExit(),
child: GestureDetector(
onTap: widget.onTap,
child: ScaleTransition(
scale: _scaleAnimation,
child: AnimatedBuilder(
animation: _shadowAnimation,
builder: (context, child) {
return Container(
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(12),
boxShadow: [
BoxShadow(
color: Colors.black.withValues(alpha: isDark ? 0.3 : 0.1),
blurRadius: _shadowAnimation.value,
offset: Offset(0, _shadowAnimation.value * 0.5),
),
],
),
child: child,
);
},
child: Container(
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(12),
color: Theme.of(context).colorScheme.surface,
border: Border(
left: BorderSide(color: accentColor, width: 4),
),
),
padding: const EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
// Header
Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
widget.route.name,
style: Theme.of(context).textTheme.titleLarge?.copyWith(
fontWeight: FontWeight.w600,
letterSpacing: -0.3,
),
maxLines: 2,
overflow: TextOverflow.ellipsis,
),
const SizedBox(height: 8),
RichText(
text: TextSpan(
children: [
TextSpan(
text: '${widget.route.deliveredCount}/${widget.route.deliveriesCount}',
style: Theme.of(context).textTheme.bodySmall?.copyWith(
fontWeight: FontWeight.w600,
color: accentColor,
),
),
TextSpan(
text: ' completed',
style: Theme.of(context).textTheme.bodySmall?.copyWith(
color: Theme.of(context).textTheme.bodySmall?.color,
),
),
],
),
),
],
),
),
const SizedBox(width: 12),
Container(
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
decoration: BoxDecoration(
color: SvrntyColors.crimsonRed,
borderRadius: BorderRadius.circular(6),
),
child: Text(
widget.route.deliveriesCount.toString(),
style: Theme.of(context).textTheme.labelSmall?.copyWith(
color: Colors.white,
fontWeight: FontWeight.w600,
),
),
),
],
),
// Progress
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'$progressPercentage% progress',
style: Theme.of(context).textTheme.labelSmall?.copyWith(
fontSize: 11,
letterSpacing: 0.3,
),
),
const SizedBox(height: 8),
ClipRRect(
borderRadius: BorderRadius.circular(4),
child: SizedBox(
height: 6,
child: LinearProgressIndicator(
value: widget.route.progress,
backgroundColor: Theme.of(context).colorScheme.surfaceContainerHighest,
valueColor: AlwaysStoppedAnimation<Color>(accentColor),
),
),
),
],
),
],
),
),
),
),
),
);
}
}

View File

@ -1,258 +0,0 @@
import 'package:flutter/material.dart';
import '../models/delivery_route.dart';
import '../theme/animation_system.dart';
import '../theme/color_system.dart';
import '../l10n/app_localizations.dart';
class RouteListItem extends StatefulWidget {
final DeliveryRoute route;
final bool isSelected;
final VoidCallback onTap;
final int? animationIndex;
final bool isCollapsed;
const RouteListItem({
super.key,
required this.route,
required this.isSelected,
required this.onTap,
this.animationIndex,
this.isCollapsed = false,
});
@override
State<RouteListItem> createState() => _RouteListItemState();
}
class _RouteListItemState extends State<RouteListItem>
with SingleTickerProviderStateMixin {
late AnimationController _controller;
late Animation<double> _slideAnimation;
late Animation<double> _fadeAnimation;
late Animation<double> _scaleAnimation;
bool _isHovered = false;
@override
void initState() {
super.initState();
_controller = AnimationController(
duration: const Duration(milliseconds: 400),
vsync: this,
);
final staggerDelay = Duration(
milliseconds:
(widget.animationIndex ?? 0) * AppAnimations.staggerDelayMs,
);
Future.delayed(staggerDelay, () {
if (mounted) {
_controller.forward();
}
});
_slideAnimation = Tween<double>(begin: 20, end: 0).animate(
CurvedAnimation(parent: _controller, curve: Curves.easeOut),
);
_fadeAnimation = Tween<double>(begin: 0, end: 1).animate(
CurvedAnimation(parent: _controller, curve: Curves.easeOut),
);
_scaleAnimation = Tween<double>(begin: 0.95, end: 1).animate(
CurvedAnimation(parent: _controller, curve: Curves.easeOut),
);
}
@override
void dispose() {
_controller.dispose();
super.dispose();
}
Color _getStatusColor(DeliveryRoute route) {
if (route.completed) return SvrntyColors.statusCompleted; // Green
if (route.deliveredCount > 0) return SvrntyColors.warning; // Yellow - started but not complete
return SvrntyColors.statusCancelled; // Grey - not started
}
@override
Widget build(BuildContext context) {
final isDark = Theme.of(context).brightness == Brightness.dark;
final statusColor = _getStatusColor(widget.route);
final l10n = AppLocalizations.of(context)!;
// Collapsed view: Show only the badge
if (widget.isCollapsed) {
return ScaleTransition(
scale: _scaleAnimation,
child: FadeTransition(
opacity: _fadeAnimation,
child: MouseRegion(
onEnter: (_) => setState(() => _isHovered = true),
onExit: (_) => setState(() => _isHovered = false),
child: GestureDetector(
onTap: widget.onTap,
child: Container(
margin: const EdgeInsets.symmetric(
horizontal: 10,
vertical: 10,
),
child: Center(
child: Container(
width: 60,
height: 60,
decoration: BoxDecoration(
color: statusColor,
borderRadius: BorderRadius.circular(10),
boxShadow: (_isHovered || widget.isSelected)
? [
BoxShadow(
color: Colors.black.withValues(
alpha: isDark ? 0.3 : 0.15,
),
blurRadius: 8,
offset: const Offset(0, 4),
),
]
: [],
),
child: Center(
child: Text(
'${(widget.animationIndex ?? 0) + 1}',
style: const TextStyle(
color: Colors.white,
fontSize: 26,
fontWeight: FontWeight.w700,
),
),
),
),
),
),
),
),
),
);
}
// Expanded view: Show full layout
return ScaleTransition(
scale: _scaleAnimation,
child: FadeTransition(
opacity: _fadeAnimation,
child: Transform.translate(
offset: Offset(_slideAnimation.value, 0),
child: MouseRegion(
onEnter: (_) => setState(() => _isHovered = true),
onExit: (_) => setState(() => _isHovered = false),
child: GestureDetector(
onTap: widget.onTap,
child: AnimatedContainer(
duration: AppAnimations.durationFast,
margin: const EdgeInsets.symmetric(
horizontal: 2,
vertical: 6,
),
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(8),
color: widget.route.completed
? Colors.green.withValues(alpha: 0.15)
: (_isHovered || widget.isSelected
? Theme.of(context).colorScheme.surfaceContainer
: Colors.transparent),
boxShadow: (_isHovered || widget.isSelected) && !widget.route.completed
? [
BoxShadow(
color: Colors.black.withValues(
alpha: isDark ? 0.3 : 0.08,
),
blurRadius: 8,
offset: const Offset(0, 4),
),
]
: [],
),
padding: const EdgeInsets.symmetric(horizontal: 4, vertical: 12),
child: Column(
children: [
// Main route info row
Row(
crossAxisAlignment: CrossAxisAlignment.center,
children: [
// Route number badge (left of status bar)
Container(
width: 45,
height: 45,
decoration: BoxDecoration(
color: statusColor,
borderRadius: BorderRadius.circular(8),
),
child: Center(
child: Text(
'${(widget.animationIndex ?? 0) + 1}',
style: const TextStyle(
color: Colors.white,
fontSize: 20,
fontWeight: FontWeight.w700,
),
),
),
),
const SizedBox(width: 8),
// Left accent bar (vertical status bar)
Container(
width: 4,
height: 50,
decoration: BoxDecoration(
color: statusColor,
borderRadius: BorderRadius.circular(2),
),
),
const SizedBox(width: 10),
// Route info
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// Route Name
Text(
widget.route.name,
style: Theme.of(context)
.textTheme
.titleMedium
?.copyWith(
fontWeight: FontWeight.w600,
fontSize: 16,
),
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
const SizedBox(height: 4),
// Route details
Text(
l10n.routeDeliveries(widget.route.deliveredCount, widget.route.deliveriesCount),
style: Theme.of(context)
.textTheme
.bodyMedium
?.copyWith(
fontSize: 13,
),
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
],
),
),
],
),
],
),
),
),
),
),
),
);
}
}

View File

@ -0,0 +1,36 @@
class AppConstants {
// App Info
static const String appName = 'Fleet Driver';
static const String appVersion = '1.0.0';
// Google Maps
static const String googleMapsApiKey = 'YOUR_GOOGLE_MAPS_API_KEY_HERE';
// Navigation URLs
static const String googleMapsUrlScheme = 'comgooglemaps://';
static const String appleMapsUrlScheme = 'maps://';
// API Endpoints (Replace with your actual backend URL)
static const String baseApiUrl = 'https://your-api-url.com/api';
static const String routesEndpoint = '/routes';
static const String stopsEndpoint = '/stops';
static const String updateStatusEndpoint = '/update-status';
// Timeouts
static const int apiTimeout = 30; // seconds
static const int locationUpdateInterval = 5; // seconds
// Permissions
static const String locationPermissionMessage =
'This app needs location access to show your current position and navigate to destinations.';
// Local Storage Keys
static const String driverIdKey = 'driver_id';
static const String driverNameKey = 'driver_name';
static const String authTokenKey = 'auth_token';
// Map Settings
static const double defaultZoom = 15.0;
static const double defaultTilt = 0.0;
static const double defaultBearing = 0.0;
}

View File

@ -0,0 +1,150 @@
import 'package:flutter/material.dart';
class AppTheme {
// Colors
static const Color primaryColor = Color(0xFF2196F3);
static const Color secondaryColor = Color(0xFF03DAC6);
static const Color backgroundColor = Color(0xFFF5F5F5);
static const Color surfaceColor = Colors.white;
static const Color errorColor = Color(0xFFB00020);
static const Color successColor = Color(0xFF4CAF50);
static const Color warningColor = Color(0xFFFFC107);
// Status Colors
static const Color pendingColor = Color(0xFFFF9800);
static const Color inProgressColor = Color(0xFF2196F3);
static const Color completedColor = Color(0xFF4CAF50);
static const Color failedColor = Color(0xFFB00020);
// Text Colors
static const Color textPrimaryColor = Color(0xFF212121);
static const Color textSecondaryColor = Color(0xFF757575);
static const Color textHintColor = Color(0xFFBDBDBD);
static ThemeData get lightTheme {
return ThemeData(
primaryColor: primaryColor,
scaffoldBackgroundColor: backgroundColor,
colorScheme: const ColorScheme.light(
primary: primaryColor,
secondary: secondaryColor,
surface: surfaceColor,
error: errorColor,
),
appBarTheme: const AppBarTheme(
backgroundColor: primaryColor,
foregroundColor: Colors.white,
elevation: 2,
centerTitle: true,
titleTextStyle: TextStyle(
fontSize: 20,
fontWeight: FontWeight.bold,
color: Colors.white,
),
),
cardTheme: CardTheme(
elevation: 2,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12),
),
margin: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
),
elevatedButtonTheme: ElevatedButtonThemeData(
style: ElevatedButton.styleFrom(
backgroundColor: primaryColor,
foregroundColor: Colors.white,
padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 12),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(8),
),
textStyle: const TextStyle(
fontSize: 16,
fontWeight: FontWeight.bold,
),
),
),
floatingActionButtonTheme: const FloatingActionButtonThemeData(
backgroundColor: primaryColor,
foregroundColor: Colors.white,
),
inputDecorationTheme: InputDecorationTheme(
filled: true,
fillColor: Colors.white,
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(8),
borderSide: const BorderSide(color: Colors.grey),
),
enabledBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(8),
borderSide: const BorderSide(color: Colors.grey),
),
focusedBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(8),
borderSide: const BorderSide(color: primaryColor, width: 2),
),
contentPadding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12),
),
textTheme: const TextTheme(
displayLarge: TextStyle(
fontSize: 32,
fontWeight: FontWeight.bold,
color: textPrimaryColor,
),
displayMedium: TextStyle(
fontSize: 28,
fontWeight: FontWeight.bold,
color: textPrimaryColor,
),
displaySmall: TextStyle(
fontSize: 24,
fontWeight: FontWeight.bold,
color: textPrimaryColor,
),
headlineMedium: TextStyle(
fontSize: 20,
fontWeight: FontWeight.w600,
color: textPrimaryColor,
),
titleLarge: TextStyle(
fontSize: 18,
fontWeight: FontWeight.w600,
color: textPrimaryColor,
),
titleMedium: TextStyle(
fontSize: 16,
fontWeight: FontWeight.w500,
color: textPrimaryColor,
),
bodyLarge: TextStyle(
fontSize: 16,
color: textPrimaryColor,
),
bodyMedium: TextStyle(
fontSize: 14,
color: textSecondaryColor,
),
bodySmall: TextStyle(
fontSize: 12,
color: textSecondaryColor,
),
),
);
}
static Color getStatusColor(String status) {
switch (status.toLowerCase()) {
case 'pending':
case 'notstartedCamel':
return pendingColor;
case 'inprogress':
return inProgressColor;
case 'completed':
return completedColor;
case 'failed':
case 'cancelled':
return failedColor;
default:
return textSecondaryColor;
}
}
}

View File

@ -0,0 +1,55 @@
import 'package:flutter/material.dart';
import '../theme/app_theme.dart';
class StatusBadge extends StatelessWidget {
final String status;
final double? fontSize;
const StatusBadge({
super.key,
required this.status,
this.fontSize,
});
@override
Widget build(BuildContext context) {
return Container(
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 6),
decoration: BoxDecoration(
color: AppTheme.getStatusColor(status).withOpacity(0.2),
borderRadius: BorderRadius.circular(16),
border: Border.all(
color: AppTheme.getStatusColor(status),
width: 1.5,
),
),
child: Text(
_formatStatus(status),
style: TextStyle(
color: AppTheme.getStatusColor(status),
fontWeight: FontWeight.bold,
fontSize: fontSize ?? 12,
),
),
);
}
String _formatStatus(String status) {
switch (status.toLowerCase()) {
case 'pending':
return 'Pending';
case 'notstarted':
return 'Not Started';
case 'inprogress':
return 'In Progress';
case 'completed':
return 'Completed';
case 'failed':
return 'Failed';
case 'cancelled':
return 'Cancelled';
default:
return status;
}
}
}

View File

@ -0,0 +1,39 @@
class LocationModel {
final double latitude;
final double longitude;
final String? address;
LocationModel({
required this.latitude,
required this.longitude,
this.address,
});
factory LocationModel.fromJson(Map<String, dynamic> json) {
return LocationModel(
latitude: json['latitude'] as double,
longitude: json['longitude'] as double,
address: json['address'] as String?,
);
}
Map<String, dynamic> toJson() {
return {
'latitude': latitude,
'longitude': longitude,
'address': address,
};
}
LocationModel copyWith({
double? latitude,
double? longitude,
String? address,
}) {
return LocationModel(
latitude: latitude ?? this.latitude,
longitude: longitude ?? this.longitude,
address: address ?? this.address,
);
}
}

View File

@ -0,0 +1,118 @@
import 'stop_model.dart';
enum RouteStatus { notStarted, inProgress, completed, cancelled }
class RouteModel {
final String id;
final String driverId;
final String driverName;
final DateTime date;
final RouteStatus status;
final List<StopModel> stops;
final double totalDistance; // in kilometers
final int estimatedDuration; // in minutes
final DateTime? startTime;
final DateTime? endTime;
final String? vehicleId;
final String? notes;
RouteModel({
required this.id,
required this.driverId,
required this.driverName,
required this.date,
required this.status,
required this.stops,
required this.totalDistance,
required this.estimatedDuration,
this.startTime,
this.endTime,
this.vehicleId,
this.notes,
});
factory RouteModel.fromJson(Map<String, dynamic> json) {
return RouteModel(
id: json['id'] as String,
driverId: json['driverId'] as String,
driverName: json['driverName'] as String,
date: DateTime.parse(json['date'] as String),
status: RouteStatus.values.firstWhere(
(e) => e.toString() == 'RouteStatus.${json['status']}',
),
stops: (json['stops'] as List<dynamic>)
.map((stop) => StopModel.fromJson(stop as Map<String, dynamic>))
.toList(),
totalDistance: (json['totalDistance'] as num).toDouble(),
estimatedDuration: json['estimatedDuration'] as int,
startTime: json['startTime'] != null
? DateTime.parse(json['startTime'] as String)
: null,
endTime: json['endTime'] != null
? DateTime.parse(json['endTime'] as String)
: null,
vehicleId: json['vehicleId'] as String?,
notes: json['notes'] as String?,
);
}
Map<String, dynamic> toJson() {
return {
'id': id,
'driverId': driverId,
'driverName': driverName,
'date': date.toIso8601String(),
'status': status.toString().split('.').last,
'stops': stops.map((stop) => stop.toJson()).toList(),
'totalDistance': totalDistance,
'estimatedDuration': estimatedDuration,
'startTime': startTime?.toIso8601String(),
'endTime': endTime?.toIso8601String(),
'vehicleId': vehicleId,
'notes': notes,
};
}
RouteModel copyWith({
String? id,
String? driverId,
String? driverName,
DateTime? date,
RouteStatus? status,
List<StopModel>? stops,
double? totalDistance,
int? estimatedDuration,
DateTime? startTime,
DateTime? endTime,
String? vehicleId,
String? notes,
}) {
return RouteModel(
id: id ?? this.id,
driverId: driverId ?? this.driverId,
driverName: driverName ?? this.driverName,
date: date ?? this.date,
status: status ?? this.status,
stops: stops ?? this.stops,
totalDistance: totalDistance ?? this.totalDistance,
estimatedDuration: estimatedDuration ?? this.estimatedDuration,
startTime: startTime ?? this.startTime,
endTime: endTime ?? this.endTime,
vehicleId: vehicleId ?? this.vehicleId,
notes: notes ?? this.notes,
);
}
int get completedStopsCount =>
stops.where((stop) => stop.status == StopStatus.completed).length;
int get totalStopsCount => stops.length;
double get progressPercentage =>
totalStopsCount > 0 ? (completedStopsCount / totalStopsCount) * 100 : 0;
StopModel? get nextStop => stops.firstWhere(
(stop) => stop.status == StopStatus.pending,
orElse: () => stops.first,
);
}

View File

@ -0,0 +1,117 @@
import 'location_model.dart';
enum StopType { pickup, dropoff }
enum StopStatus { pending, inProgress, completed, failed }
class StopModel {
final String id;
final String customerId;
final String customerName;
final String? customerPhone;
final LocationModel location;
final StopType type;
final StopStatus status;
final DateTime scheduledTime;
final DateTime? completedTime;
final String? notes;
final List<String> items;
final int orderNumber;
final String? signature;
final String? photo;
StopModel({
required this.id,
required this.customerId,
required this.customerName,
this.customerPhone,
required this.location,
required this.type,
required this.status,
required this.scheduledTime,
this.completedTime,
this.notes,
required this.items,
required this.orderNumber,
this.signature,
this.photo,
});
factory StopModel.fromJson(Map<String, dynamic> json) {
return StopModel(
id: json['id'] as String,
customerId: json['customerId'] as String,
customerName: json['customerName'] as String,
customerPhone: json['customerPhone'] as String?,
location: LocationModel.fromJson(json['location'] as Map<String, dynamic>),
type: StopType.values.firstWhere(
(e) => e.toString() == 'StopType.${json['type']}',
),
status: StopStatus.values.firstWhere(
(e) => e.toString() == 'StopStatus.${json['status']}',
),
scheduledTime: DateTime.parse(json['scheduledTime'] as String),
completedTime: json['completedTime'] != null
? DateTime.parse(json['completedTime'] as String)
: null,
notes: json['notes'] as String?,
items: (json['items'] as List<dynamic>).cast<String>(),
orderNumber: json['orderNumber'] as int,
signature: json['signature'] as String?,
photo: json['photo'] as String?,
);
}
Map<String, dynamic> toJson() {
return {
'id': id,
'customerId': customerId,
'customerName': customerName,
'customerPhone': customerPhone,
'location': location.toJson(),
'type': type.toString().split('.').last,
'status': status.toString().split('.').last,
'scheduledTime': scheduledTime.toIso8601String(),
'completedTime': completedTime?.toIso8601String(),
'notes': notes,
'items': items,
'orderNumber': orderNumber,
'signature': signature,
'photo': photo,
};
}
StopModel copyWith({
String? id,
String? customerId,
String? customerName,
String? customerPhone,
LocationModel? location,
StopType? type,
StopStatus? status,
DateTime? scheduledTime,
DateTime? completedTime,
String? notes,
List<String>? items,
int? orderNumber,
String? signature,
String? photo,
}) {
return StopModel(
id: id ?? this.id,
customerId: customerId ?? this.customerId,
customerName: customerName ?? this.customerName,
customerPhone: customerPhone ?? this.customerPhone,
location: location ?? this.location,
type: type ?? this.type,
status: status ?? this.status,
scheduledTime: scheduledTime ?? this.scheduledTime,
completedTime: completedTime ?? this.completedTime,
notes: notes ?? this.notes,
items: items ?? this.items,
orderNumber: orderNumber ?? this.orderNumber,
signature: signature ?? this.signature,
photo: photo ?? this.photo,
);
}
}

View File

@ -0,0 +1,227 @@
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import '../../../../core/theme/app_theme.dart';
import '../../data/models/route_model.dart';
import '../providers/route_provider.dart';
import '../widgets/route_card.dart';
import 'route_details_page.dart';
class HomePage extends StatefulWidget {
const HomePage({super.key});
@override
State<HomePage> createState() => _HomePageState();
}
class _HomePageState extends State<HomePage> {
@override
void initState() {
super.initState();
// Load routes for demo driver
// In production, use actual driver ID from authentication
Future.microtask(() {
context.read<RouteProvider>().loadRoutes('driver_1');
});
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('My Routes'),
actions: [
IconButton(
icon: const Icon(Icons.refresh),
onPressed: () {
context.read<RouteProvider>().loadRoutes('driver_1');
},
),
IconButton(
icon: const Icon(Icons.person),
onPressed: () {
// Navigate to profile page
},
),
],
),
body: Consumer<RouteProvider>(
builder: (context, routeProvider, child) {
if (routeProvider.isLoading) {
return const Center(
child: CircularProgressIndicator(),
);
}
if (routeProvider.error != null) {
return Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
const Icon(
Icons.error_outline,
size: 64,
color: AppTheme.errorColor,
),
const SizedBox(height: 16),
Text(
'Error loading routes',
style: Theme.of(context).textTheme.titleLarge,
),
const SizedBox(height: 8),
Padding(
padding: const EdgeInsets.symmetric(horizontal: 32),
child: Text(
routeProvider.error!,
textAlign: TextAlign.center,
style: Theme.of(context).textTheme.bodyMedium,
),
),
const SizedBox(height: 24),
ElevatedButton.icon(
onPressed: () {
routeProvider.loadRoutes('driver_1');
},
icon: const Icon(Icons.refresh),
label: const Text('Retry'),
),
],
),
);
}
if (routeProvider.routes.isEmpty) {
return Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
const Icon(
Icons.route,
size: 64,
color: Colors.grey,
),
const SizedBox(height: 16),
Text(
'No routes available',
style: Theme.of(context).textTheme.titleLarge,
),
const SizedBox(height: 8),
Text(
'Check back later for new routes',
style: Theme.of(context).textTheme.bodyMedium,
),
],
),
);
}
return RefreshIndicator(
onRefresh: () async {
await routeProvider.loadRoutes('driver_1');
},
child: ListView(
padding: const EdgeInsets.symmetric(vertical: 8),
children: [
_buildSummaryCard(context, routeProvider.routes),
const SizedBox(height: 8),
...routeProvider.routes.map((route) {
return RouteCard(
route: route,
onTap: () {
routeProvider.setCurrentRoute(route);
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => const RouteDetailsPage(),
),
);
},
);
}),
],
),
);
},
),
);
}
Widget _buildSummaryCard(BuildContext context, List<RouteModel> routes) {
final todayRoutes = routes.where((route) {
return route.date.day == DateTime.now().day &&
route.date.month == DateTime.now().month &&
route.date.year == DateTime.now().year;
}).toList();
final completedRoutes =
todayRoutes.where((r) => r.status == RouteStatus.completed).length;
return Card(
margin: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
child: Padding(
padding: const EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'Today\'s Summary',
style: Theme.of(context).textTheme.titleLarge,
),
const SizedBox(height: 16),
Row(
mainAxisAlignment: MainAxisAlignment.spaceAround,
children: [
_buildSummaryItem(
context,
'Total Routes',
todayRoutes.length.toString(),
Icons.route,
AppTheme.primaryColor,
),
_buildSummaryItem(
context,
'Completed',
completedRoutes.toString(),
Icons.check_circle,
AppTheme.completedColor,
),
_buildSummaryItem(
context,
'Pending',
(todayRoutes.length - completedRoutes).toString(),
Icons.pending,
AppTheme.warningColor,
),
],
),
],
),
),
);
}
Widget _buildSummaryItem(
BuildContext context,
String label,
String value,
IconData icon,
Color color,
) {
return Column(
children: [
Icon(icon, color: color, size: 32),
const SizedBox(height: 8),
Text(
value,
style: Theme.of(context).textTheme.displaySmall?.copyWith(
color: color,
fontWeight: FontWeight.bold,
),
),
Text(
label,
style: Theme.of(context).textTheme.bodySmall,
),
],
);
}
}

Some files were not shown because too many files have changed in this diff Show More