diff --git a/app/App.tsx b/app/App.tsx
index 22fcca9..d774074 100644
--- a/app/App.tsx
+++ b/app/App.tsx
@@ -1,5 +1,5 @@
import React from 'react';
-import { StatusBar, useColorScheme } from 'react-native';
+import { StatusBar, StyleSheet, useColorScheme, View } from 'react-native';
import {
initialWindowMetrics,
SafeAreaProvider,
@@ -10,22 +10,47 @@ import { Provider } from 'react-redux';
import { persistor, store } from './store';
import { RootNavigator } from './navigation/rootNavigator';
import { PersistGate } from 'redux-persist/integration/react';
+import { colors } from '@theme';
function App() {
- const isDarkMode = useColorScheme() === 'dark';
-
return (
-
-
-
-
+
);
}
+function AppContent() {
+ const isDarkMode = useColorScheme() === 'dark';
+ const safeAreaInsets = useSafeAreaInsets(); // β
now inside SafeAreaProvider
+
+ return (
+
+
+
+
+
+
+ );
+}
+
+const styles = StyleSheet.create({
+ container: {
+ flex: 1,
+ },
+});
+
export default App;
diff --git a/app/api/customerDetailsApi.ts b/app/api/customerDetailsApi.ts
index f4e2b7b..ffcc7d0 100644
--- a/app/api/customerDetailsApi.ts
+++ b/app/api/customerDetailsApi.ts
@@ -1,6 +1,10 @@
-import { CustomerResponse } from '@interfaces';
+import { CustomerResponse, WalletResponse } from '@interfaces';
import { apiClient } from '@services';
export const getCustomerDetails = async () => {
return await apiClient.get('/customers/profile');
};
+
+export const getWalletDetails = async (): Promise => {
+ return await apiClient.get('/wallets/balance');
+};
diff --git a/app/components/TrackingMap/TrackingMap.styles.ts b/app/components/TrackingMap/TrackingMap.styles.ts
new file mode 100644
index 0000000..67024d5
--- /dev/null
+++ b/app/components/TrackingMap/TrackingMap.styles.ts
@@ -0,0 +1,28 @@
+import { StyleSheet } from 'react-native';
+
+export const getStyles = (colors: any) =>
+ StyleSheet.create({
+ container: {
+ flex: 1,
+ },
+ map: {
+ ...StyleSheet.absoluteFill,
+ },
+ driverMarker: {
+ backgroundColor: '#FFF',
+ padding: 6,
+ borderRadius: 20,
+ borderWidth: 2,
+ borderColor: '#FF7F00',
+ elevation: 4,
+ shadowColor: '#000',
+ shadowOffset: { width: 0, height: 2 },
+ shadowOpacity: 0.25,
+ shadowRadius: 3.84,
+ justifyContent: 'center',
+ alignItems: 'center',
+ },
+ driverEmoji: {
+ fontSize: 20,
+ },
+ });
diff --git a/app/components/TrackingMap/TrackingMap.tsx b/app/components/TrackingMap/TrackingMap.tsx
new file mode 100644
index 0000000..13d7766
--- /dev/null
+++ b/app/components/TrackingMap/TrackingMap.tsx
@@ -0,0 +1,172 @@
+import React, { useEffect, useRef, useState } from 'react';
+import { View, Text } from 'react-native';
+import MapView, { Marker, Polyline, PROVIDER_GOOGLE } from 'react-native-maps';
+import { useAppTheme } from '@theme';
+import { getRouteDirections, getHaversineDistance } from '@services';
+import { getStyles } from './TrackingMap.styles';
+
+interface TrackingMapProps {
+ customerDropoff: { latitude: number; longitude: number };
+ driverLocation: {
+ latitude: number;
+ longitude: number;
+ heading?: number;
+ } | null;
+ showRoute?: boolean;
+}
+
+export const TrackingMap: React.FC = ({
+ customerDropoff,
+ driverLocation,
+ showRoute = true,
+}) => {
+ const { colors } = useAppTheme();
+ const styles = getStyles(colors);
+ const mapRef = useRef(null);
+ const [routeCoords, setRouteCoords] = useState<
+ { latitude: number; longitude: number }[]
+ >([]);
+ const lastFetchedLocation = useRef<{
+ latitude: number;
+ longitude: number;
+ } | null>(null);
+
+ const initialRegion = {
+ latitude: customerDropoff.latitude,
+ longitude: customerDropoff.longitude,
+ latitudeDelta: 0.02,
+ longitudeDelta: 0.02,
+ };
+ // console.log('[TrackingMap] driverLocation:', driverLocation);
+ // console.log('[TrackingMap] customerDropoff:', customerDropoff);
+ // console.log('[TrackingMap] routeCoords:', routeCoords);
+ // Fetch routing coordinates between driver and customer dropoff
+ useEffect(() => {
+ let isMounted = true;
+ if (showRoute && driverLocation && customerDropoff) {
+ const currentDriverPos = {
+ latitude: driverLocation.latitude,
+ longitude: driverLocation.longitude,
+ };
+
+ // Limit routing API calls if the driver has moved less than 50 meters
+ if (lastFetchedLocation.current) {
+ const dist = getHaversineDistance(
+ currentDriverPos,
+ lastFetchedLocation.current,
+ );
+ if (dist < 0.05 && routeCoords.length > 0) {
+ return;
+ }
+ }
+
+ getRouteDirections(currentDriverPos, customerDropoff)
+ .then(points => {
+ if (isMounted) {
+ setRouteCoords(points);
+ lastFetchedLocation.current = currentDriverPos;
+ }
+ })
+ .catch(err => {
+ console.error(
+ '[TrackingMap] Error fetching routing directions:',
+ err,
+ );
+ if (isMounted) {
+ // Fallback to straight line
+ setRouteCoords([currentDriverPos, customerDropoff]);
+ }
+ });
+ } else {
+ setRouteCoords([]);
+ lastFetchedLocation.current = null;
+ }
+
+ return () => {
+ isMounted = false;
+ };
+ }, [
+ showRoute,
+ driverLocation?.latitude,
+ driverLocation?.longitude,
+ customerDropoff?.latitude,
+ customerDropoff?.longitude,
+ ]);
+
+ // Adjust camera to fit both the customer dropoff and driver location
+ useEffect(() => {
+ const coordsToFit: { latitude: number; longitude: number }[] = [];
+ if (customerDropoff) {
+ coordsToFit.push(customerDropoff);
+ }
+ if (driverLocation) {
+ coordsToFit.push({
+ latitude: driverLocation.latitude,
+ longitude: driverLocation.longitude,
+ });
+ }
+
+ if (coordsToFit.length > 0 && mapRef.current) {
+ const timer = setTimeout(() => {
+ mapRef.current?.fitToCoordinates(coordsToFit, {
+ edgePadding: { top: 80, right: 80, bottom: 80, left: 80 },
+ animated: true,
+ });
+ }, 500);
+ return () => clearTimeout(timer);
+ }
+ }, [
+ customerDropoff?.latitude,
+ customerDropoff?.longitude,
+ driverLocation?.latitude,
+ driverLocation?.longitude,
+ ]);
+
+ return (
+
+
+ {/* Customer Dropoff Location Marker */}
+ {customerDropoff && (
+
+ )}
+
+ {/* Live Driver Marker */}
+ {driverLocation && (
+
+
+ π΅
+
+
+ )}
+
+ {/* Live Routing Polyline */}
+ {showRoute && routeCoords.length > 0 && (
+
+ )}
+
+
+ );
+};
+
+export default TrackingMap;
diff --git a/app/components/TrackingMap/index.ts b/app/components/TrackingMap/index.ts
new file mode 100644
index 0000000..bc4d010
--- /dev/null
+++ b/app/components/TrackingMap/index.ts
@@ -0,0 +1 @@
+export * from './TrackingMap';
diff --git a/app/components/index.ts b/app/components/index.ts
index 00a4a73..63fbf59 100644
--- a/app/components/index.ts
+++ b/app/components/index.ts
@@ -14,3 +14,5 @@ export * from './ratingStars';
export * from './orderHistoryCard';
export * from './paymentOption';
export * from './OrderStatusTimeline';
+export * from './TrackingMap';
+
diff --git a/app/components/orderHistoryCard/orderHistoryCard.props.ts b/app/components/orderHistoryCard/orderHistoryCard.props.ts
index 2161c08..a9ff15e 100644
--- a/app/components/orderHistoryCard/orderHistoryCard.props.ts
+++ b/app/components/orderHistoryCard/orderHistoryCard.props.ts
@@ -6,7 +6,7 @@ export interface OrderHistoryCardProps {
orderDate: string;
status: string;
total: number;
- onReorder?: () => void;
+ onTrack?: () => void;
onDetails?: () => void;
items?: OrderItem[];
}
diff --git a/app/components/orderHistoryCard/orderHistoryCard.tsx b/app/components/orderHistoryCard/orderHistoryCard.tsx
index e37a9b8..f58a1ea 100644
--- a/app/components/orderHistoryCard/orderHistoryCard.tsx
+++ b/app/components/orderHistoryCard/orderHistoryCard.tsx
@@ -10,7 +10,7 @@ export const OrderHistoryCard: React.FC = ({
orderDate,
status,
total,
- onReorder,
+ onTrack,
onDetails,
items,
}) => {
@@ -48,13 +48,13 @@ export const OrderHistoryCard: React.FC = ({
βΉ{total}
- {onReorder && (
+ {onTrack && status === 'OUT_FOR_DELIVERY' && (
- Reorder
+ Track Order
)}
{onDetails && (
diff --git a/app/features/screens/LoginScreen/hooks/useLoginScreen.ts b/app/features/screens/LoginScreen/hooks/useLoginScreen.ts
index acc485a..710ccf5 100644
--- a/app/features/screens/LoginScreen/hooks/useLoginScreen.ts
+++ b/app/features/screens/LoginScreen/hooks/useLoginScreen.ts
@@ -3,6 +3,7 @@ import { useNavigation } from '@react-navigation/native';
import { StackNavigationProp } from '@react-navigation/stack';
import { AuthStackParamList } from 'app/navigation/authStack';
import { loginWithPhone, useAppDispatch } from '@store';
+import { Alert } from 'react-native';
type LoginNavProp = StackNavigationProp;
@@ -50,8 +51,14 @@ export const useLoginScreen = (): UseLoginScreenResult => {
}
setError(undefined);
- dispatch(loginWithPhone(mobileNumber));
- navigation.navigate('OtpScreen', { mobileNumber });
+ dispatch(loginWithPhone(mobileNumber))
+ .unwrap()
+ .then(() => {
+ navigation.navigate('OtpScreen', { mobileNumber });
+ })
+ .catch(err => {
+ Alert.alert(err);
+ });
}, [mobileNumber, dispatch, navigation]);
return {
diff --git a/app/features/screens/accountScreen/accountScreen.tsx b/app/features/screens/accountScreen/accountScreen.tsx
index 3bc53a6..24b5dc6 100644
--- a/app/features/screens/accountScreen/accountScreen.tsx
+++ b/app/features/screens/accountScreen/accountScreen.tsx
@@ -1,18 +1,19 @@
-import React from 'react';
+import React, { useCallback } from 'react';
import { View, Text, TouchableOpacity, ScrollView } from 'react-native';
import {
useNavigation,
CompositeNavigationProp,
+ useFocusEffect,
} from '@react-navigation/native';
import { BottomTabNavigationProp } from '@react-navigation/bottom-tabs';
import { StackNavigationProp } from '@react-navigation/stack';
import { getStyles } from './accountScreen.styles';
import { Header, PrimaryButton } from '@components';
import { useAppTheme } from '@theme';
-import { logout } from '../../../store/commonreducers/auth';
import { AppStackParamList } from '../../../navigation/appStack';
import { MainTabParamList } from '../../../navigation/mainTabNavigator';
-import { useAppDispatch, useAppSelector } from '@store';
+import { logoutUser, useAppDispatch, useAppSelector } from '@store';
+import { getWalletBalanceThunk } from './thunk';
type AccountNavProp = CompositeNavigationProp<
BottomTabNavigationProp,
@@ -44,7 +45,7 @@ const MENU_SECTIONS: MenuSectionData[] = [
},
{
icon: 'π³',
- label: 'Payment Methods',
+ label: 'Wallet',
subLabel: 'Cards, UPI & wallets',
tint: '#FFF4E5',
},
@@ -87,7 +88,16 @@ export const AccountScreen: React.FC = () => {
const styles = getStyles(colors);
const dispatch = useAppDispatch();
const navigation = useNavigation();
- const user = useAppSelector(state => state.auth.user);
+ const user = useAppSelector(
+ state => state.customerProfile.customerDetails?.user,
+ );
+ const { wallet } = useAppSelector(state => state.account);
+
+ useFocusEffect(
+ useCallback(() => {
+ dispatch(getWalletBalanceThunk());
+ }, []),
+ );
// TODO: wire these to real selectors once order/wallet state is available.
// const ordersCount = user?.ordersCount ?? 0;
@@ -189,7 +199,10 @@ export const AccountScreen: React.FC = () => {
{/* Logout */}
- dispatch(logout())} />
+ dispatch(logoutUser())}
+ />
App version 1.0.0
diff --git a/app/features/screens/accountScreen/index.ts b/app/features/screens/accountScreen/index.ts
index efe9de3..98d361c 100644
--- a/app/features/screens/accountScreen/index.ts
+++ b/app/features/screens/accountScreen/index.ts
@@ -1 +1,3 @@
export * from './accountScreen';
+export * from './thunk';
+export * from './reducer';
diff --git a/app/features/screens/accountScreen/reducer.ts b/app/features/screens/accountScreen/reducer.ts
new file mode 100644
index 0000000..04b22e6
--- /dev/null
+++ b/app/features/screens/accountScreen/reducer.ts
@@ -0,0 +1,26 @@
+import { createReducer } from '@reduxjs/toolkit';
+import { WalletResponse } from '@interfaces';
+import { getWalletBalanceThunk } from './thunk';
+
+export interface WalletState {
+ wallet: WalletResponse;
+ error: string | null;
+}
+
+const initialState: WalletState = {
+ wallet: {
+ walletId: '',
+ currency: '',
+ balance: 0,
+ },
+ error: null,
+};
+
+export const accountReducer = createReducer(initialState, builder => {
+ builder.addCase(getWalletBalanceThunk.fulfilled, (state, action) => {
+ state.wallet = action.payload;
+ });
+ builder.addCase(getWalletBalanceThunk.rejected, (state, action) => {
+ state.error = action.payload as string;
+ });
+});
diff --git a/app/features/screens/accountScreen/thunk.ts b/app/features/screens/accountScreen/thunk.ts
new file mode 100644
index 0000000..8899e0f
--- /dev/null
+++ b/app/features/screens/accountScreen/thunk.ts
@@ -0,0 +1,14 @@
+import { createAsyncThunk } from '@reduxjs/toolkit';
+import { getWalletDetails } from '@api';
+
+export const getWalletBalanceThunk = createAsyncThunk(
+ 'wallet/getWalletBalance',
+ async (_, { rejectWithValue }) => {
+ try {
+ const data = await getWalletDetails();
+ return data;
+ } catch (error) {
+ return rejectWithValue(error);
+ }
+ },
+);
diff --git a/app/features/screens/liveTrackingScreen/hooks/index.ts b/app/features/screens/liveTrackingScreen/hooks/index.ts
new file mode 100644
index 0000000..aaf348c
--- /dev/null
+++ b/app/features/screens/liveTrackingScreen/hooks/index.ts
@@ -0,0 +1 @@
+export * from './useOrderTracking';
diff --git a/app/features/screens/liveTrackingScreen/hooks/useOrderTracking.ts b/app/features/screens/liveTrackingScreen/hooks/useOrderTracking.ts
new file mode 100644
index 0000000..38293cb
--- /dev/null
+++ b/app/features/screens/liveTrackingScreen/hooks/useOrderTracking.ts
@@ -0,0 +1,49 @@
+import { useEffect, useState } from 'react';
+import { socketService, DriverLocationUpdate } from '@services';
+
+export function useOrderTracking(orderId: string, userJwtToken: string | null) {
+ const [driverLocation, setDriverLocation] =
+ useState(null);
+ const [orderStatus, setOrderStatus] = useState(null);
+
+ useEffect(() => {
+ if (!orderId || !userJwtToken) {
+ return;
+ }
+
+ console.log(`[useOrderTracking] Connecting socket for orderId: ${orderId}`);
+ socketService.connect(userJwtToken);
+ socketService.joinOrderTracking(orderId);
+
+ const handleDriverLocationUpdate = (data: DriverLocationUpdate) => {
+ console.log('[useOrderTracking] handleDriverLocationUpdate:', data);
+ if (data.orderId === orderId) {
+ setDriverLocation(data);
+ }
+ };
+
+ const handleOrderStatusUpdate = (data: any) => {
+ if (data.orderId === orderId) {
+ setOrderStatus(data.status);
+ if (data.status === 'DELIVERED') {
+ console.log(
+ '[useOrderTracking] Order delivered. Disconnecting socket.',
+ );
+ socketService.disconnect(orderId);
+ }
+ }
+ };
+
+ socketService.onDriverLocationUpdate(handleDriverLocationUpdate);
+ socketService.onOrderStatusUpdate(handleOrderStatusUpdate);
+
+ return () => {
+ console.log(
+ `[useOrderTracking] Cleaning up tracking socket for orderId: ${orderId}`,
+ );
+ socketService.disconnect(orderId);
+ };
+ }, [orderId, userJwtToken]);
+
+ return { driverLocation, orderStatus };
+}
diff --git a/app/features/screens/liveTrackingScreen/index.ts b/app/features/screens/liveTrackingScreen/index.ts
index 293397b..dbff3ba 100644
--- a/app/features/screens/liveTrackingScreen/index.ts
+++ b/app/features/screens/liveTrackingScreen/index.ts
@@ -1 +1,2 @@
export * from './liveTrackingScreen';
+export * from './hooks';
diff --git a/app/features/screens/liveTrackingScreen/liveTrackingScreen.tsx b/app/features/screens/liveTrackingScreen/liveTrackingScreen.tsx
index 0e5304b..447b384 100644
--- a/app/features/screens/liveTrackingScreen/liveTrackingScreen.tsx
+++ b/app/features/screens/liveTrackingScreen/liveTrackingScreen.tsx
@@ -1,13 +1,19 @@
-import React from 'react';
-import { View, Text } from 'react-native';
+import React, { useEffect } from 'react';
+import { View, Text, ActivityIndicator } from 'react-native';
import { useNavigation, useRoute, RouteProp } from '@react-navigation/native';
import { StackNavigationProp } from '@react-navigation/stack';
import { getStyles } from './liveTrackingScreen.styles';
-import { Header, PrimaryButton } from '@components';
+import { Header, PrimaryButton, TrackingMap } from '@components';
import { useAppTheme } from '@theme';
import { AppStackParamList } from '../../../navigation/appStack';
+import { RootState, useAppDispatch, useAppSelector } from '@store';
+import { getOrderByIdThunk } from '../checkoutPaymentScreen';
+import { useOrderTracking } from './hooks';
-type LiveTrackingNavProp = StackNavigationProp;
+type LiveTrackingNavProp = StackNavigationProp<
+ AppStackParamList,
+ 'LiveTrackingScreen'
+>;
type LiveTrackingRouteProp = RouteProp;
export const LiveTrackingScreen: React.FC = () => {
@@ -17,23 +23,73 @@ export const LiveTrackingScreen: React.FC = () => {
const route = useRoute();
const orderId = route.params?.orderId || 'ORD-123456';
+ const dispatch = useAppDispatch();
+ const { accessToken } = useAppSelector((state: RootState) => state.auth);
+ const { orderDetails, orderDetailsLoading } = useAppSelector(
+ (state: RootState) => state.paymentMethods,
+ );
+ // console.log('[LiveTrackingScreen] orderDetails:', orderDetails);
+
+ const { driverLocation, orderStatus } = useOrderTracking(
+ orderId,
+ accessToken,
+ );
+
+ // Fetch order details if not matching or missing
+ useEffect(() => {
+ if (orderId && (!orderDetails || orderDetails.id !== orderId)) {
+ dispatch(getOrderByIdThunk(orderId));
+ }
+ }, [dispatch, orderId, orderDetails]);
+
+ // Navigate to delivered screen once status transitions to DELIVERED
+ useEffect(() => {
+ if (orderStatus === 'DELIVERED') {
+ console.log(
+ `[LiveTrackingScreen] Order ${orderId} delivered! Navigating to OrderDeliveredScreen.`,
+ );
+ navigation.navigate('OrderDeliveredScreen', { orderId });
+ }
+ }, [orderStatus, navigation, orderId]);
+
+ if (orderDetailsLoading || !orderDetails) {
+ return (
+
+ navigation.goBack()} />
+
+
+
+ Loading tracking details...
+
+
+
+ );
+ }
+
+ // Get drop address coordinates from orderDetails
+ const dropCoords = {
+ latitude: orderDetails.dropAddress?.latitude || 12.9352,
+ longitude: orderDetails.dropAddress?.longitude || 77.6245,
+ };
+
return (
navigation.goBack()} />
-
- πΊοΈ
- Live Map
-
- Delivery partner location tracking for {orderId}
-
-
- β Live
-
+
+
navigation.navigate('OrderDeliveredScreen', { orderId })}
+ onPress={() =>
+ navigation.navigate('OrderDeliveredScreen', { orderId })
+ }
style={{ marginBottom: 16 }}
/>
Rahul Sharma
@@ -55,3 +111,4 @@ export const LiveTrackingScreen: React.FC = () => {
);
};
+export default LiveTrackingScreen;
diff --git a/app/features/screens/myOrdersScreen/myOrdersScreen.tsx b/app/features/screens/myOrdersScreen/myOrdersScreen.tsx
index 3f0e179..9458986 100644
--- a/app/features/screens/myOrdersScreen/myOrdersScreen.tsx
+++ b/app/features/screens/myOrdersScreen/myOrdersScreen.tsx
@@ -57,7 +57,7 @@ export const MyOrdersScreen: React.FC = () => {
const { orderHistory } = useAppSelector(
(state: RootState) => state.paymentMethods,
);
- console.log(orderHistory);
+ // console.log(orderHistory);
useEffect(() => {
dispatch(getOrderHistoryThunk());
@@ -114,29 +114,34 @@ export const MyOrdersScreen: React.FC = () => {
status={item.status}
total={Number(item?.totalAmount) || 0}
items={item.orderItems}
- onReorder={() => {
- navigation.navigate('ProviderDetailsScreen', {
- providerId: 'p1',
- providerName: item?.merchant?.name || '',
- });
- }}
- onDetails={() => {
- if (
- item?.status === 'PENDING' ||
- item?.status === 'CONFIRMED' ||
- item?.status === 'PREPARING' ||
- item?.status === 'READY_FOR_PICKUP' ||
- item?.status === 'OUT_FOR_DELIVERY'
- ) {
- navigation.navigate('OrderDetailsScreen', {
- orderId: item.id,
- });
- } else {
- navigation.navigate('OrderDeliveredScreen', {
- orderId: item.id,
+ onTrack={() => {
+ if (item?.status === 'OUT_FOR_DELIVERY') {
+ // navigation.navigate('ProviderDetailsScreen', {
+ // providerName: item?.merchant?.name || '',
+ // });
+ // } else {
+ navigation.navigate('LiveTrackingScreen', {
+ orderId: item?.id || '',
});
}
}}
+ onDetails={() => {
+ // if (
+ // item?.status === 'PENDING' ||
+ // item?.status === 'CONFIRMED' ||
+ // item?.status === 'PREPARING' ||
+ // item?.status === 'READY_FOR_PICKUP' ||
+ // item?.status === 'OUT_FOR_DELIVERY'
+ // ) {
+ navigation.navigate('OrderDetailsScreen', {
+ orderId: item.id,
+ });
+ // } else {
+ // navigation.navigate('OrderDeliveredScreen', {
+ // orderId: item.id,
+ // });
+ // }
+ }}
/>
)}
contentContainerStyle={styles.list}
diff --git a/app/interfaces/customerProfile.ts b/app/interfaces/customerProfile.ts
index 105b5fa..186ae9d 100644
--- a/app/interfaces/customerProfile.ts
+++ b/app/interfaces/customerProfile.ts
@@ -47,3 +47,9 @@ export interface CustomerAddress {
// export type UserStatus = 'ACTIVE' | 'INACTIVE' | 'SUSPENDED';
export type AddressLabel = 'Home' | 'Work' | 'Other';
+
+export interface WalletResponse {
+ walletId: string;
+ currency: string;
+ balance: number;
+}
diff --git a/app/navigation/appStack.tsx b/app/navigation/appStack.tsx
index 6fa7868..0f32658 100644
--- a/app/navigation/appStack.tsx
+++ b/app/navigation/appStack.tsx
@@ -20,7 +20,7 @@ import {
export type AppStackParamList = {
MainTabs: NavigatorScreenParams | undefined;
ProviderListScreen: { category?: string } | undefined;
- ProviderDetailsScreen: { providerId: string; providerName?: string };
+ ProviderDetailsScreen: { providerId?: string; providerName?: string };
CartScreen: undefined;
CheckoutAddressScreen: undefined;
CheckoutPaymentScreen: { selectedAddressId?: string } | undefined;
@@ -68,10 +68,7 @@ export const AppStack: React.FC = () => {
/>
-
+
);
};
diff --git a/app/services/apiClient.ts b/app/services/apiClient.ts
index 9e7ba68..4bc15fd 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://2dbb-202-8-116-13.ngrok-free.app'; // TODO: replace with your actual base URL
// βββ Token Helpers βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
export const tokenManager = {
@@ -131,7 +131,7 @@ axiosInstance.interceptors.response.use(
}
// Call refresh endpoint (uses a fresh axios call to avoid interceptor loop)
- const { data } = await axios.post(`${BASE_URL}/auth/refresh-token`, {
+ const { data } = await axios.post(`${BASE_URL}/auth/refresh`, {
refreshToken,
});
diff --git a/app/services/index.ts b/app/services/index.ts
index ca175f6..66fa671 100644
--- a/app/services/index.ts
+++ b/app/services/index.ts
@@ -1,2 +1,3 @@
export * from './apiClient';
export * from './locationServices';
+export * from './socketService';
diff --git a/app/services/locationServices.ts b/app/services/locationServices.ts
index d13fbee..e92c780 100644
--- a/app/services/locationServices.ts
+++ b/app/services/locationServices.ts
@@ -227,3 +227,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,
+): LatLng[] => {
+ const points: LatLng[] = [];
+ 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: LatLng,
+ destination: LatLng,
+): Promise => {
+ // 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: LatLng,
+ coords2: LatLng,
+): 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
new file mode 100644
index 0000000..5189e5a
--- /dev/null
+++ b/app/services/socketService.ts
@@ -0,0 +1,94 @@
+import { io, Socket } from 'socket.io-client';
+
+const BASE_URL = 'https://2dbb-202-8-116-13.ngrok-free.app';
+const SOCKET_URL = `${BASE_URL}/tracking`;
+
+export interface DriverLocationUpdate {
+ orderId: string;
+ latitude: number;
+ longitude: number;
+ heading?: number;
+ speedKph?: number;
+ recordedAt: string;
+}
+
+export interface TrackingStatusUpdate {
+ orderId: string;
+ status: 'OUT_FOR_DELIVERY' | 'DELIVERED';
+ timestamp: string;
+}
+
+class SocketService {
+ private socket: Socket | null = null;
+
+ connect(token: string): Socket {
+ if (this.socket?.connected) {
+ console.log(
+ '[Socket] Already connected to Tracking Namespace',
+ this.socket,
+ );
+ return this.socket;
+ }
+
+ this.socket = io(SOCKET_URL, {
+ auth: { token },
+ transports: ['websocket'],
+ reconnection: true,
+ reconnectionAttempts: Infinity,
+ reconnectionDelay: 2000,
+ });
+
+ this.socket.on('connect', () => {
+ console.log('[Socket] Connected to Tracking Namespace');
+ });
+
+ this.socket.on('connect_error', error => {
+ console.error('[Socket] Connection Error:', error.message);
+ });
+
+ this.socket.on('disconnect', reason => {
+ console.log('[Socket] Disconnected from Tracking Namespace:', reason);
+ });
+
+ return this.socket;
+ }
+
+ joinOrderTracking(orderId: string) {
+ this.socket?.emit('join_order_tracking', { orderId });
+ console.log(`[Socket] Emitted join_order_tracking for order: ${orderId}`);
+ }
+
+ leaveOrderTracking(orderId: string) {
+ this.socket?.emit('leave_order_tracking', { orderId });
+ console.log(`[Socket] Emitted leave_order_tracking for order: ${orderId}`);
+ }
+
+ onDriverLocationUpdate(callback: (data: DriverLocationUpdate) => void) {
+ console.log(callback);
+ this.socket?.on('driver_location_update', callback);
+ }
+
+ onOrderStatusUpdate(callback: (data: TrackingStatusUpdate) => void) {
+ this.socket?.on('order_tracking_status', callback);
+ }
+
+ disconnect(orderId?: string) {
+ if (this.socket) {
+ if (orderId) {
+ this.leaveOrderTracking(orderId);
+ }
+ this.socket.off('driver_location_update');
+ this.socket.off('order_tracking_status');
+ this.socket.disconnect();
+ this.socket = null;
+ console.log('[Socket] Disconnected manually');
+ }
+ }
+
+ get connected(): boolean {
+ return this.socket?.connected ?? false;
+ }
+}
+
+export const socketService = new SocketService();
+export default socketService;
diff --git a/app/store/commonreducers/index.ts b/app/store/commonreducers/index.ts
index 6304c85..cef4eba 100644
--- a/app/store/commonreducers/index.ts
+++ b/app/store/commonreducers/index.ts
@@ -2,3 +2,4 @@ export * from './auth';
export * from './cart';
export * from './order';
export * from './customerProfile';
+export * from './offer';
diff --git a/app/store/rootReducer.ts b/app/store/rootReducer.ts
index fae5a19..bbbb08f 100644
--- a/app/store/rootReducer.ts
+++ b/app/store/rootReducer.ts
@@ -11,6 +11,7 @@ import providerDetailsReducer from '@features/screens/providerDetailsScreen/redu
import paymentMethodsReducer from '@features/screens/checkoutPaymentScreen/reducer';
import customerProfileReducer from './commonreducers/customerProfile/reducer';
import { offerReducer } from './commonreducers/offer';
+import { accountReducer } from '@features/screens';
const rootReducer = combineReducers({
auth: authReducer,
@@ -24,6 +25,7 @@ const rootReducer = combineReducers({
paymentMethods: paymentMethodsReducer,
customerProfile: customerProfileReducer,
offer: offerReducer,
+ account: accountReducer,
});
export type RootState = ReturnType;
diff --git a/app/utils/helper.ts b/app/utils/helper.ts
index 22b2e6e..eb9a59c 100644
--- a/app/utils/helper.ts
+++ b/app/utils/helper.ts
@@ -1,5 +1,5 @@
export const getFullUrl = (url?: string) => {
- const BASE_URL = 'https://157a-202-8-116-13.ngrok-free.app';
+ const BASE_URL = 'https://2dbb-202-8-116-13.ngrok-free.app';
if (!url) return '';
return url.startsWith('/') ? `${BASE_URL}${url}` : url;
};
diff --git a/package.json b/package.json
index 60b8bb3..ed85aea 100644
--- a/package.json
+++ b/package.json
@@ -34,7 +34,8 @@
"react-redux": "^9.3.0",
"reactotron-react-native": "^5.2.0",
"redux-persist": "^6.0.0",
- "redux-thunk": "^3.1.0"
+ "redux-thunk": "^3.1.0",
+ "socket.io-client": "^4.8.3"
},
"devDependencies": {
"@babel/core": "^7.25.2",
diff --git a/yarn.lock b/yarn.lock
index 644c061..2597882 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -1837,6 +1837,11 @@
dependencies:
"@sinonjs/commons" "^3.0.0"
+"@socket.io/component-emitter@~3.1.0":
+ version "3.1.2"
+ resolved "https://registry.yarnpkg.com/@socket.io/component-emitter/-/component-emitter-3.1.2.tgz#821f8442f4175d8f0467b9daf26e3a18e2d02af2"
+ integrity sha512-9BCxFwvbGg/RsZK9tjXd8s4UcwR0MWeFQ1XEKIQVVvAGJyINdrqKMcTRyLoK8Rse1GjzLV9cwjWV1olXRWEXVA==
+
"@standard-schema/spec@^1.0.0":
version "1.1.0"
resolved "https://registry.yarnpkg.com/@standard-schema/spec/-/spec-1.1.0.tgz#a79b55dbaf8604812f52d140b2c9ab41bc150bb8"
@@ -2929,7 +2934,7 @@ debug@2.6.9, debug@^2.6.9:
dependencies:
ms "2.0.0"
-debug@4, debug@^4.1.0, debug@^4.1.1, debug@^4.3.1, debug@^4.3.2, debug@^4.4.0, debug@^4.4.3:
+debug@4, debug@^4.1.0, debug@^4.1.1, debug@^4.3.1, debug@^4.3.2, debug@^4.4.0, debug@^4.4.3, debug@~4.4.1:
version "4.4.3"
resolved "https://registry.yarnpkg.com/debug/-/debug-4.4.3.tgz#c6ae432d9bd9662582fce08709b038c58e9e3d6a"
integrity sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==
@@ -3094,6 +3099,22 @@ encodeurl@~2.0.0:
resolved "https://registry.yarnpkg.com/encodeurl/-/encodeurl-2.0.0.tgz#7b8ea898077d7e409d3ac45474ea38eaf0857a58"
integrity sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==
+engine.io-client@~6.6.1:
+ version "6.6.6"
+ resolved "https://registry.yarnpkg.com/engine.io-client/-/engine.io-client-6.6.6.tgz#8a8f1e451b1f6d4acf413305445e42133d1cbe9a"
+ integrity sha512-iY6QdftLQ9pyiPoX082bpf/u1UewnOaJrtJIF9T0++QB34lZrj0uP+Q/bj8AlUsAxqhnkTV2BS8SBZSxOmoV5Q==
+ dependencies:
+ "@socket.io/component-emitter" "~3.1.0"
+ debug "~4.4.1"
+ engine.io-parser "~5.2.1"
+ ws "~8.21.0"
+ xmlhttprequest-ssl "~2.1.1"
+
+engine.io-parser@~5.2.1:
+ version "5.2.3"
+ resolved "https://registry.yarnpkg.com/engine.io-parser/-/engine.io-parser-5.2.3.tgz#00dc5b97b1f233a23c9398d0209504cf5f94d92f"
+ integrity sha512-HqD3yTBfnBxIrbnM1DoD6Pcq8NECnh8d4As1Qgh0z5Gg3jRRIqijury0CL3ghu/edArpUYiYqQiDUQBIs4np3Q==
+
entities@^4.2.0:
version "4.5.0"
resolved "https://registry.yarnpkg.com/entities/-/entities-4.5.0.tgz#5d268ea5e7113ec74c4d033b79ea5a35a488fb48"
@@ -6379,6 +6400,24 @@ slice-ansi@^2.0.0:
astral-regex "^1.0.0"
is-fullwidth-code-point "^2.0.0"
+socket.io-client@^4.8.3:
+ version "4.8.3"
+ resolved "https://registry.yarnpkg.com/socket.io-client/-/socket.io-client-4.8.3.tgz#62717edd46a318c918125b57e92dc7f8bb71c34c"
+ integrity sha512-uP0bpjWrjQmUt5DTHq9RuoCBdFJF10cdX9X+a368j/Ft0wmaVgxlrjvK3kjvgCODOMMOz9lcaRzxmso0bTWZ/g==
+ dependencies:
+ "@socket.io/component-emitter" "~3.1.0"
+ debug "~4.4.1"
+ engine.io-client "~6.6.1"
+ socket.io-parser "~4.2.4"
+
+socket.io-parser@~4.2.4:
+ version "4.2.7"
+ resolved "https://registry.yarnpkg.com/socket.io-parser/-/socket.io-parser-4.2.7.tgz#679e51fe24d1c81df90fc5f7efe4a5f432fe99c0"
+ integrity sha512-IH/iSeO9T6gz1KkFleGDWkG9N3dl4jXVYUtMhIqH10Md0ttMer8nUNWiP1DKuNrybD2xBrixLJdCC9J6ECoYkg==
+ dependencies:
+ "@socket.io/component-emitter" "~3.1.0"
+ debug "~4.4.1"
+
source-map-support@0.5.13:
version "0.5.13"
resolved "https://registry.yarnpkg.com/source-map-support/-/source-map-support-0.5.13.tgz#31b24a9c2e73c2de85066c0feb7d44767ed52932"
@@ -6987,6 +7026,16 @@ ws@^7, ws@^7.5.10:
resolved "https://registry.yarnpkg.com/ws/-/ws-7.5.11.tgz#9460daf1812bb81a423c5b9eac746941a86310fa"
integrity sha512-zS54Oen9bITtp7kp2XM3AydrCIq1D+HwJOuH+c+e4LfpL/lotP5osijd+UoMnxwAam1GN8R4KtLAyIrIcBNpiA==
+ws@~8.21.0:
+ version "8.21.1"
+ resolved "https://registry.yarnpkg.com/ws/-/ws-8.21.1.tgz#045650cd4b1207809e7547146223c3814a9af586"
+ integrity sha512-+0NTnW77fFN/DjQi6k/Sq/Yvk4Sgajw7urW8V+asjXnRgDs9gyGkdb7EzgfhA4goXsRIZKE28fzIXBHEzhuiWw==
+
+xmlhttprequest-ssl@~2.1.1:
+ version "2.1.2"
+ resolved "https://registry.yarnpkg.com/xmlhttprequest-ssl/-/xmlhttprequest-ssl-2.1.2.tgz#e9e8023b3f29ef34b97a859f584c5e6c61418e23"
+ integrity sha512-TEU+nJVUUnA4CYJFLvK5X9AOeH4KvDvhIfm0vV1GaQRtchnG0hgK5p8hw/xjv8cunWYCsiPCSDzObPyhEwq3KQ==
+
y18n@^4.0.0:
version "4.0.3"
resolved "https://registry.yarnpkg.com/y18n/-/y18n-4.0.3.tgz#b5f259c82cd6e336921efd7bfd8bf560de9eeedf"