diff --git a/ios/Podfile b/ios/Podfile
index fad4db7..8aff80e 100644
--- a/ios/Podfile
+++ b/ios/Podfile
@@ -39,5 +39,21 @@ 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
diff --git a/ios/Runner/Info.plist b/ios/Runner/Info.plist
index d012aa6..5ab417b 100644
--- a/ios/Runner/Info.plist
+++ b/ios/Runner/Info.plist
@@ -56,8 +56,13 @@
UIBackgroundModes
location
+ fetch
NSLocationAlwaysUsageDescription
This app needs continuous access to your location for navigation and delivery tracking.
+ NSCameraUsageDescription
+ This app needs camera access to take photos of deliveries.
+ NSPhotoLibraryUsageDescription
+ This app needs access to your photos to select delivery images.
diff --git a/lib/components/dark_mode_map.dart b/lib/components/dark_mode_map.dart
index b2798c6..8027ef2 100644
--- a/lib/components/dark_mode_map.dart
+++ b/lib/components/dark_mode_map.dart
@@ -26,6 +26,11 @@ class _DarkModeMapComponentState extends State {
GoogleNavigationViewController? _navigationController;
bool _isNavigating = false;
LatLng? _destinationLocation;
+ LatLng? _driverLocation;
+ bool _isSessionInitialized = false;
+ bool _isInitializing = false;
+ bool _isStartingNavigation = false;
+ String _loadingMessage = 'Initializing...';
@override
void initState() {
@@ -34,6 +39,12 @@ class _DarkModeMapComponentState extends State {
}
Future _initializeNavigation() async {
+ if (_isInitializing || _isSessionInitialized) return;
+
+ setState(() {
+ _isInitializing = true;
+ });
+
try {
final termsAccepted = await GoogleMapsNavigator.areTermsAccepted();
if (!termsAccepted) {
@@ -43,11 +54,44 @@ class _DarkModeMapComponentState extends State {
);
}
await GoogleMapsNavigator.initializeNavigationSession();
+
+ if (mounted) {
+ setState(() {
+ _isSessionInitialized = true;
+ _isInitializing = false;
+ });
+ }
} catch (e) {
- debugPrint('Map initialization error: $e');
+ final errorMessage = _formatErrorMessage(e);
+ debugPrint('Map initialization error: $errorMessage');
+
+ if (mounted) {
+ setState(() {
+ _isInitializing = false;
+ });
+
+ ScaffoldMessenger.of(context).showSnackBar(
+ SnackBar(
+ content: Text('Navigation initialization failed: $errorMessage'),
+ duration: const Duration(seconds: 5),
+ ),
+ );
+ }
}
}
+ 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;
+ }
+
@override
void didUpdateWidget(DarkModeMapComponent oldWidget) {
super.didUpdateWidget(oldWidget);
@@ -66,34 +110,30 @@ class _DarkModeMapComponentState extends State {
longitude: address.longitude!,
);
});
- _navigateToLocation(_destinationLocation!);
+ // Just store the destination, don't move camera
+ // The navigation will handle camera positioning
}
}
}
- Future _navigateToLocation(LatLng location) async {
- if (_navigationController == null) return;
- try {
- await _navigationController!.animateCamera(
- CameraUpdate.newLatLngZoom(location, 15),
- );
- } catch (e) {
- debugPrint('Camera navigation error: $e');
- }
- }
-
Future _applyDarkModeStyle() async {
- if (_navigationController == null) return;
+ // Check if widget is still mounted and controller exists
+ if (!mounted || _navigationController == null) return;
+
try {
// Apply dark mode style configuration for Google Maps
// This reduces eye strain in low-light environments
+ if (!mounted) return;
+
final isDarkMode = Theme.of(context).brightness == Brightness.dark;
if (isDarkMode) {
// Dark map style with warm accent colors
await _navigationController!.setMapStyle(_getDarkMapStyle());
}
} catch (e) {
- debugPrint('Error applying map style: $e');
+ if (mounted) {
+ debugPrint('Error applying map style: $e');
+ }
}
}
@@ -219,7 +259,50 @@ class _DarkModeMapComponentState extends State {
Future _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;
+ });
+ ScaffoldMessenger.of(context).showSnackBar(
+ const SnackBar(
+ content: Text('Navigation initialization timeout'),
+ duration: Duration(seconds: 3),
+ ),
+ );
+ }
+ return;
+ }
+
+ if (mounted) {
+ setState(() {
+ _loadingMessage = 'Setting destination...';
+ });
+ }
+
final waypoint = NavigationWaypoint.withLatLngTarget(
title: widget.selectedDelivery?.name ?? 'Destination',
target: _destinationLocation!,
@@ -230,17 +313,50 @@ class _DarkModeMapComponentState extends State {
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();
- setState(() {
- _isNavigating = true;
- });
- } catch (e) {
- debugPrint('Navigation start error: $e');
+ debugPrint('Navigation started successfully');
+
+ // 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;
+ });
+
ScaffoldMessenger.of(context).showSnackBar(
- SnackBar(content: Text('Navigation error: $e')),
+ SnackBar(
+ content: Text('Navigation error: $errorMessage'),
+ duration: const Duration(seconds: 4),
+ ),
);
}
}
@@ -250,11 +366,15 @@ class _DarkModeMapComponentState extends State {
try {
await GoogleMapsNavigator.stopGuidance();
await GoogleMapsNavigator.clearDestinations();
- setState(() {
- _isNavigating = false;
- });
+ if (mounted) {
+ setState(() {
+ _isNavigating = false;
+ });
+ }
} catch (e) {
- debugPrint('Navigation stop error: $e');
+ if (mounted) {
+ debugPrint('Navigation stop error: $e');
+ }
}
}
@@ -289,11 +409,12 @@ class _DarkModeMapComponentState extends State {
}
Future _recenterMap() async {
- if (_navigationController == null || _destinationLocation == null) return;
+ if (_navigationController == null) return;
try {
- await _navigationController!.animateCamera(
- CameraUpdate.newLatLngZoom(_destinationLocation!, 15),
- );
+ // 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');
}
@@ -301,28 +422,39 @@ class _DarkModeMapComponentState extends State {
@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);
+ // Driver's current location (defaults to Montreal if not available)
+ final initialPosition = const LatLng(latitude: 45.5017, longitude: -73.5673);
+
+ // Store driver location for navigation centering
+ _driverLocation = initialPosition;
+
+ // Calculate dynamic padding for top info panel and bottom button bar
+ // Increased to accommodate navigation widget info and action buttons
+ final topPadding = widget.selectedDelivery != null ? 110.0 : 0.0;
+ final bottomPadding = widget.selectedDelivery != null ? 110.0 : 0.0;
return Stack(
children: [
- GoogleMapsNavigationView(
- onViewCreated: (controller) {
- _navigationController = controller;
- _applyDarkModeStyle();
- controller.animateCamera(
- CameraUpdate.newLatLngZoom(initialPosition, 12),
- );
- },
- initialCameraPosition: CameraPosition(
- target: initialPosition,
- zoom: 12,
+ // Map with padding to accommodate overlaid elements
+ Padding(
+ padding: EdgeInsets.only(
+ top: topPadding,
+ bottom: bottomPadding,
+ ),
+ child: GoogleMapsNavigationView(
+ onViewCreated: (controller) async {
+ _navigationController = controller;
+ // Apply dark mode style with a small delay to ensure map is ready
+ await Future.delayed(const Duration(milliseconds: 500));
+ await _applyDarkModeStyle();
+ controller.animateCamera(
+ CameraUpdate.newLatLngZoom(initialPosition, 12),
+ );
+ },
+ initialCameraPosition: CameraPosition(
+ target: initialPosition,
+ zoom: 12,
+ ),
),
),
// Custom dark-themed controls overlay
@@ -346,9 +478,9 @@ class _DarkModeMapComponentState extends State {
],
),
),
- // Navigation and action buttons
+ // Navigation and action buttons (positioned above bottom button bar)
Positioned(
- bottom: 16,
+ bottom: 120,
right: 16,
child: Column(
mainAxisSize: MainAxisSize.min,
@@ -356,9 +488,9 @@ class _DarkModeMapComponentState extends State {
// Start navigation button
if (widget.selectedDelivery != null && !_isNavigating)
_buildActionButton(
- label: 'Navigate',
+ label: _isStartingNavigation || _isInitializing ? 'Loading...' : 'Navigate',
icon: Icons.directions,
- onPressed: _startNavigation,
+ onPressed: _isStartingNavigation || _isInitializing ? null : _startNavigation,
color: SvrntyColors.crimsonRed,
),
if (_isNavigating)
@@ -497,9 +629,9 @@ class _DarkModeMapComponentState extends State {
if (widget.selectedDelivery!.delivered)
Expanded(
child: _buildBottomActionButton(
- label: 'Start Navigation',
+ label: _isInitializing ? 'Initializing...' : 'Start Navigation',
icon: Icons.directions,
- onPressed: _startNavigation,
+ onPressed: _isInitializing ? null : _startNavigation,
isPrimary: true,
),
),
@@ -508,9 +640,9 @@ class _DarkModeMapComponentState extends State {
if (!_isNavigating && !widget.selectedDelivery!.delivered)
Expanded(
child: _buildBottomActionButton(
- label: 'Navigate',
+ label: _isStartingNavigation || _isInitializing ? 'Loading...' : 'Navigate',
icon: Icons.directions,
- onPressed: _startNavigation,
+ onPressed: _isStartingNavigation || _isInitializing ? null : _startNavigation,
),
),
if (_isNavigating)
@@ -528,6 +660,63 @@ class _DarkModeMapComponentState extends State {
),
),
),
+ // Loading overlay during navigation initialization and start
+ if (_isStartingNavigation || _isInitializing)
+ Positioned.fill(
+ child: Container(
+ color: Colors.black.withOpacity(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.withOpacity(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(
+ 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.withOpacity(0.7),
+ ),
+ textAlign: TextAlign.center,
+ ),
+ ],
+ ),
+ ),
+ ),
+ ),
+ ),
],
);
}
@@ -569,7 +758,7 @@ class _DarkModeMapComponentState extends State {
Widget _buildBottomActionButton({
required String label,
required IconData icon,
- required VoidCallback onPressed,
+ required VoidCallback? onPressed,
bool isPrimary = false,
bool isDanger = false,
}) {
@@ -585,6 +774,11 @@ class _DarkModeMapComponentState extends State {
textColor = Theme.of(context).colorScheme.onSurface;
}
+ // Reduce opacity when disabled
+ if (onPressed == null) {
+ backgroundColor = backgroundColor.withOpacity(0.5);
+ }
+
return Material(
color: backgroundColor,
borderRadius: BorderRadius.circular(8),
@@ -624,9 +818,12 @@ class _DarkModeMapComponentState extends State {
Widget _buildActionButton({
required String label,
required IconData icon,
- required VoidCallback onPressed,
+ required VoidCallback? onPressed,
required Color color,
}) {
+ final isDisabled = onPressed == null;
+ final buttonColor = isDisabled ? color.withOpacity(0.5) : color;
+
return Container(
margin: const EdgeInsets.only(bottom: 8),
decoration: BoxDecoration(
@@ -640,7 +837,7 @@ class _DarkModeMapComponentState extends State {
],
),
child: Material(
- color: color,
+ color: buttonColor,
borderRadius: BorderRadius.circular(8),
child: InkWell(
onTap: onPressed,
diff --git a/lib/pages/deliveries_page.dart b/lib/pages/deliveries_page.dart
index bc6c019..0836a23 100644
--- a/lib/pages/deliveries_page.dart
+++ b/lib/pages/deliveries_page.dart
@@ -70,7 +70,8 @@ class _DeliveriesPageState extends ConsumerState {
title: Text(widget.routeName),
elevation: 0,
),
- body: deliveriesData.when(
+ body: SafeArea(
+ child: deliveriesData.when(
data: (deliveries) {
// Auto-scroll to first pending delivery when page loads or route changes
if (_lastRouteFragmentId != widget.routeFragmentId) {
@@ -167,6 +168,7 @@ class _DeliveriesPageState extends ConsumerState {
child: Text('Error: $error'),
),
),
+ ),
);
}
@@ -286,6 +288,7 @@ class UnifiedDeliveryListView extends StatelessWidget {
child: ListView.builder(
controller: scrollController,
padding: const EdgeInsets.symmetric(vertical: 8),
+ physics: const AlwaysScrollableScrollPhysics(),
itemCount: deliveries.length,
itemBuilder: (context, index) {
final delivery = deliveries[index];