diff --git a/app/api/accountApi.ts b/app/api/accountApi.ts index e343b2a..baf2973 100644 --- a/app/api/accountApi.ts +++ b/app/api/accountApi.ts @@ -4,7 +4,7 @@ import { DeliveryPartnerProfileResponse } from '@interfaces'; export const getAccountInfo = async (): Promise => { const response = await apiClient.get( - '/account-info', + '/delivery-partners/profile', ); return response; }; diff --git a/app/components/mapView/mapView.tsx b/app/components/mapView/mapView.tsx index ae62070..d17d6d3 100644 --- a/app/components/mapView/mapView.tsx +++ b/app/components/mapView/mapView.tsx @@ -1,9 +1,13 @@ -import React from 'react'; +import React, { useState, useEffect, useRef } from 'react'; import { View, Text } from 'react-native'; import MapView, { Marker, Polyline, PROVIDER_DEFAULT } from 'react-native-maps'; import { useAppTheme } from '@theme'; import { Coordinates } from '@app-types/index'; import { getStyles } from './mapView.styles'; +import { + getRouteDirections, + getHaversineDistance, +} from '@services/locationService'; interface MapViewComponentProps { origin?: Coordinates; @@ -22,6 +26,9 @@ export const MapViewComponent: React.FC = ({ }) => { const { colors } = useAppTheme(); const styles = getStyles(colors); + const mapRef = useRef(null); + const [routeCoords, setRouteCoords] = useState([]); + const lastFetchedOrigin = useRef(null); const defaultRegion = { latitude: currentLocation?.latitude || origin?.latitude || 12.9716, @@ -30,9 +37,83 @@ export const MapViewComponent: React.FC = ({ longitudeDelta: 0.05, }; + // Fetch routing coordinates + useEffect(() => { + let active = true; + if (showRoute && origin && destination) { + // Calculate distance from last fetched origin + if (lastFetchedOrigin.current) { + const dist = getHaversineDistance(origin, lastFetchedOrigin.current); + // If driver moved less than 50 meters (0.05 km), skip re-fetching to prevent spamming + if (dist < 0.05 && routeCoords.length > 0) { + return; + } + } + + getRouteDirections(origin, destination) + .then(points => { + if (active) { + setRouteCoords(points); + lastFetchedOrigin.current = origin; + } + }) + .catch(err => { + console.error('Error in MapViewComponent getRouteDirections:', err); + if (active) { + setRouteCoords([origin, destination]); + } + }); + } else { + setRouteCoords([]); + lastFetchedOrigin.current = null; + } + return () => { + active = false; + }; + }, [ + showRoute, + origin?.latitude, + origin?.longitude, + destination?.latitude, + destination?.longitude, + routeCoords.length, + ]); + + // Adjust camera to fit all coordinates + useEffect(() => { + const coordsToFit: Coordinates[] = []; + if (currentLocation) { + coordsToFit.push(currentLocation); + } + if (origin) { + coordsToFit.push(origin); + } + if (destination) { + coordsToFit.push(destination); + } + + if (coordsToFit.length > 0 && mapRef.current) { + const timer = setTimeout(() => { + mapRef.current?.fitToCoordinates(coordsToFit, { + edgePadding: { top: 60, right: 60, bottom: 60, left: 60 }, + animated: true, + }); + }, 500); + return () => clearTimeout(timer); + } + }, [ + currentLocation?.latitude, + currentLocation?.longitude, + origin?.latitude, + origin?.longitude, + destination?.latitude, + destination?.longitude, + ]); + return ( = ({ pinColor={colors.statusOnline} /> )} - {showRoute && origin && destination && ( + {showRoute && routeCoords.length > 0 && ( @@ -74,4 +155,6 @@ export const MapViewComponent: React.FC = ({ ); }; + export default MapViewComponent; + diff --git a/app/features/screens/dashboardScreen/dashboardScreen.tsx b/app/features/screens/dashboardScreen/dashboardScreen.tsx index 5e89411..1563fdd 100644 --- a/app/features/screens/dashboardScreen/dashboardScreen.tsx +++ b/app/features/screens/dashboardScreen/dashboardScreen.tsx @@ -25,6 +25,7 @@ import { showPermissionDeniedAlert, socketService, } from '@services'; +import { getAccountInfoThunk } from '../profileScreen'; export const DashboardScreen: React.FC = () => { const { colors } = useAppTheme(); @@ -38,14 +39,14 @@ export const DashboardScreen: React.FC = () => { const { availabiltyStatus, loading: toggleLoading } = useAppSelector( state => state.dashboard, ); - const { user } = useAppSelector(state => state.auth); + const { profile } = useAppSelector(state => state.accountInfo); const authIsOnline = useAppSelector(state => state.auth.isOnline); const isOnline = availabiltyStatus ? availabiltyStatus.availabilityStatus === 'ONLINE' : authIsOnline; const { todayEarnings, completedCount, onlineMinutes, weeklyData } = useAppSelector(state => state.earnings); - const { jobStatus, activeJob } = useAppSelector(state => state.job); + // const { jobStatus, activeJob } = useAppSelector(state => state.job); const locationIntervalRef = useRef | null>( null, @@ -70,6 +71,10 @@ export const DashboardScreen: React.FC = () => { } }, [dispatch]); + useEffect(() => { + dispatch(getAccountInfoThunk()); + }, [dispatch]); + useEffect(() => { if (isOnline) { // Fire one immediately on going online, then every 15s @@ -122,31 +127,31 @@ export const DashboardScreen: React.FC = () => { }); }; - const handleActiveJobBannerPress = () => { - // Navigate back to the screen corresponding to the current job step - switch (jobStatus) { - case JobStatus.NewRequest: - navigation.navigate(RouteNames.NewJobRequest); - break; - case JobStatus.Accepted: - navigation.navigate(RouteNames.OrderAccepted); - break; - case JobStatus.ArrivedAtStore: - navigation.navigate(RouteNames.ConfirmPickup); - break; - case JobStatus.PickedUp: - navigation.navigate(RouteNames.OrderPickedUp); - break; - case JobStatus.EnRoute: - navigation.navigate(RouteNames.LiveTracking); - break; - case JobStatus.ArrivedAtCustomer: - navigation.navigate(RouteNames.DeliverOrder); - break; - default: - Alert.alert('Status Error', 'Unknown active delivery state.'); - } - }; + // const handleActiveJobBannerPress = () => { + // // Navigate back to the screen corresponding to the current job step + // switch (jobStatus) { + // case JobStatus.NewRequest: + // navigation.navigate(RouteNames.NewJobRequest); + // break; + // case JobStatus.Accepted: + // navigation.navigate(RouteNames.OrderAccepted); + // break; + // case JobStatus.ArrivedAtStore: + // navigation.navigate(RouteNames.ConfirmPickup); + // break; + // case JobStatus.PickedUp: + // navigation.navigate(RouteNames.OrderPickedUp); + // break; + // case JobStatus.EnRoute: + // navigation.navigate(RouteNames.LiveTracking); + // break; + // case JobStatus.ArrivedAtCustomer: + // navigation.navigate(RouteNames.DeliverOrder); + // break; + // default: + // Alert.alert('Status Error', 'Unknown active delivery state.'); + // } + // }; const formatOnlineTime = (mins: number) => { const hours = Math.floor(mins / 60); @@ -169,7 +174,7 @@ export const DashboardScreen: React.FC = () => { Good Morning ☀️ - {user?.name} + {profile?.user?.name} @@ -371,7 +376,7 @@ export const DashboardScreen: React.FC = () => { {/* Active Job Floating Bottom Banner */} - {jobStatus !== JobStatus.Idle && + {/* {jobStatus !== JobStatus.Idle && jobStatus !== JobStatus.NewRequest && activeJob && ( { - )} + )} */} ); }; diff --git a/app/features/screens/dashboardScreen/thunk.ts b/app/features/screens/dashboardScreen/thunk.ts index 86d3112..85693e0 100644 --- a/app/features/screens/dashboardScreen/thunk.ts +++ b/app/features/screens/dashboardScreen/thunk.ts @@ -24,6 +24,7 @@ export const updateDeliveryPartnerLocation = createAsyncThunk( const response = await deliveryPartnerLocation(payload); return response; } catch (error: unknown) { + console.log(error); return rejectWithValue(error); } }, diff --git a/app/features/screens/jobsScreen/jobsScreen.tsx b/app/features/screens/jobsScreen/jobsScreen.tsx index d579447..48b5d12 100644 --- a/app/features/screens/jobsScreen/jobsScreen.tsx +++ b/app/features/screens/jobsScreen/jobsScreen.tsx @@ -10,6 +10,7 @@ import { useFocusEffect, useNavigation } from '@react-navigation/native'; import { NativeStackNavigationProp } from '@react-navigation/native-stack'; import { AppStackParamList } from '@navigation/navigationTypes'; import { RouteNames } from '@utils/constants'; +import { DeliveryStatus } from '@interfaces'; export const JobsScreen: React.FC = () => { const navigation = @@ -133,11 +134,23 @@ export const JobsScreen: React.FC = () => { activeDeliveries?.delivery?.order?.orderItems?.length } paymentType={activeDeliveries?.delivery?.order?.paymentMethod} - onPress={() => - navigation.navigate(RouteNames.OrderPickedUp, { - jobId: activeDeliveries?.delivery?.id, - }) - } + onPress={() => { + const deliveryId = activeDeliveries?.delivery?.id; + const status = activeDeliveries?.delivery?.status as DeliveryStatus | undefined; + + // Statuses that mean delivery has already started — skip OrderPickedUp + const deliveryStartedStatuses: DeliveryStatus[] = [ + 'OUT_FOR_DELIVERY', + 'IN_TRANSIT', + 'ARRIVED_AT_DROPOFF', + ]; + + if (status && deliveryStartedStatuses.includes(status)) { + navigation.navigate(RouteNames.LiveTracking, { jobId: deliveryId }); + } else { + navigation.navigate(RouteNames.OrderPickedUp, { jobId: deliveryId }); + } + }} /> ) : ( diff --git a/app/features/screens/liveTrackingScreen/liveTrackingScreen.tsx b/app/features/screens/liveTrackingScreen/liveTrackingScreen.tsx index 6d68ffe..8b42d73 100644 --- a/app/features/screens/liveTrackingScreen/liveTrackingScreen.tsx +++ b/app/features/screens/liveTrackingScreen/liveTrackingScreen.tsx @@ -1,7 +1,7 @@ -import React from 'react'; -import { View, Text, Alert } from 'react-native'; +import React, { useState, useEffect } from 'react'; +import { View, Text, Alert, BackHandler } from 'react-native'; import { useAppTheme } from '@theme'; -import { PrimaryButton } from '@components'; +import { PrimaryButton, MapViewComponent } from '@components'; import { useAppDispatch, useAppSelector } from '@store'; import { advanceJobStatus } from '@store/commonReducers/job'; import { RouteProp, useNavigation, useRoute } from '@react-navigation/native'; @@ -9,6 +9,15 @@ import { NativeStackNavigationProp } from '@react-navigation/native-stack'; import { AppStackParamList } from '@navigation/navigationTypes'; import { RouteNames, JobStatus } from '@utils/constants'; import { getStyles } from './liveTrackingScreen.styles'; +import { Coordinates } from '@app-types/index'; +import { + requestLocationPermission, + showPermissionDeniedAlert, + getCurrentPosition, + getHaversineDistance, +} from '@services/locationService'; +import { socketService } from '@services/socketService'; +import Geolocation from 'react-native-geolocation-service'; export const LiveTrackingScreen: React.FC = () => { const { colors } = useAppTheme(); @@ -17,12 +26,102 @@ export const LiveTrackingScreen: React.FC = () => { const navigation = useNavigation>(); const route = - useRoute>(); + useRoute>(); const { jobId } = route.params; const { activeDeliveries } = useAppSelector(state => state.deliveries); const activeJob = activeDeliveries?.delivery; + const [currentLocation, setCurrentLocation] = useState( + null, + ); + const [distance, setDistance] = useState(null); + const [speed, setSpeed] = useState(24); // default speed in km/h + + // Block the hardware back button — once delivery starts the partner + // must not navigate back to the OrderPickedUp screen. + // useEffect(() => { + // const onBackPress = () => true; // return true = event consumed, back suppressed + // const subscription = BackHandler.addEventListener('hardwareBackPress', onBackPress); + // return () => subscription.remove(); + // }, []); + + useEffect(() => { + let watchId: number | null = null; + + const startTracking = async () => { + try { + const hasPermission = await requestLocationPermission(); + if (!hasPermission) { + showPermissionDeniedAlert(); + return; + } + + // Get initial current position + const initialPos = await getCurrentPosition(); + setCurrentLocation(initialPos); + + // Send initial location update to socket + socketService.updateLocation(initialPos); + + // Start watching position + watchId = Geolocation.watchPosition( + position => { + const coords: Coordinates = { + latitude: position.coords.latitude, + longitude: position.coords.longitude, + }; + setCurrentLocation(coords); + + // Emit location update via socket safely + try { + socketService.updateLocation(coords); + } catch (err) { + console.warn('Socket location update error:', err); + } + + // Update speed if available (speed in m/s from GPS) + if (position.coords.speed !== null && position.coords.speed > 0) { + const speedKmH = position.coords.speed * 3.6; + setSpeed(Math.max(speedKmH, 10)); // keep speed at least 10 km/h for a realistic ETA + } + }, + error => { + console.error('watchPosition error:', error); + }, + { + enableHighAccuracy: true, + distanceFilter: 10, // every 10 meters + interval: 5000, + fastestInterval: 2000, + }, + ); + } catch (error) { + console.error('Error starting location tracking:', error); + } + }; + + startTracking(); + + return () => { + if (watchId !== null) { + Geolocation.clearWatch(watchId); + } + }; + }, []); + + // Compute live distance to drop-off address + useEffect(() => { + if (currentLocation && activeJob?.order?.dropAddress) { + const dropCoords = { + latitude: activeJob.order.dropAddress.latitude, + longitude: activeJob.order.dropAddress.longitude, + }; + const km = getHaversineDistance(currentLocation, dropCoords); + setDistance(km); + } + }, [currentLocation, activeJob?.order?.dropAddress]); + const handleArrived = () => { dispatch(advanceJobStatus(JobStatus.ArrivedAtCustomer)); navigation.navigate(RouteNames.DeliverOrder, { @@ -39,22 +138,39 @@ export const LiveTrackingScreen: React.FC = () => { ); }; - // if (!activeJob) return null; + // Calculate ETA in minutes based on distance and speed (distance / speed * 60) + const etaMinutes = + distance !== null ? Math.max(Math.round((distance / speed) * 60), 1) : null; return ( {/* Top Floating ETA Card */} - Arriving in 8 mins + + {etaMinutes !== null + ? `Arriving in ${etaMinutes} mins` + : 'Calculating ETA…'} + - Distance: {activeJob?.estimatedDistanceKm} km • Speed: 24 km/h + Distance: {distance !== null ? `${distance.toFixed(2)} km` : '...'} • + Speed: {Math.round(speed)} km/h - {/* Map Background Placeholder */} - - [ Map Placeholder ] - + {/* Map Content */} + {activeJob?.order?.dropAddress ? ( + + ) : ( + + Loading Map... + + )} {/* Customer Location Bottom Card */} @@ -63,9 +179,9 @@ export const LiveTrackingScreen: React.FC = () => { {activeJob?.order?.customer?.user?.name} - {activeJob?.order?.dropAddress?.houseNumber + - ', ' + - activeJob?.order?.dropAddress?.addressLine1} + {activeJob?.order?.dropAddress + ? `${activeJob.order.dropAddress.houseNumber}, ${activeJob.order.dropAddress.addressLine1}` + : 'Loading address...'} diff --git a/app/features/screens/newJobRequestScreen/newJobRequestScreen.tsx b/app/features/screens/newJobRequestScreen/newJobRequestScreen.tsx index bac9405..92d85bc 100644 --- a/app/features/screens/newJobRequestScreen/newJobRequestScreen.tsx +++ b/app/features/screens/newJobRequestScreen/newJobRequestScreen.tsx @@ -25,9 +25,7 @@ export const NewJobRequestScreen: React.FC = () => { // Read from the socket-driven delivery slice const currentOffer = useAppSelector(state => state.delivery.currentOffer); - const respondLoading = useAppSelector( - state => state.delivery.respondLoading, - ); + const respondLoading = useAppSelector(state => state.delivery.respondLoading); const [secondsLeft, setSecondsLeft] = useState(TOTAL_SECONDS); @@ -83,7 +81,9 @@ export const NewJobRequestScreen: React.FC = () => { ) .unwrap() .then(() => { - navigation.navigate(RouteNames.OrderAccepted); + navigation.navigate(RouteNames.BottomTabs, { + screen: RouteNames.Jobs, + }); }) .catch(() => { // stay on screen — respondError will be set in redux @@ -109,8 +109,7 @@ export const NewJobRequestScreen: React.FC = () => { // Timer text color: warning when < 5s const timerColor = secondsLeft <= 5 ? colors.error : colors.primary; - const timerBorderColor = - secondsLeft <= 5 ? colors.error : colors.primary; + const timerBorderColor = secondsLeft <= 5 ? colors.error : colors.primary; return ( @@ -121,10 +120,7 @@ export const NewJobRequestScreen: React.FC = () => { {/* Circular countdown timer (pure RN — no SVG needed) */} {secondsLeft}s @@ -134,10 +130,7 @@ export const NewJobRequestScreen: React.FC = () => { {/* ── Earnings Badge ─────────────────────────────────────────────── */} Estimated Earnings @@ -164,9 +157,7 @@ export const NewJobRequestScreen: React.FC = () => { Distance - - {currentOffer.distanceKm} km - + {currentOffer.distanceKm} km diff --git a/app/features/screens/orderPickedUpScreen/orderPickedUpScreen.tsx b/app/features/screens/orderPickedUpScreen/orderPickedUpScreen.tsx index efac8ee..bd33dac 100644 --- a/app/features/screens/orderPickedUpScreen/orderPickedUpScreen.tsx +++ b/app/features/screens/orderPickedUpScreen/orderPickedUpScreen.tsx @@ -1,7 +1,7 @@ import React from 'react'; import { View, Text } from 'react-native'; import { useAppTheme } from '@theme'; -import { PrimaryButton } from '@components'; +import { PrimaryButton, MapViewComponent } from '@components'; import { changeOrderStatusThunk, useAppDispatch, useAppSelector } from '@store'; import { advanceJobStatus } from '@store/commonReducers/job'; import { RouteProp, useNavigation, useRoute } from '@react-navigation/native'; @@ -25,23 +25,40 @@ export const OrderPickedUpScreen: React.FC = () => { const { activeDeliveries } = useAppSelector(state => state.deliveries); const activeJob = activeDeliveries?.delivery; - const handleStartDelivery = () => { - dispatch( + const handleStartDelivery = async () => { + await dispatch( changeOrderStatusThunk({ deliveryId: jobId, action: JobStatus.PickedUp }), - ); - navigation.navigate(RouteNames.LiveTracking, { - jobId: jobId, - }); + ) + .unwrap() + .then(() => { + // Use replace so OrderPickedUp is removed from the stack — + // the user cannot come back to it via the back button. + navigation.replace(RouteNames.LiveTracking, { + jobId: jobId, + }); + }) + .catch(err => { + console.log(err, 'err'); + }); }; if (!activeJob) return null; return ( - {/* Map Background Placeholder */} - - [ Map Placeholder ] - + {/* Map Routing Path */} + {activeJob?.order?.pickupAddress && activeJob?.order?.dropAddress ? ( + + ) : ( + + Loading Map... + + )} {/* Deliver Bottom Card */} diff --git a/app/features/screens/profileScreen/profileScreen.tsx b/app/features/screens/profileScreen/profileScreen.tsx index ee90c5e..dc8b46b 100644 --- a/app/features/screens/profileScreen/profileScreen.tsx +++ b/app/features/screens/profileScreen/profileScreen.tsx @@ -2,7 +2,7 @@ import React from 'react'; import { View, Text, ScrollView, TouchableOpacity, Alert } from 'react-native'; import { useAppTheme } from '@theme'; import { PersonIcon, ChevronRightIcon, ClipboardIcon, StarIcon } from '@icons'; -import { useAppDispatch } from '@store'; +import { useAppDispatch, useAppSelector } from '@store'; import { logout } from '@store/commonReducers/auth'; import { useNavigation, CommonActions } from '@react-navigation/native'; import { NativeStackNavigationProp } from '@react-navigation/native-stack'; @@ -14,15 +14,42 @@ export const ProfileScreen: React.FC = () => { const { colors } = useAppTheme(); const styles = getStyles(colors); const dispatch = useAppDispatch(); - const navigation = useNavigation>(); + const navigation = + useNavigation>(); + const { profile } = useAppSelector(state => state.accountInfo); const menuItems = [ - { id: 'personal', title: 'Personal Info', icon: }, - { id: 'vehicle', title: 'Vehicle Info', icon: }, - { id: 'documents', title: 'Documents', icon: , route: RouteNames.Documents }, - { id: 'bank', title: 'Bank Details', icon: }, - { id: 'emergency', title: 'Emergency Contact', icon: }, - { id: 'settings', title: 'App Settings', icon: }, + { + id: 'personal', + title: 'Personal Info', + icon: , + }, + { + id: 'vehicle', + title: 'Vehicle Info', + icon: , + }, + { + id: 'documents', + title: 'Documents', + icon: , + route: RouteNames.Documents, + }, + { + id: 'bank', + title: 'Bank Details', + icon: , + }, + { + id: 'emergency', + title: 'Emergency Contact', + icon: , + }, + { + id: 'settings', + title: 'App Settings', + icon: , + }, ]; const handleMenuPress = (item: any) => { @@ -49,32 +76,35 @@ export const ProfileScreen: React.FC = () => { CommonActions.reset({ index: 0, routes: [{ name: 'Auth' }], - }) + }), ); }, }, - ] + ], ); }; return ( - + {/* Header User Card */} - - Rahul Kumar - + + {profile?.user?.name} + - 4.8 - (128 Trips) + {profile?.rating} + {profile?.user?.email} - - ID: DP126478 + + ID: {profile?.user?.id} {/* Profile Menu options */} @@ -82,7 +112,10 @@ export const ProfileScreen: React.FC = () => { {menuItems.map((item, idx) => ( handleMenuPress(item)} > @@ -96,7 +129,11 @@ export const ProfileScreen: React.FC = () => { {/* Logout Button */} - + Log Out diff --git a/app/navigation/appStack.tsx b/app/navigation/appStack.tsx index 263f891..3b81b17 100644 --- a/app/navigation/appStack.tsx +++ b/app/navigation/appStack.tsx @@ -20,19 +20,49 @@ const Stack = createNativeStackNavigator(); const AppStack: React.FC = () => { return ( - - - - - - - - - - - - - + + + + + + + + + + + + + ); }; diff --git a/app/services/apiClient.ts b/app/services/apiClient.ts index 19c9edc..da29139 100644 --- a/app/services/apiClient.ts +++ b/app/services/apiClient.ts @@ -15,7 +15,7 @@ const STORAGE_KEYS = { } as const; // ─── Config ────────────────────────────────────────────────────────────────── -const BASE_URL = 'https://157a-202-8-116-13.ngrok-free.app'; // TODO: replace with your actual base URL +const BASE_URL = 'https://53a9-202-8-116-13.ngrok-free.app'; // TODO: replace with your actual base URL // ─── Token Helpers ─────────────────────────────────────────────────────────── export const tokenManager = { diff --git a/app/services/locationService.ts b/app/services/locationService.ts index 033ce7e..af8ea7a 100644 --- a/app/services/locationService.ts +++ b/app/services/locationService.ts @@ -247,3 +247,134 @@ export const getCurrentLocationWithAddress = return { coords, address }; }; + +// --------------------------------------------------------------------------- +// Route & Distance helpers +// --------------------------------------------------------------------------- + +/** + * Decodes a Google Maps encoded polyline string. + * @param encoded - The encoded polyline string. + */ +export const decodePolyline = ( + encoded: string, +): { latitude: number; longitude: number }[] => { + const points: { latitude: number; longitude: number }[] = []; + let index = 0; + const len = encoded.length; + let lat = 0; + let lng = 0; + + while (index < len) { + let b; + let shift = 0; + let result = 0; + do { + b = encoded.charCodeAt(index++) - 63; + result |= (b & 0x1f) << shift; + shift += 5; + } while (b >= 0x20); + const dlat = result & 1 ? ~(result >> 1) : result >> 1; + lat += dlat; + + shift = 0; + result = 0; + do { + b = encoded.charCodeAt(index++) - 63; + result |= (b & 0x1f) << shift; + shift += 5; + } while (b >= 0x20); + const dlng = result & 1 ? ~(result >> 1) : result >> 1; + lng += dlng; + + points.push({ + latitude: lat / 1e5, + longitude: lng / 1e5, + }); + } + return points; +}; + +/** + * Fetch directions coordinates between origin and destination using the Google Maps Directions API. + * Falls back to a straight line [origin, destination] if the request fails. + */ +export const getRouteDirections = async ( + origin: { latitude: number; longitude: number }, + destination: { latitude: number; longitude: number }, +): Promise<{ latitude: number; longitude: number }[]> => { + // 1. Try Google Maps Directions API first + try { + const url = + `https://maps.googleapis.com/maps/api/directions/json` + + `?origin=${origin.latitude},${origin.longitude}` + + `&destination=${destination.latitude},${destination.longitude}` + + `&key=${GOOGLE_MAPS_API_KEY}`; + const response = await fetch(url); + const data = await response.json(); + if (data.status === 'OK' && data.routes && data.routes.length > 0) { + console.log('Successfully fetched Google Directions'); + return decodePolyline(data.routes[0].overview_polyline.points); + } else { + console.warn( + 'Google Maps Directions status not OK:', + data.status, + data.error_message || '', + ); + throw new Error(`Google Directions status: ${data.status}`); + } + } catch (error) { + console.error( + 'Error fetching Google Directions, trying OSRM fallback:', + error, + ); + } + + // 2. Fallback to OpenStreetMap OSRM API (completely free and no key required) + try { + const osmUrl = `https://router.project-osrm.org/route/v1/driving/${origin.longitude},${origin.latitude};${destination.longitude},${destination.latitude}?overview=full&geometries=geojson`; + const response = await fetch(osmUrl); + const data = await response.json(); + if (data.code === 'Ok' && data.routes && data.routes.length > 0) { + console.log('Successfully fetched OSRM Directions'); + return data.routes[0].geometry.coordinates.map( + (point: [number, number]) => ({ + latitude: point[1], + longitude: point[0], + }), + ); + } else { + console.warn('OSM OSRM Routing status not Ok:', data.code); + } + } catch (error) { + console.error('Error fetching OSRM directions:', error); + } + + // 3. Ultimate Fallback to straight line + return [origin, destination]; +}; + +/** + * Calculate the distance between two coordinates in kilometers using the Haversine formula. + */ +export const getHaversineDistance = ( + coords1: { latitude: number; longitude: number }, + coords2: { latitude: number; longitude: number }, +): number => { + const toRad = (value: number) => (value * Math.PI) / 180; + const R = 6371; // Earth's radius in km + + const dLat = toRad(coords2.latitude - coords1.latitude); + const dLon = toRad(coords2.longitude - coords1.longitude); + + const a = + Math.sin(dLat / 2) * Math.sin(dLat / 2) + + Math.cos(toRad(coords1.latitude)) * + Math.cos(toRad(coords2.latitude)) * + Math.sin(dLon / 2) * + Math.sin(dLon / 2); + + const c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a)); + return R * c; // Distance in km +}; + diff --git a/app/services/socketService.ts b/app/services/socketService.ts index b5f5ef4..1da1616 100644 --- a/app/services/socketService.ts +++ b/app/services/socketService.ts @@ -6,7 +6,7 @@ import { setDeliveryOffer } from '@store/commonReducers/delivery'; import { RouteNames } from '@utils/constants'; // Must match the REST API base URL (without trailing slash) -const BASE_URL = 'https://157a-202-8-116-13.ngrok-free.app'; +const BASE_URL = 'https://53a9-202-8-116-13.ngrok-free.app'; type Callback = (data: any) => void;