feat: implement real-time order tracking with socket.io, location services, and dedicated map components
This commit is contained in:
parent
c7dbdd3b8a
commit
5b8bc3d88a
39
app/App.tsx
39
app/App.tsx
@ -1,5 +1,5 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { StatusBar, useColorScheme } from 'react-native';
|
import { StatusBar, StyleSheet, useColorScheme, View } from 'react-native';
|
||||||
import {
|
import {
|
||||||
initialWindowMetrics,
|
initialWindowMetrics,
|
||||||
SafeAreaProvider,
|
SafeAreaProvider,
|
||||||
@ -10,22 +10,47 @@ import { Provider } from 'react-redux';
|
|||||||
import { persistor, store } from './store';
|
import { persistor, store } from './store';
|
||||||
import { RootNavigator } from './navigation/rootNavigator';
|
import { RootNavigator } from './navigation/rootNavigator';
|
||||||
import { PersistGate } from 'redux-persist/integration/react';
|
import { PersistGate } from 'redux-persist/integration/react';
|
||||||
|
import { colors } from '@theme';
|
||||||
|
|
||||||
function App() {
|
function App() {
|
||||||
const isDarkMode = useColorScheme() === 'dark';
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Provider store={store}>
|
<Provider store={store}>
|
||||||
<PersistGate loading={null} persistor={persistor}>
|
<PersistGate loading={null} persistor={persistor}>
|
||||||
<SafeAreaProvider>
|
<SafeAreaProvider>
|
||||||
<StatusBar barStyle={isDarkMode ? 'light-content' : 'dark-content'} />
|
<AppContent />
|
||||||
<NavigationContainer>
|
|
||||||
<RootNavigator />
|
|
||||||
</NavigationContainer>
|
|
||||||
</SafeAreaProvider>
|
</SafeAreaProvider>
|
||||||
</PersistGate>
|
</PersistGate>
|
||||||
</Provider>
|
</Provider>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function AppContent() {
|
||||||
|
const isDarkMode = useColorScheme() === 'dark';
|
||||||
|
const safeAreaInsets = useSafeAreaInsets(); // ✅ now inside SafeAreaProvider
|
||||||
|
|
||||||
|
return (
|
||||||
|
<View
|
||||||
|
style={[
|
||||||
|
styles.container,
|
||||||
|
{
|
||||||
|
paddingTop: safeAreaInsets.top,
|
||||||
|
paddingBottom: safeAreaInsets.bottom,
|
||||||
|
backgroundColor: colors.background,
|
||||||
|
},
|
||||||
|
]}
|
||||||
|
>
|
||||||
|
<StatusBar barStyle={isDarkMode ? 'light-content' : 'dark-content'} />
|
||||||
|
<NavigationContainer>
|
||||||
|
<RootNavigator />
|
||||||
|
</NavigationContainer>
|
||||||
|
</View>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const styles = StyleSheet.create({
|
||||||
|
container: {
|
||||||
|
flex: 1,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
export default App;
|
export default App;
|
||||||
|
|||||||
@ -1,6 +1,10 @@
|
|||||||
import { CustomerResponse } from '@interfaces';
|
import { CustomerResponse, WalletResponse } from '@interfaces';
|
||||||
import { apiClient } from '@services';
|
import { apiClient } from '@services';
|
||||||
|
|
||||||
export const getCustomerDetails = async () => {
|
export const getCustomerDetails = async () => {
|
||||||
return await apiClient.get<CustomerResponse>('/customers/profile');
|
return await apiClient.get<CustomerResponse>('/customers/profile');
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const getWalletDetails = async (): Promise<WalletResponse> => {
|
||||||
|
return await apiClient.get<WalletResponse>('/wallets/balance');
|
||||||
|
};
|
||||||
|
|||||||
28
app/components/TrackingMap/TrackingMap.styles.ts
Normal file
28
app/components/TrackingMap/TrackingMap.styles.ts
Normal file
@ -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,
|
||||||
|
},
|
||||||
|
});
|
||||||
172
app/components/TrackingMap/TrackingMap.tsx
Normal file
172
app/components/TrackingMap/TrackingMap.tsx
Normal file
@ -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<TrackingMapProps> = ({
|
||||||
|
customerDropoff,
|
||||||
|
driverLocation,
|
||||||
|
showRoute = true,
|
||||||
|
}) => {
|
||||||
|
const { colors } = useAppTheme();
|
||||||
|
const styles = getStyles(colors);
|
||||||
|
const mapRef = useRef<MapView>(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 (
|
||||||
|
<View style={styles.container}>
|
||||||
|
<MapView
|
||||||
|
ref={mapRef}
|
||||||
|
provider={PROVIDER_GOOGLE}
|
||||||
|
style={styles.map}
|
||||||
|
initialRegion={initialRegion}
|
||||||
|
>
|
||||||
|
{/* Customer Dropoff Location Marker */}
|
||||||
|
{customerDropoff && (
|
||||||
|
<Marker
|
||||||
|
coordinate={customerDropoff}
|
||||||
|
title="Delivery Location"
|
||||||
|
pinColor="red"
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Live Driver Marker */}
|
||||||
|
{driverLocation && (
|
||||||
|
<Marker
|
||||||
|
coordinate={{
|
||||||
|
latitude: driverLocation.latitude,
|
||||||
|
longitude: driverLocation.longitude,
|
||||||
|
}}
|
||||||
|
title="Delivery Partner"
|
||||||
|
anchor={{ x: 0.5, y: 0.5 }}
|
||||||
|
rotation={driverLocation.heading ?? 0}
|
||||||
|
>
|
||||||
|
<View style={styles.driverMarker}>
|
||||||
|
<Text style={styles.driverEmoji}>🛵</Text>
|
||||||
|
</View>
|
||||||
|
</Marker>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Live Routing Polyline */}
|
||||||
|
{showRoute && routeCoords.length > 0 && (
|
||||||
|
<Polyline
|
||||||
|
coordinates={routeCoords}
|
||||||
|
strokeColor={colors.primary}
|
||||||
|
strokeWidth={4}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</MapView>
|
||||||
|
</View>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default TrackingMap;
|
||||||
1
app/components/TrackingMap/index.ts
Normal file
1
app/components/TrackingMap/index.ts
Normal file
@ -0,0 +1 @@
|
|||||||
|
export * from './TrackingMap';
|
||||||
@ -14,3 +14,5 @@ export * from './ratingStars';
|
|||||||
export * from './orderHistoryCard';
|
export * from './orderHistoryCard';
|
||||||
export * from './paymentOption';
|
export * from './paymentOption';
|
||||||
export * from './OrderStatusTimeline';
|
export * from './OrderStatusTimeline';
|
||||||
|
export * from './TrackingMap';
|
||||||
|
|
||||||
|
|||||||
@ -6,7 +6,7 @@ export interface OrderHistoryCardProps {
|
|||||||
orderDate: string;
|
orderDate: string;
|
||||||
status: string;
|
status: string;
|
||||||
total: number;
|
total: number;
|
||||||
onReorder?: () => void;
|
onTrack?: () => void;
|
||||||
onDetails?: () => void;
|
onDetails?: () => void;
|
||||||
items?: OrderItem[];
|
items?: OrderItem[];
|
||||||
}
|
}
|
||||||
|
|||||||
@ -10,7 +10,7 @@ export const OrderHistoryCard: React.FC<OrderHistoryCardProps> = ({
|
|||||||
orderDate,
|
orderDate,
|
||||||
status,
|
status,
|
||||||
total,
|
total,
|
||||||
onReorder,
|
onTrack,
|
||||||
onDetails,
|
onDetails,
|
||||||
items,
|
items,
|
||||||
}) => {
|
}) => {
|
||||||
@ -48,13 +48,13 @@ export const OrderHistoryCard: React.FC<OrderHistoryCardProps> = ({
|
|||||||
<View style={styles.footer}>
|
<View style={styles.footer}>
|
||||||
<Text style={styles.total}>₹{total}</Text>
|
<Text style={styles.total}>₹{total}</Text>
|
||||||
<View style={styles.actions}>
|
<View style={styles.actions}>
|
||||||
{onReorder && (
|
{onTrack && status === 'OUT_FOR_DELIVERY' && (
|
||||||
<TouchableOpacity
|
<TouchableOpacity
|
||||||
style={styles.actionButton}
|
style={styles.actionButton}
|
||||||
onPress={onReorder}
|
onPress={onTrack}
|
||||||
activeOpacity={0.7}
|
activeOpacity={0.7}
|
||||||
>
|
>
|
||||||
<Text style={styles.actionButtonText}>Reorder</Text>
|
<Text style={styles.actionButtonText}>Track Order</Text>
|
||||||
</TouchableOpacity>
|
</TouchableOpacity>
|
||||||
)}
|
)}
|
||||||
{onDetails && (
|
{onDetails && (
|
||||||
|
|||||||
@ -3,6 +3,7 @@ import { useNavigation } from '@react-navigation/native';
|
|||||||
import { StackNavigationProp } from '@react-navigation/stack';
|
import { StackNavigationProp } from '@react-navigation/stack';
|
||||||
import { AuthStackParamList } from 'app/navigation/authStack';
|
import { AuthStackParamList } from 'app/navigation/authStack';
|
||||||
import { loginWithPhone, useAppDispatch } from '@store';
|
import { loginWithPhone, useAppDispatch } from '@store';
|
||||||
|
import { Alert } from 'react-native';
|
||||||
|
|
||||||
type LoginNavProp = StackNavigationProp<AuthStackParamList, 'LoginScreen'>;
|
type LoginNavProp = StackNavigationProp<AuthStackParamList, 'LoginScreen'>;
|
||||||
|
|
||||||
@ -50,8 +51,14 @@ export const useLoginScreen = (): UseLoginScreenResult => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
setError(undefined);
|
setError(undefined);
|
||||||
dispatch(loginWithPhone(mobileNumber));
|
dispatch(loginWithPhone(mobileNumber))
|
||||||
|
.unwrap()
|
||||||
|
.then(() => {
|
||||||
navigation.navigate('OtpScreen', { mobileNumber });
|
navigation.navigate('OtpScreen', { mobileNumber });
|
||||||
|
})
|
||||||
|
.catch(err => {
|
||||||
|
Alert.alert(err);
|
||||||
|
});
|
||||||
}, [mobileNumber, dispatch, navigation]);
|
}, [mobileNumber, dispatch, navigation]);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
|||||||
@ -1,18 +1,19 @@
|
|||||||
import React from 'react';
|
import React, { useCallback } from 'react';
|
||||||
import { View, Text, TouchableOpacity, ScrollView } from 'react-native';
|
import { View, Text, TouchableOpacity, ScrollView } from 'react-native';
|
||||||
import {
|
import {
|
||||||
useNavigation,
|
useNavigation,
|
||||||
CompositeNavigationProp,
|
CompositeNavigationProp,
|
||||||
|
useFocusEffect,
|
||||||
} from '@react-navigation/native';
|
} from '@react-navigation/native';
|
||||||
import { BottomTabNavigationProp } from '@react-navigation/bottom-tabs';
|
import { BottomTabNavigationProp } from '@react-navigation/bottom-tabs';
|
||||||
import { StackNavigationProp } from '@react-navigation/stack';
|
import { StackNavigationProp } from '@react-navigation/stack';
|
||||||
import { getStyles } from './accountScreen.styles';
|
import { getStyles } from './accountScreen.styles';
|
||||||
import { Header, PrimaryButton } from '@components';
|
import { Header, PrimaryButton } from '@components';
|
||||||
import { useAppTheme } from '@theme';
|
import { useAppTheme } from '@theme';
|
||||||
import { logout } from '../../../store/commonreducers/auth';
|
|
||||||
import { AppStackParamList } from '../../../navigation/appStack';
|
import { AppStackParamList } from '../../../navigation/appStack';
|
||||||
import { MainTabParamList } from '../../../navigation/mainTabNavigator';
|
import { MainTabParamList } from '../../../navigation/mainTabNavigator';
|
||||||
import { useAppDispatch, useAppSelector } from '@store';
|
import { logoutUser, useAppDispatch, useAppSelector } from '@store';
|
||||||
|
import { getWalletBalanceThunk } from './thunk';
|
||||||
|
|
||||||
type AccountNavProp = CompositeNavigationProp<
|
type AccountNavProp = CompositeNavigationProp<
|
||||||
BottomTabNavigationProp<MainTabParamList, 'AccountScreen'>,
|
BottomTabNavigationProp<MainTabParamList, 'AccountScreen'>,
|
||||||
@ -44,7 +45,7 @@ const MENU_SECTIONS: MenuSectionData[] = [
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
icon: '💳',
|
icon: '💳',
|
||||||
label: 'Payment Methods',
|
label: 'Wallet',
|
||||||
subLabel: 'Cards, UPI & wallets',
|
subLabel: 'Cards, UPI & wallets',
|
||||||
tint: '#FFF4E5',
|
tint: '#FFF4E5',
|
||||||
},
|
},
|
||||||
@ -87,7 +88,16 @@ export const AccountScreen: React.FC = () => {
|
|||||||
const styles = getStyles(colors);
|
const styles = getStyles(colors);
|
||||||
const dispatch = useAppDispatch();
|
const dispatch = useAppDispatch();
|
||||||
const navigation = useNavigation<AccountNavProp>();
|
const navigation = useNavigation<AccountNavProp>();
|
||||||
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.
|
// TODO: wire these to real selectors once order/wallet state is available.
|
||||||
// const ordersCount = user?.ordersCount ?? 0;
|
// const ordersCount = user?.ordersCount ?? 0;
|
||||||
@ -189,7 +199,10 @@ export const AccountScreen: React.FC = () => {
|
|||||||
|
|
||||||
{/* Logout */}
|
{/* Logout */}
|
||||||
<View style={styles.logoutSection}>
|
<View style={styles.logoutSection}>
|
||||||
<PrimaryButton title="Logout" onPress={() => dispatch(logout())} />
|
<PrimaryButton
|
||||||
|
title="Logout"
|
||||||
|
onPress={() => dispatch(logoutUser())}
|
||||||
|
/>
|
||||||
<Text style={styles.versionText}>App version 1.0.0</Text>
|
<Text style={styles.versionText}>App version 1.0.0</Text>
|
||||||
</View>
|
</View>
|
||||||
</ScrollView>
|
</ScrollView>
|
||||||
|
|||||||
@ -1 +1,3 @@
|
|||||||
export * from './accountScreen';
|
export * from './accountScreen';
|
||||||
|
export * from './thunk';
|
||||||
|
export * from './reducer';
|
||||||
|
|||||||
26
app/features/screens/accountScreen/reducer.ts
Normal file
26
app/features/screens/accountScreen/reducer.ts
Normal file
@ -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;
|
||||||
|
});
|
||||||
|
});
|
||||||
14
app/features/screens/accountScreen/thunk.ts
Normal file
14
app/features/screens/accountScreen/thunk.ts
Normal file
@ -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);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
);
|
||||||
1
app/features/screens/liveTrackingScreen/hooks/index.ts
Normal file
1
app/features/screens/liveTrackingScreen/hooks/index.ts
Normal file
@ -0,0 +1 @@
|
|||||||
|
export * from './useOrderTracking';
|
||||||
@ -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<DriverLocationUpdate | null>(null);
|
||||||
|
const [orderStatus, setOrderStatus] = useState<string | null>(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 };
|
||||||
|
}
|
||||||
@ -1 +1,2 @@
|
|||||||
export * from './liveTrackingScreen';
|
export * from './liveTrackingScreen';
|
||||||
|
export * from './hooks';
|
||||||
|
|||||||
@ -1,13 +1,19 @@
|
|||||||
import React from 'react';
|
import React, { useEffect } from 'react';
|
||||||
import { View, Text } from 'react-native';
|
import { View, Text, ActivityIndicator } from 'react-native';
|
||||||
import { useNavigation, useRoute, RouteProp } from '@react-navigation/native';
|
import { useNavigation, useRoute, RouteProp } from '@react-navigation/native';
|
||||||
import { StackNavigationProp } from '@react-navigation/stack';
|
import { StackNavigationProp } from '@react-navigation/stack';
|
||||||
import { getStyles } from './liveTrackingScreen.styles';
|
import { getStyles } from './liveTrackingScreen.styles';
|
||||||
import { Header, PrimaryButton } from '@components';
|
import { Header, PrimaryButton, TrackingMap } from '@components';
|
||||||
import { useAppTheme } from '@theme';
|
import { useAppTheme } from '@theme';
|
||||||
import { AppStackParamList } from '../../../navigation/appStack';
|
import { AppStackParamList } from '../../../navigation/appStack';
|
||||||
|
import { RootState, useAppDispatch, useAppSelector } from '@store';
|
||||||
|
import { getOrderByIdThunk } from '../checkoutPaymentScreen';
|
||||||
|
import { useOrderTracking } from './hooks';
|
||||||
|
|
||||||
type LiveTrackingNavProp = StackNavigationProp<AppStackParamList, 'LiveTrackingScreen'>;
|
type LiveTrackingNavProp = StackNavigationProp<
|
||||||
|
AppStackParamList,
|
||||||
|
'LiveTrackingScreen'
|
||||||
|
>;
|
||||||
type LiveTrackingRouteProp = RouteProp<AppStackParamList, 'LiveTrackingScreen'>;
|
type LiveTrackingRouteProp = RouteProp<AppStackParamList, 'LiveTrackingScreen'>;
|
||||||
|
|
||||||
export const LiveTrackingScreen: React.FC = () => {
|
export const LiveTrackingScreen: React.FC = () => {
|
||||||
@ -17,23 +23,73 @@ export const LiveTrackingScreen: React.FC = () => {
|
|||||||
const route = useRoute<LiveTrackingRouteProp>();
|
const route = useRoute<LiveTrackingRouteProp>();
|
||||||
const orderId = route.params?.orderId || 'ORD-123456';
|
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 (
|
return (
|
||||||
<View style={styles.container}>
|
<View style={styles.container}>
|
||||||
<Header title="Live Tracking" onBack={() => navigation.goBack()} />
|
<Header title="Live Tracking" onBack={() => navigation.goBack()} />
|
||||||
<View style={styles.mapPlaceholder}>
|
<View
|
||||||
<Text style={styles.mapIcon}>🗺️</Text>
|
style={{ flex: 1, justifyContent: 'center', alignItems: 'center' }}
|
||||||
<Text style={styles.mapText}>Live Map</Text>
|
>
|
||||||
<Text style={styles.mapSubtext}>
|
<ActivityIndicator size="large" color={colors.primary} />
|
||||||
Delivery partner location tracking for {orderId}
|
<Text style={{ marginTop: 8, color: colors.textSecondary }}>
|
||||||
|
Loading tracking details...
|
||||||
</Text>
|
</Text>
|
||||||
<View style={styles.blinkingBadge}>
|
|
||||||
<Text style={styles.blinkingText}>● Live</Text>
|
|
||||||
</View>
|
</View>
|
||||||
</View>
|
</View>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get drop address coordinates from orderDetails
|
||||||
|
const dropCoords = {
|
||||||
|
latitude: orderDetails.dropAddress?.latitude || 12.9352,
|
||||||
|
longitude: orderDetails.dropAddress?.longitude || 77.6245,
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<View style={styles.container}>
|
||||||
|
<Header title="Live Tracking" onBack={() => navigation.goBack()} />
|
||||||
|
<View style={{ flex: 1 }}>
|
||||||
|
<TrackingMap
|
||||||
|
customerDropoff={dropCoords}
|
||||||
|
driverLocation={driverLocation}
|
||||||
|
showRoute={true}
|
||||||
|
/>
|
||||||
|
</View>
|
||||||
<View style={styles.agentSheet}>
|
<View style={styles.agentSheet}>
|
||||||
<PrimaryButton
|
<PrimaryButton
|
||||||
title="Simulate Order Delivered"
|
title="Simulate Order Delivered"
|
||||||
onPress={() => navigation.navigate('OrderDeliveredScreen', { orderId })}
|
onPress={() =>
|
||||||
|
navigation.navigate('OrderDeliveredScreen', { orderId })
|
||||||
|
}
|
||||||
style={{ marginBottom: 16 }}
|
style={{ marginBottom: 16 }}
|
||||||
/>
|
/>
|
||||||
<Text style={styles.agentName}>Rahul Sharma</Text>
|
<Text style={styles.agentName}>Rahul Sharma</Text>
|
||||||
@ -55,3 +111,4 @@ export const LiveTrackingScreen: React.FC = () => {
|
|||||||
</View>
|
</View>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
export default LiveTrackingScreen;
|
||||||
|
|||||||
@ -57,7 +57,7 @@ export const MyOrdersScreen: React.FC = () => {
|
|||||||
const { orderHistory } = useAppSelector(
|
const { orderHistory } = useAppSelector(
|
||||||
(state: RootState) => state.paymentMethods,
|
(state: RootState) => state.paymentMethods,
|
||||||
);
|
);
|
||||||
console.log(orderHistory);
|
// console.log(orderHistory);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
dispatch(getOrderHistoryThunk());
|
dispatch(getOrderHistoryThunk());
|
||||||
@ -114,28 +114,33 @@ export const MyOrdersScreen: React.FC = () => {
|
|||||||
status={item.status}
|
status={item.status}
|
||||||
total={Number(item?.totalAmount) || 0}
|
total={Number(item?.totalAmount) || 0}
|
||||||
items={item.orderItems}
|
items={item.orderItems}
|
||||||
onReorder={() => {
|
onTrack={() => {
|
||||||
navigation.navigate('ProviderDetailsScreen', {
|
if (item?.status === 'OUT_FOR_DELIVERY') {
|
||||||
providerId: 'p1',
|
// navigation.navigate('ProviderDetailsScreen', {
|
||||||
providerName: item?.merchant?.name || '',
|
// providerName: item?.merchant?.name || '',
|
||||||
|
// });
|
||||||
|
// } else {
|
||||||
|
navigation.navigate('LiveTrackingScreen', {
|
||||||
|
orderId: item?.id || '',
|
||||||
});
|
});
|
||||||
|
}
|
||||||
}}
|
}}
|
||||||
onDetails={() => {
|
onDetails={() => {
|
||||||
if (
|
// if (
|
||||||
item?.status === 'PENDING' ||
|
// item?.status === 'PENDING' ||
|
||||||
item?.status === 'CONFIRMED' ||
|
// item?.status === 'CONFIRMED' ||
|
||||||
item?.status === 'PREPARING' ||
|
// item?.status === 'PREPARING' ||
|
||||||
item?.status === 'READY_FOR_PICKUP' ||
|
// item?.status === 'READY_FOR_PICKUP' ||
|
||||||
item?.status === 'OUT_FOR_DELIVERY'
|
// item?.status === 'OUT_FOR_DELIVERY'
|
||||||
) {
|
// ) {
|
||||||
navigation.navigate('OrderDetailsScreen', {
|
navigation.navigate('OrderDetailsScreen', {
|
||||||
orderId: item.id,
|
orderId: item.id,
|
||||||
});
|
});
|
||||||
} else {
|
// } else {
|
||||||
navigation.navigate('OrderDeliveredScreen', {
|
// navigation.navigate('OrderDeliveredScreen', {
|
||||||
orderId: item.id,
|
// orderId: item.id,
|
||||||
});
|
// });
|
||||||
}
|
// }
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@ -47,3 +47,9 @@ export interface CustomerAddress {
|
|||||||
// export type UserStatus = 'ACTIVE' | 'INACTIVE' | 'SUSPENDED';
|
// export type UserStatus = 'ACTIVE' | 'INACTIVE' | 'SUSPENDED';
|
||||||
|
|
||||||
export type AddressLabel = 'Home' | 'Work' | 'Other';
|
export type AddressLabel = 'Home' | 'Work' | 'Other';
|
||||||
|
|
||||||
|
export interface WalletResponse {
|
||||||
|
walletId: string;
|
||||||
|
currency: string;
|
||||||
|
balance: number;
|
||||||
|
}
|
||||||
|
|||||||
@ -20,7 +20,7 @@ import {
|
|||||||
export type AppStackParamList = {
|
export type AppStackParamList = {
|
||||||
MainTabs: NavigatorScreenParams<MainTabParamList> | undefined;
|
MainTabs: NavigatorScreenParams<MainTabParamList> | undefined;
|
||||||
ProviderListScreen: { category?: string } | undefined;
|
ProviderListScreen: { category?: string } | undefined;
|
||||||
ProviderDetailsScreen: { providerId: string; providerName?: string };
|
ProviderDetailsScreen: { providerId?: string; providerName?: string };
|
||||||
CartScreen: undefined;
|
CartScreen: undefined;
|
||||||
CheckoutAddressScreen: undefined;
|
CheckoutAddressScreen: undefined;
|
||||||
CheckoutPaymentScreen: { selectedAddressId?: string } | undefined;
|
CheckoutPaymentScreen: { selectedAddressId?: string } | undefined;
|
||||||
@ -68,10 +68,7 @@ export const AppStack: React.FC = () => {
|
|||||||
/>
|
/>
|
||||||
<Stack.Screen name="HelpSupportScreen" component={HelpSupportScreen} />
|
<Stack.Screen name="HelpSupportScreen" component={HelpSupportScreen} />
|
||||||
<Stack.Screen name="WriteReviewScreen" component={WriteReviewScreen} />
|
<Stack.Screen name="WriteReviewScreen" component={WriteReviewScreen} />
|
||||||
<Stack.Screen
|
<Stack.Screen name="OrderDetailsScreen" component={OrderDetailsScreen} />
|
||||||
name="OrderDetailsScreen"
|
|
||||||
component={OrderDetailsScreen}
|
|
||||||
/>
|
|
||||||
</Stack.Navigator>
|
</Stack.Navigator>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@ -15,7 +15,7 @@ const STORAGE_KEYS = {
|
|||||||
} as const;
|
} as const;
|
||||||
|
|
||||||
// ─── Config ──────────────────────────────────────────────────────────────────
|
// ─── 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 ───────────────────────────────────────────────────────────
|
// ─── Token Helpers ───────────────────────────────────────────────────────────
|
||||||
export const tokenManager = {
|
export const tokenManager = {
|
||||||
@ -131,7 +131,7 @@ axiosInstance.interceptors.response.use(
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Call refresh endpoint (uses a fresh axios call to avoid interceptor loop)
|
// 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,
|
refreshToken,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@ -1,2 +1,3 @@
|
|||||||
export * from './apiClient';
|
export * from './apiClient';
|
||||||
export * from './locationServices';
|
export * from './locationServices';
|
||||||
|
export * from './socketService';
|
||||||
|
|||||||
@ -227,3 +227,134 @@ export const getCurrentLocationWithAddress =
|
|||||||
|
|
||||||
return { coords, address };
|
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<LatLng[]> => {
|
||||||
|
// 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
|
||||||
|
};
|
||||||
|
|
||||||
|
|||||||
94
app/services/socketService.ts
Normal file
94
app/services/socketService.ts
Normal file
@ -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;
|
||||||
@ -2,3 +2,4 @@ export * from './auth';
|
|||||||
export * from './cart';
|
export * from './cart';
|
||||||
export * from './order';
|
export * from './order';
|
||||||
export * from './customerProfile';
|
export * from './customerProfile';
|
||||||
|
export * from './offer';
|
||||||
|
|||||||
@ -11,6 +11,7 @@ import providerDetailsReducer from '@features/screens/providerDetailsScreen/redu
|
|||||||
import paymentMethodsReducer from '@features/screens/checkoutPaymentScreen/reducer';
|
import paymentMethodsReducer from '@features/screens/checkoutPaymentScreen/reducer';
|
||||||
import customerProfileReducer from './commonreducers/customerProfile/reducer';
|
import customerProfileReducer from './commonreducers/customerProfile/reducer';
|
||||||
import { offerReducer } from './commonreducers/offer';
|
import { offerReducer } from './commonreducers/offer';
|
||||||
|
import { accountReducer } from '@features/screens';
|
||||||
|
|
||||||
const rootReducer = combineReducers({
|
const rootReducer = combineReducers({
|
||||||
auth: authReducer,
|
auth: authReducer,
|
||||||
@ -24,6 +25,7 @@ const rootReducer = combineReducers({
|
|||||||
paymentMethods: paymentMethodsReducer,
|
paymentMethods: paymentMethodsReducer,
|
||||||
customerProfile: customerProfileReducer,
|
customerProfile: customerProfileReducer,
|
||||||
offer: offerReducer,
|
offer: offerReducer,
|
||||||
|
account: accountReducer,
|
||||||
});
|
});
|
||||||
|
|
||||||
export type RootState = ReturnType<typeof rootReducer>;
|
export type RootState = ReturnType<typeof rootReducer>;
|
||||||
|
|||||||
@ -1,5 +1,5 @@
|
|||||||
export const getFullUrl = (url?: string) => {
|
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 '';
|
if (!url) return '';
|
||||||
return url.startsWith('/') ? `${BASE_URL}${url}` : url;
|
return url.startsWith('/') ? `${BASE_URL}${url}` : url;
|
||||||
};
|
};
|
||||||
|
|||||||
@ -34,7 +34,8 @@
|
|||||||
"react-redux": "^9.3.0",
|
"react-redux": "^9.3.0",
|
||||||
"reactotron-react-native": "^5.2.0",
|
"reactotron-react-native": "^5.2.0",
|
||||||
"redux-persist": "^6.0.0",
|
"redux-persist": "^6.0.0",
|
||||||
"redux-thunk": "^3.1.0"
|
"redux-thunk": "^3.1.0",
|
||||||
|
"socket.io-client": "^4.8.3"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@babel/core": "^7.25.2",
|
"@babel/core": "^7.25.2",
|
||||||
|
|||||||
51
yarn.lock
51
yarn.lock
@ -1837,6 +1837,11 @@
|
|||||||
dependencies:
|
dependencies:
|
||||||
"@sinonjs/commons" "^3.0.0"
|
"@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":
|
"@standard-schema/spec@^1.0.0":
|
||||||
version "1.1.0"
|
version "1.1.0"
|
||||||
resolved "https://registry.yarnpkg.com/@standard-schema/spec/-/spec-1.1.0.tgz#a79b55dbaf8604812f52d140b2c9ab41bc150bb8"
|
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:
|
dependencies:
|
||||||
ms "2.0.0"
|
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"
|
version "4.4.3"
|
||||||
resolved "https://registry.yarnpkg.com/debug/-/debug-4.4.3.tgz#c6ae432d9bd9662582fce08709b038c58e9e3d6a"
|
resolved "https://registry.yarnpkg.com/debug/-/debug-4.4.3.tgz#c6ae432d9bd9662582fce08709b038c58e9e3d6a"
|
||||||
integrity sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==
|
integrity sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==
|
||||||
@ -3094,6 +3099,22 @@ encodeurl@~2.0.0:
|
|||||||
resolved "https://registry.yarnpkg.com/encodeurl/-/encodeurl-2.0.0.tgz#7b8ea898077d7e409d3ac45474ea38eaf0857a58"
|
resolved "https://registry.yarnpkg.com/encodeurl/-/encodeurl-2.0.0.tgz#7b8ea898077d7e409d3ac45474ea38eaf0857a58"
|
||||||
integrity sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==
|
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:
|
entities@^4.2.0:
|
||||||
version "4.5.0"
|
version "4.5.0"
|
||||||
resolved "https://registry.yarnpkg.com/entities/-/entities-4.5.0.tgz#5d268ea5e7113ec74c4d033b79ea5a35a488fb48"
|
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"
|
astral-regex "^1.0.0"
|
||||||
is-fullwidth-code-point "^2.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:
|
source-map-support@0.5.13:
|
||||||
version "0.5.13"
|
version "0.5.13"
|
||||||
resolved "https://registry.yarnpkg.com/source-map-support/-/source-map-support-0.5.13.tgz#31b24a9c2e73c2de85066c0feb7d44767ed52932"
|
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"
|
resolved "https://registry.yarnpkg.com/ws/-/ws-7.5.11.tgz#9460daf1812bb81a423c5b9eac746941a86310fa"
|
||||||
integrity sha512-zS54Oen9bITtp7kp2XM3AydrCIq1D+HwJOuH+c+e4LfpL/lotP5osijd+UoMnxwAam1GN8R4KtLAyIrIcBNpiA==
|
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:
|
y18n@^4.0.0:
|
||||||
version "4.0.3"
|
version "4.0.3"
|
||||||
resolved "https://registry.yarnpkg.com/y18n/-/y18n-4.0.3.tgz#b5f259c82cd6e336921efd7bfd8bf560de9eeedf"
|
resolved "https://registry.yarnpkg.com/y18n/-/y18n-4.0.3.tgz#b5f259c82cd6e336921efd7bfd8bf560de9eeedf"
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user