feat: implement core delivery partner dashboard with live location tracking, socket connectivity, and availability status toggling

This commit is contained in:
Tamojit Biswas 2026-07-13 18:35:17 +05:30
parent a6c713ae8c
commit 98e28742de
37 changed files with 2443 additions and 1272 deletions

View File

@ -1,3 +1,4 @@
import React, { useEffect, useRef } from 'react';
import { StatusBar, StyleSheet, useColorScheme, View } from 'react-native';
import {
SafeAreaProvider,
@ -5,10 +6,11 @@ import {
} from 'react-native-safe-area-context';
import RootNavigator from '@navigation/rootNavigator';
import { Provider } from 'react-redux';
// import { store } from '@store';
import { useAppTheme } from '@theme';
import { persistor, store } from '@store';
import { persistor, store, useAppSelector } from '@store';
import { PersistGate } from 'redux-persist/integration/react';
import { socketService } from '@services';
import { checkPendingOfferThunk } from '@store/commonReducers/delivery';
function App() {
const isDarkMode = useColorScheme() === 'dark';
@ -28,6 +30,27 @@ function App() {
function AppContent() {
const safeAreaInsets = useSafeAreaInsets();
const { colors } = useAppTheme();
const accessToken = useAppSelector(state => state.auth.accessToken);
const prevTokenRef = useRef<string | null>(null);
// Bootstrap the socket service once with the store reference.
// The navigationRef is managed inside RootNavigator, so we pass null here;
// it will be injected from rootNavigator.tsx.
useEffect(() => {
socketService.bootstrap(store, null);
}, []);
// Connect / disconnect the socket when the auth token changes.
useEffect(() => {
if (accessToken && accessToken !== prevTokenRef.current) {
socketService.connect(accessToken);
// Check for any delivery offer the server held while the app was closed
store.dispatch(checkPendingOfferThunk());
} else if (!accessToken && prevTokenRef.current) {
socketService.disconnect();
}
prevTokenRef.current = accessToken;
}, [accessToken]);
return (
<View

View File

@ -1,4 +1,9 @@
import { ToggleStatusPayload, ToggleStatusResponse } from '@interfaces';
import {
DeliveryPartnerLocationRequest,
DeliveryPartnerLocationResponse,
ToggleStatusPayload,
ToggleStatusResponse,
} from '@interfaces';
import { apiClient } from '@services';
export const toggleStatus = async (
@ -10,3 +15,13 @@ export const toggleStatus = async (
);
return response;
};
export const deliveryPartnerLocation = async (
payload: DeliveryPartnerLocationRequest,
): Promise<DeliveryPartnerLocationResponse> => {
const response = await apiClient.post<DeliveryPartnerLocationResponse>(
'/delivery-partners/location',
payload,
);
return response;
};

40
app/api/deliveryApi.ts Normal file
View File

@ -0,0 +1,40 @@
import {
DeliveryAction,
RespondToOfferResponse,
PendingOfferResponse,
ActiveOrdersResponse,
} from '@interfaces';
import { apiClient } from '@services';
/**
* POST /delivery-partners/deliveries/{id}/respond
* Accept or reject a delivery offer.
*/
export const respondToOffer = async (
deliveryId: string,
action: DeliveryAction,
): Promise<RespondToOfferResponse> => {
const response = await apiClient.post<RespondToOfferResponse>(
`/delivery-partners/deliveries/${deliveryId}/respond`,
{ action },
);
return response;
};
/**
* GET /delivery-partners/deliveries/pending-offer
* Check if there is a pending offer for the current partner (e.g. after app restart).
*/
export const getPendingOffer = async (): Promise<PendingOfferResponse> => {
const response = await apiClient.get<PendingOfferResponse>(
'/delivery-partners/deliveries/pending-offer',
);
return response;
};
export const getActiveDeliveries = async (): Promise<ActiveOrdersResponse> => {
const response = await apiClient.get<ActiveOrdersResponse>(
'/delivery-partners/deliveries/active',
);
return response;
};

View File

@ -2,3 +2,4 @@ export * from './authApi';
export * from './onBoardApi';
export * from './kycApi';
export * from './dashboardApi';
export * from './deliveryApi';

View File

@ -11,8 +11,8 @@ interface OrderCardProps {
pickupAddress: string;
dropName: string;
dropAddress: string;
amount: number;
distance: number;
amount: string;
distance: string;
itemCount?: number;
paymentType?: string;
onPress?: () => void;

View File

@ -12,15 +12,17 @@ import { getStyles } from './arrivedAtStoreScreen.styles';
export const ArrivedAtStoreScreen: React.FC = () => {
const { colors } = useAppTheme();
const styles = getStyles(colors);
const navigation = useNavigation<NativeStackNavigationProp<AppStackParamList>>();
const navigation =
useNavigation<NativeStackNavigationProp<AppStackParamList>>();
const activeJob = useAppSelector((state) => state.job.activeJob);
// const activeJob = useAppSelector(state => state.job.activeJob);
const { activeDeliveries } = useAppSelector(state => state.deliveries);
const handleConfirmArrival = () => {
navigation.navigate(RouteNames.ConfirmPickup);
};
if (!activeJob) return null;
// if (!activeJob) return null;
return (
<View style={styles.container}>
@ -32,8 +34,12 @@ export const ArrivedAtStoreScreen: React.FC = () => {
{/* Arrived Card */}
<View style={styles.bottomCard}>
<Text style={styles.cardTitle}>You've arrived</Text>
<Text style={styles.storeName}>{activeJob.pickupName}</Text>
<Text style={styles.storeAddress}>{activeJob.pickupAddress}</Text>
<Text style={styles.storeName}>
{activeDeliveries?.order?.pickupAddress?.city}
</Text>
<Text style={styles.storeAddress}>
{activeDeliveries?.order?.pickupAddress?.addressLine1}
</Text>
<PrimaryButton
title="Confirm Arrival"

View File

@ -14,9 +14,10 @@ export const ConfirmPickupScreen: React.FC = () => {
const { colors } = useAppTheme();
const styles = getStyles(colors);
const dispatch = useAppDispatch();
const navigation = useNavigation<NativeStackNavigationProp<AppStackParamList>>();
const navigation =
useNavigation<NativeStackNavigationProp<AppStackParamList>>();
const activeJob = useAppSelector((state) => state.job.activeJob);
const activeJob = useAppSelector(state => state.job.activeJob);
// Togglable checklist for food/merchandise items
const [checkedItems, setCheckedItems] = useState<{ [key: number]: boolean }>({
@ -30,7 +31,7 @@ export const ConfirmPickupScreen: React.FC = () => {
];
const handleToggleCheck = (id: number) => {
setCheckedItems((prev) => ({
setCheckedItems(prev => ({
...prev,
[id]: !prev[id],
}));
@ -38,9 +39,12 @@ export const ConfirmPickupScreen: React.FC = () => {
const handlePickedUp = () => {
// Ensure all items are checked before picking up
const allChecked = items.every((item) => checkedItems[item.id]);
const allChecked = items.every(item => checkedItems[item.id]);
if (!allChecked) {
Alert.alert('Incomplete Verification', 'Please confirm that you have checked all items before picking up.');
Alert.alert(
'Incomplete Verification',
'Please confirm that you have checked all items before picking up.',
);
return;
}
@ -49,10 +53,13 @@ export const ConfirmPickupScreen: React.FC = () => {
};
const handleReportIssue = () => {
Alert.alert('Report Issue', 'Helpdesk notified. Support agent will contact you shortly.');
Alert.alert(
'Report Issue',
'Helpdesk notified. Support agent will contact you shortly.',
);
};
if (!activeJob) return null;
// if (!activeJob) return null;
return (
<View style={styles.container}>
@ -62,10 +69,13 @@ export const ConfirmPickupScreen: React.FC = () => {
onBack={() => navigation.goBack()}
/>
<ScrollView contentContainerStyle={styles.scrollContainer} showsVerticalScrollIndicator={false}>
<ScrollView
contentContainerStyle={styles.scrollContainer}
showsVerticalScrollIndicator={false}
>
{/* Order ID Banner */}
<View style={styles.orderHeader}>
<Text style={styles.orderIdText}>Order ID: {activeJob.orderId}</Text>
<Text style={styles.orderIdText}>Order ID: {activeJob?.orderId}</Text>
<View style={styles.paymentTag}>
<Text style={styles.paymentTagText}>ONLINE PAID</Text>
</View>
@ -74,21 +84,28 @@ export const ConfirmPickupScreen: React.FC = () => {
{/* Merchant Info */}
<View style={styles.merchantInfoCard}>
<Text style={styles.merchantLabel}>Pickup Store</Text>
<Text style={styles.merchantName}>{activeJob.pickupName}</Text>
<Text style={styles.merchantName}>{activeJob?.pickupName}</Text>
</View>
{/* Items Checklist */}
<View style={styles.section}>
<Text style={styles.sectionTitle}>Verify Items ({items.length})</Text>
{items.map((item) => (
{items.map(item => (
<TouchableOpacity
key={item.id}
style={styles.itemRow}
activeOpacity={0.8}
onPress={() => handleToggleCheck(item.id)}
>
<View style={[styles.checkbox, checkedItems[item.id] && styles.checkboxChecked]}>
{checkedItems[item.id] && <Text style={styles.checkSign}></Text>}
<View
style={[
styles.checkbox,
checkedItems[item.id] && styles.checkboxChecked,
]}
>
{checkedItems[item.id] && (
<Text style={styles.checkSign}></Text>
)}
</View>
<Text style={styles.itemNameText}>{item.name}</Text>
<Text style={styles.itemQtyText}>x{item.qty}</Text>

View File

@ -1,4 +1,4 @@
import React, { useEffect, useRef } from 'react';
import React, { useCallback, useEffect, useRef } from 'react';
import {
View,
Text,
@ -6,21 +6,25 @@ import {
TouchableOpacity,
Alert,
Switch,
InteractionManager,
} from 'react-native';
import { useAppTheme, borderRadius, spacing, shadow, typography } from '@theme';
import { PrimaryButton } from '@components';
import { BellIcon, PersonIcon, WalletIcon, ClipboardIcon } from '@icons';
import { useAppDispatch, useAppSelector } from '@store';
import { setOnlineStatus } from '@store/commonReducers/auth';
import { setNewJobOffer } from '@store/commonReducers/job';
import { mockJobOffer } from '@store/mockData/mockJobs';
import { useNavigation } from '@react-navigation/native';
import { NativeStackNavigationProp } from '@react-navigation/native-stack';
import { AppStackParamList } from '@navigation/navigationTypes';
import { RouteNames, JobStatus } from '@utils/constants';
import { formatCurrency } from '@utils/formatters';
import { getStyles } from './dashboardScreen.styles';
import { toggleStatus } from './thunk';
import { toggleStatus, updateDeliveryPartnerLocation } from './thunk';
import {
getCurrentLocationWithAddress,
showPermissionDeniedAlert,
socketService,
} from '@services';
export const DashboardScreen: React.FC = () => {
const { colors } = useAppTheme();
@ -34,6 +38,7 @@ export const DashboardScreen: React.FC = () => {
const { availabiltyStatus, loading: toggleLoading } = useAppSelector(
state => state.dashboard,
);
const { user } = useAppSelector(state => state.auth);
const authIsOnline = useAppSelector(state => state.auth.isOnline);
const isOnline = availabiltyStatus
? availabiltyStatus.availabilityStatus === 'ONLINE'
@ -42,28 +47,57 @@ export const DashboardScreen: React.FC = () => {
useAppSelector(state => state.earnings);
const { jobStatus, activeJob } = useAppSelector(state => state.job);
const timerRef = useRef<ReturnType<typeof setTimeout> | null>(null);
const locationIntervalRef = useRef<ReturnType<typeof setInterval> | null>(
null,
);
const sendLocationUpdate = useCallback(async () => {
try {
const result = await getCurrentLocationWithAddress();
const coords = result.coords;
dispatch(
updateDeliveryPartnerLocation({
latitude: coords.latitude,
longitude: coords.longitude,
accuracyMeters: coords.accuracy ?? 0,
speedKph: coords.speed ? coords.speed * 3.6 : 0, // convert m/s -> km/h if needed
heading: coords.heading ?? 0,
}),
);
} catch (err) {
console.log('--------location update error------', err);
}
}, [dispatch]);
// Background simulation: when online and idle, trigger new order offer after 12 seconds
useEffect(() => {
if (isOnline && jobStatus === JobStatus.Idle) {
timerRef.current = setTimeout(() => {
dispatch(setNewJobOffer(mockJobOffer));
navigation.navigate(RouteNames.NewJobRequest);
}, 12000);
if (isOnline) {
// Fire one immediately on going online, then every 15s
sendLocationUpdate();
locationIntervalRef.current = setInterval(sendLocationUpdate, 15000);
} else {
if (timerRef.current) {
clearTimeout(timerRef.current);
timerRef.current = null;
if (locationIntervalRef.current) {
clearInterval(locationIntervalRef.current);
locationIntervalRef.current = null;
}
}
return () => {
if (timerRef.current) {
clearTimeout(timerRef.current);
if (locationIntervalRef.current) {
clearInterval(locationIntervalRef.current);
locationIntervalRef.current = null;
}
};
}, [isOnline, jobStatus, dispatch, navigation]);
}, [isOnline, sendLocationUpdate]);
// Notify the socket server when online/offline status changes
useEffect(() => {
if (isOnline) {
socketService.goOnline();
} else {
socketService.goOffline();
}
}, [isOnline]);
const handleToggleOnline = () => {
const newStatus = isOnline ? 'OFFLINE' : 'ONLINE';
@ -88,22 +122,6 @@ export const DashboardScreen: React.FC = () => {
});
};
const handleSimulateOffer = () => {
if (!isOnline) {
Alert.alert('Go Online', 'Please go online to receive order offers.');
return;
}
if (jobStatus !== JobStatus.Idle) {
Alert.alert(
'Active Delivery',
'You already have an active job offer or order.',
);
return;
}
dispatch(setNewJobOffer(mockJobOffer));
navigation.navigate(RouteNames.NewJobRequest);
};
const handleActiveJobBannerPress = () => {
// Navigate back to the screen corresponding to the current job step
switch (jobStatus) {
@ -151,7 +169,7 @@ export const DashboardScreen: React.FC = () => {
<View style={styles.header}>
<View>
<Text style={styles.greetingText}>Good Morning </Text>
<Text style={styles.nameText}>Rahul Kumar</Text>
<Text style={styles.nameText}>{user?.name}</Text>
</View>
<View style={styles.toggleWrapper}>
@ -281,17 +299,6 @@ export const DashboardScreen: React.FC = () => {
</View>
</View>
{/* Developer Simulation trigger */}
<TouchableOpacity
style={styles.simulateBtn}
activeOpacity={0.8}
onPress={handleSimulateOffer}
>
<Text style={styles.simulateBtnText}>
Simulate Immediate Job Offer
</Text>
</TouchableOpacity>
<PrimaryButton
title="Go Offline"
onPress={handleToggleOnline}

View File

@ -1,17 +1,24 @@
import { ToggleStatusResponse } from '@interfaces';
import {
DeliveryPartnerLocationResponse,
ToggleStatusResponse,
} from '@interfaces';
import { createReducer } from '@reduxjs/toolkit';
import { toggleStatus } from './thunk';
import { toggleStatus, updateDeliveryPartnerLocation } from './thunk';
export interface ToggleState {
loading: boolean;
error: string | null;
availabiltyStatus: ToggleStatusResponse | null;
deliveryPartnerLocation: DeliveryPartnerLocationResponse | null;
deliveryLocError: string | null;
}
export const initialState: ToggleState = {
loading: false,
error: null,
availabiltyStatus: null,
deliveryPartnerLocation: null,
deliveryLocError: null,
};
export const dashBoardReducer = createReducer(initialState, builder => {
@ -27,5 +34,11 @@ export const dashBoardReducer = createReducer(initialState, builder => {
.addCase(toggleStatus.rejected, (state, action) => {
state.loading = false;
state.error = action.payload as string;
})
.addCase(updateDeliveryPartnerLocation.fulfilled, (state, action) => {
state.deliveryPartnerLocation = action.payload;
})
.addCase(updateDeliveryPartnerLocation.rejected, (state, action) => {
state.deliveryLocError = action.payload as string;
});
});

View File

@ -1,6 +1,9 @@
import { createAsyncThunk } from '@reduxjs/toolkit';
import { toggleStatus as toggleStatusApi } from '@api';
import { ToggleStatusPayload } from '@interfaces';
import { deliveryPartnerLocation, toggleStatus as toggleStatusApi } from '@api';
import {
DeliveryPartnerLocationRequest,
ToggleStatusPayload,
} from '@interfaces';
export const toggleStatus = createAsyncThunk(
'dashboard/toggleStatus',
@ -13,3 +16,15 @@ export const toggleStatus = createAsyncThunk(
}
},
);
export const updateDeliveryPartnerLocation = createAsyncThunk(
'dashboard/updateDeliveryPartnerLocation',
async (payload: DeliveryPartnerLocationRequest, { rejectWithValue }) => {
try {
const response = await deliveryPartnerLocation(payload);
return response;
} catch (error: unknown) {
return rejectWithValue(error);
}
},
);

View File

@ -1,2 +1,4 @@
export { default } from './jobsScreen';
export * from './jobsScreen';
export * from './thunk';
export * from './reducer';

View File

@ -1,17 +1,30 @@
import React, { useState } from 'react';
import React, { useEffect, useState } from 'react';
import { View, Text, ScrollView, TouchableOpacity } from 'react-native';
import { useAppTheme } from '@theme';
import { OrderCard, EmptyState } from '@components';
import { useAppSelector } from '@store';
import { useAppDispatch, useAppSelector } from '@store';
import { ClipboardIcon } from '@icons';
import { getStyles } from './jobsScreen.styles';
import { getAllActiveDeliveries } from './thunk';
import { useNavigation } from '@react-navigation/native';
import { NativeStackNavigationProp } from '@react-navigation/native-stack';
import { AppStackParamList } from '@navigation/navigationTypes';
import { RouteNames } from '@utils/constants';
export const JobsScreen: React.FC = () => {
const navigation =
useNavigation<NativeStackNavigationProp<AppStackParamList>>();
const { colors } = useAppTheme();
const styles = getStyles(colors);
const dispatch = useAppDispatch();
const [activeTab, setActiveTab] = useState<'active' | 'history'>('active');
const { activeJob, jobHistory } = useAppSelector((state) => state.job);
const { activeJob, jobHistory } = useAppSelector(state => state.job);
const { activeDeliveries } = useAppSelector(state => state.deliveries);
useEffect(() => {
dispatch(getAllActiveDeliveries());
}, [dispatch]);
// Default history mock jobs
const defaultHistory = [
@ -20,8 +33,8 @@ export const JobsScreen: React.FC = () => {
pickupAddress: 'MG Road, Bengaluru',
dropName: 'Rohit Sharma',
dropAddress: 'Koramangala 4th Block, Bengaluru',
amount: 78.0,
distance: 1.2,
amount: '78.0',
distance: '1.2',
itemCount: 2,
paymentType: 'online',
},
@ -30,8 +43,8 @@ export const JobsScreen: React.FC = () => {
pickupAddress: 'Indiranagar, Bengaluru',
dropName: 'Aishwarya Sen',
dropAddress: 'Domlur Stage 2, Bengaluru',
amount: 95.0,
distance: 3.8,
amount: '95.0',
distance: '3.8',
itemCount: 3,
paymentType: 'cod',
},
@ -40,8 +53,8 @@ export const JobsScreen: React.FC = () => {
pickupAddress: 'Koramangala 5th Block',
dropName: 'Sameer Khan',
dropAddress: 'HSR Layout Sector 3, Bengaluru',
amount: 110.0,
distance: 4.5,
amount: '110.0',
distance: '4.5',
itemCount: 1,
paymentType: 'online',
},
@ -60,7 +73,12 @@ export const JobsScreen: React.FC = () => {
activeOpacity={0.8}
onPress={() => setActiveTab('active')}
>
<Text style={[styles.tabText, activeTab === 'active' && styles.tabTextActive]}>
<Text
style={[
styles.tabText,
activeTab === 'active' && styles.tabTextActive,
]}
>
Active / Upcoming
</Text>
</TouchableOpacity>
@ -70,32 +88,49 @@ export const JobsScreen: React.FC = () => {
activeOpacity={0.8}
onPress={() => setActiveTab('history')}
>
<Text style={[styles.tabText, activeTab === 'history' && styles.tabTextActive]}>
<Text
style={[
styles.tabText,
activeTab === 'history' && styles.tabTextActive,
]}
>
History
</Text>
</TouchableOpacity>
</View>
<ScrollView contentContainerStyle={styles.listContainer} showsVerticalScrollIndicator={false}>
<ScrollView
contentContainerStyle={styles.listContainer}
showsVerticalScrollIndicator={false}
>
{activeTab === 'active' ? (
// Active job tab
activeJob ? (
activeDeliveries ? (
<View style={styles.cardWrapper}>
<OrderCard
pickupName={activeJob.pickupName}
pickupAddress={activeJob.pickupAddress}
dropName={activeJob.dropName}
dropAddress={activeJob.dropAddress}
amount={activeJob.amount}
distance={activeJob.distance}
itemCount={activeJob.itemCount}
paymentType={activeJob.paymentType}
pickupName={activeDeliveries?.order?.pickupAddress?.city}
pickupAddress={
activeDeliveries?.order?.pickupAddress?.addressLine1
}
dropName={activeDeliveries?.order?.dropAddress?.city}
dropAddress={activeDeliveries?.order?.dropAddress?.addressLine1}
amount={activeDeliveries?.order?.totalAmount}
distance={activeDeliveries?.estimatedDistanceKm}
itemCount={activeDeliveries?.order?.orderItems?.length}
paymentType={activeDeliveries?.order?.paymentMethod}
onPress={() =>
navigation.navigate(RouteNames.ArrivedAtStore, {
jobId: activeDeliveries?.orderId,
})
}
/>
</View>
) : (
<View style={styles.emptyStateContainer}>
<ClipboardIcon size={48} color={colors.placeholder} />
<Text style={styles.emptyStateText}>No active deliveries right now</Text>
<Text style={styles.emptyStateText}>
No active deliveries right now
</Text>
</View>
)
) : (

View File

@ -0,0 +1,31 @@
import { ActiveOrdersResponse } from '@interfaces';
import { createReducer } from '@reduxjs/toolkit';
import { getAllActiveDeliveries } from './thunk';
export interface ActiveDeliveriesState {
activeDeliveries: ActiveOrdersResponse | null;
loading: boolean;
error: string | null;
}
const initialState: ActiveDeliveriesState = {
activeDeliveries: null,
loading: false,
error: null,
};
export const activeDeliveriesReducer = createReducer(initialState, builder => {
builder
.addCase(getAllActiveDeliveries.pending, state => {
state.loading = true;
})
.addCase(getAllActiveDeliveries.fulfilled, (state, action) => {
state.loading = false;
state.activeDeliveries = action.payload;
// console.log(action.payload);
})
.addCase(getAllActiveDeliveries.rejected, (state, action) => {
state.loading = false;
state.error = action.payload as string;
});
});

View File

@ -0,0 +1,14 @@
import { getActiveDeliveries } from '@api';
import { createAsyncThunk } from '@reduxjs/toolkit';
export const getAllActiveDeliveries = createAsyncThunk(
'jobs/getAllActiveDeliveries',
async (_, { rejectWithValue }) => {
try {
const response = await getActiveDeliveries();
return response;
} catch (error) {
return rejectWithValue(error);
}
},
);

View File

@ -1,6 +1,8 @@
import { StyleSheet } from 'react-native';
import { typography, spacing, borderRadius, shadow } from '@theme';
const TIMER_SIZE = 56;
export const getStyles = (colors: any) =>
StyleSheet.create({
container: {
@ -27,21 +29,45 @@ export const getStyles = (colors: any) =>
fontWeight: typography.fontWeight.bold,
color: colors.text,
},
timerContainer: {
width: 44,
height: 44,
borderRadius: 22,
// ── Circular Timer (border-based) ─────────────────────────────────────
timerWrapper: {
width: TIMER_SIZE,
height: TIMER_SIZE,
borderRadius: TIMER_SIZE / 2,
borderWidth: 3,
borderColor: colors.primary,
justifyContent: 'center',
alignItems: 'center',
backgroundColor: colors.primaryLight,
},
timerText: {
fontSize: typography.fontSize.sm,
fontSize: typography.fontSize.md,
fontWeight: typography.fontWeight.bold,
color: colors.primary,
},
// ── Earnings Badge ────────────────────────────────────────────────────
earningsBadge: {
borderRadius: borderRadius.md,
paddingVertical: spacing.md,
paddingHorizontal: spacing.lg,
marginBottom: spacing.lg,
alignItems: 'center',
overflow: 'hidden',
},
earningsBadgeLabel: {
fontSize: 10,
fontWeight: 'bold' as const,
textTransform: 'uppercase' as const,
color: 'rgba(255,255,255,0.75)',
marginBottom: 2,
},
earningsBadgeValue: {
fontSize: 28,
fontWeight: typography.fontWeight.bold,
color: '#fff',
},
// ── Route Card ────────────────────────────────────────────────────────
routeContainer: {
backgroundColor: colors.inputBg,
borderRadius: borderRadius.md,
@ -53,6 +79,8 @@ export const getStyles = (colors: any) =>
spacer: {
height: spacing.md,
},
// ── Details Row ───────────────────────────────────────────────────────
detailsRow: {
flexDirection: 'row',
justifyContent: 'space-between',
@ -84,6 +112,8 @@ export const getStyles = (colors: any) =>
height: 28,
backgroundColor: colors.border,
},
// ── Buttons ───────────────────────────────────────────────────────────
btnRow: {
flexDirection: 'row',
gap: 12,

View File

@ -1,9 +1,12 @@
import React, { useState, useEffect } from 'react';
import { View, Text, Alert } from 'react-native';
import React, { useState, useEffect, useRef } from 'react';
import { View, Text, Animated, Easing } from 'react-native';
import { useAppTheme } from '@theme';
import { PrimaryButton, LocationRow } from '@components';
import { useAppDispatch, useAppSelector } from '@store';
import { acceptJobThunk, rejectJobThunk } from '@store/commonReducers/job';
import {
respondToOfferThunk,
clearDeliveryOffer,
} from '@store/commonReducers/delivery';
import { useNavigation } from '@react-navigation/native';
import { NativeStackNavigationProp } from '@react-navigation/native-stack';
import { AppStackParamList } from '@navigation/navigationTypes';
@ -11,101 +14,188 @@ import { RouteNames } from '@utils/constants';
import { formatCurrency } from '@utils/formatters';
import { getStyles } from './newJobRequestScreen.styles';
const TOTAL_SECONDS = 15;
export const NewJobRequestScreen: React.FC = () => {
const { colors } = useAppTheme();
const styles = getStyles(colors);
const dispatch = useAppDispatch();
const navigation = useNavigation<NativeStackNavigationProp<AppStackParamList>>();
const navigation =
useNavigation<NativeStackNavigationProp<AppStackParamList>>();
const newJobOffer = useAppSelector((state) => state.job.newJobOffer);
const [secondsLeft, setSecondsLeft] = useState(15);
// Read from the socket-driven delivery slice
const currentOffer = useAppSelector(state => state.delivery.currentOffer);
const respondLoading = useAppSelector(
state => state.delivery.respondLoading,
);
const [secondsLeft, setSecondsLeft] = useState(TOTAL_SECONDS);
// Animated values
const pulseAnim = useRef(new Animated.Value(1)).current;
// ── Countdown timer ──────────────────────────────────────────────────────
useEffect(() => {
if (secondsLeft <= 0) {
handleReject();
return;
}
const timer = setInterval(() => {
setSecondsLeft((prev) => prev - 1);
setSecondsLeft(prev => prev - 1);
}, 1000);
return () => clearInterval(timer);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [secondsLeft]);
const handleAccept = () => {
if (newJobOffer?.jobId) {
dispatch(acceptJobThunk(newJobOffer.jobId));
// ── Pulse animation on Accept button when < 5 s remain ──────────────────
useEffect(() => {
if (secondsLeft <= 5 && secondsLeft > 0) {
Animated.loop(
Animated.sequence([
Animated.timing(pulseAnim, {
toValue: 1.06,
duration: 400,
easing: Easing.inOut(Easing.ease),
useNativeDriver: true,
}),
Animated.timing(pulseAnim, {
toValue: 1,
duration: 400,
easing: Easing.inOut(Easing.ease),
useNativeDriver: true,
}),
]),
).start();
} else {
pulseAnim.setValue(1);
}
Alert.alert('Job Accepted', 'Heading to Cafe Coffee Day pickup location.', [
{
text: 'OK',
onPress: () => navigation.navigate(RouteNames.OrderAccepted),
},
]);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [secondsLeft <= 5]);
// ── Handlers ─────────────────────────────────────────────────────────────
const handleAccept = () => {
if (!currentOffer) return;
dispatch(
respondToOfferThunk({
deliveryId: currentOffer.deliveryId,
action: 'ACCEPT',
}),
)
.unwrap()
.then(() => {
navigation.navigate(RouteNames.OrderAccepted);
})
.catch(() => {
// stay on screen — respondError will be set in redux
});
};
const handleReject = () => {
if (newJobOffer?.jobId) {
dispatch(rejectJobThunk(newJobOffer.jobId));
if (currentOffer) {
dispatch(
respondToOfferThunk({
deliveryId: currentOffer.deliveryId,
action: 'REJECT',
}),
);
}
navigation.navigate(RouteNames.BottomTabs, { screen: RouteNames.Dashboard });
dispatch(clearDeliveryOffer());
navigation.navigate(RouteNames.BottomTabs, {
screen: RouteNames.Dashboard,
});
};
if (!newJobOffer) return null;
if (!currentOffer) return null;
// Timer text color: warning when < 5s
const timerColor = secondsLeft <= 5 ? colors.error : colors.primary;
const timerBorderColor =
secondsLeft <= 5 ? colors.error : colors.primary;
return (
<View style={styles.container}>
<View style={styles.modalContent}>
{/* Header Row with Title and Countdown */}
{/* ── Header Row ─────────────────────────────────────────────────── */}
<View style={styles.headerRow}>
<Text style={styles.modalTitle}>New Delivery Request</Text>
<View style={styles.timerContainer}>
<Text style={styles.timerText}>{secondsLeft}s</Text>
{/* Circular countdown timer (pure RN — no SVG needed) */}
<View
style={[
styles.timerWrapper,
{ borderColor: timerBorderColor },
]}
>
<Text style={[styles.timerText, { color: timerColor }]}>
{secondsLeft}s
</Text>
</View>
</View>
{/* Route Details Card */}
{/* ── Earnings Badge ─────────────────────────────────────────────── */}
<View
style={[
styles.earningsBadge,
{ backgroundColor: colors.primary },
]}
>
<Text style={styles.earningsBadgeLabel}>Estimated Earnings</Text>
<Text style={styles.earningsBadgeValue}>
{formatCurrency(currentOffer.totalAmount)}
</Text>
</View>
{/* ── Route Details ──────────────────────────────────────────────── */}
<View style={styles.routeContainer}>
<LocationRow
type="pickup"
name="Cafe Coffee Day"
address={newJobOffer.pickupAddress}
name="Pickup Location"
address={currentOffer.pickupAddress.label}
/>
<View style={styles.spacer} />
<LocationRow
type="drop"
name="Rohit Sharma"
address={newJobOffer.dropAddress}
name="Drop Location"
address={currentOffer.dropAddress.label}
/>
</View>
{/* Earnings & Distance Details */}
{/* ── Distance & Order ID ────────────────────────────────────────── */}
<View style={styles.detailsRow}>
<View style={styles.detailBox}>
<Text style={styles.detailLabel}>Estimated Earnings</Text>
<Text style={styles.detailValue}>{formatCurrency(newJobOffer.amount)}</Text>
<Text style={styles.detailLabel}>Distance</Text>
<Text style={styles.detailValue}>
{currentOffer.distanceKm} km
</Text>
</View>
<View style={styles.divider} />
<View style={styles.detailBox}>
<Text style={styles.detailLabel}>Distance</Text>
<Text style={styles.detailValue}>{newJobOffer.distance} km</Text>
<Text style={styles.detailLabel}>Order</Text>
<Text style={styles.detailValue}>#{currentOffer.orderId}</Text>
</View>
</View>
{/* Button Actions */}
{/* ── Action Buttons ─────────────────────────────────────────────── */}
<View style={styles.btnRow}>
<PrimaryButton
title="Reject"
onPress={handleReject}
style={styles.rejectBtn}
textStyle={{ color: colors.text }}
disabled={respondLoading}
/>
<Animated.View
style={[{ flex: 2, transform: [{ scale: pulseAnim }] }]}
>
<PrimaryButton
title="Accept"
title={respondLoading ? 'Accepting...' : 'Accept'}
onPress={handleAccept}
style={styles.acceptBtn}
style={[styles.acceptBtn, { flex: undefined }]}
disabled={respondLoading}
/>
</Animated.View>
</View>
</View>
</View>

View File

@ -7,3 +7,17 @@ export interface ToggleStatusResponse {
availabilityStatus: string;
message: string;
}
export interface DeliveryPartnerLocationRequest {
latitude: number;
longitude: number;
accuracyMeters: number;
speedKph: number;
heading: number;
}
export interface DeliveryPartnerLocationResponse {
message: string;
latitude: number;
longitude: number;
}

View File

@ -0,0 +1,27 @@
// Socket offer payload (delivery_offer event)
export interface DeliveryOffer {
deliveryId: string;
orderId: string;
pickupAddress: { label: string; lat: number; lng: number };
dropAddress: { label: string; lat: number; lng: number };
totalAmount: number;
distanceKm: number;
}
// REST: POST /delivery-partners/deliveries/{id}/respond
export type DeliveryAction = 'ACCEPT' | 'REJECT';
export interface RespondToOfferPayload {
action: DeliveryAction;
}
export interface RespondToOfferResponse {
success: boolean;
message: string;
deliveryId?: string;
}
// REST: GET /delivery-partners/deliveries/pending-offer
export interface PendingOfferResponse {
offer: DeliveryOffer | null;
}

View File

@ -2,3 +2,5 @@ export * from './auth';
export * from './onboard';
export * from './kyc';
export * from './dashboard';
export * from './delivery';
export * from './order';

112
app/interfaces/order.ts Normal file
View File

@ -0,0 +1,112 @@
export interface ActiveOrdersResponse {
id: string;
orderId: string;
deliveryPartnerId: string;
deliveryNumber: string;
status: string;
estimatedDistanceKm: string;
actualDistanceKm: string;
proofOfPickupKey: string;
proofOfDeliveryKey: string;
pickedUpAt: string;
deliveredAt: string;
order: {
id: string;
orderNumber: string;
customerId: string;
merchantId: string;
couponCode: string;
appliedPromotionId: string;
status: string;
orderType: string;
paymentMethodId: string;
paymentMethod: string;
currency: string;
subtotal: string;
deliveryFee: string;
platformFee: string;
serviceFee: string;
taxAmount: string;
discountAmount: string;
totalAmount: string;
pickupAddress: {
city: string;
label: string;
phone: string;
state: string;
landmark: string;
latitude: number;
longitude: number;
postalCode: string;
houseNumber: string;
addressLine1: string;
};
dropAddress: {
city: string;
label: string;
phone: string;
state: string;
landmark: string;
latitude: number;
longitude: number;
postalCode: string;
houseNumber: string;
addressLine1: string;
};
// "metadata": {
// "idempotencyKey": "idemp-key-12345",
// "trackingHistory": {
// "CONFIRMED": "2026-07-13T10:21:35.863Z"
// },
// "rejectedDriverIds": [
// "e5f639a8-f0cf-4357-ab45-919ebc3012bd"
// ]
// },
version: 1;
createdAt: string;
updatedAt: string;
deletedAt: string;
customer: {
id: string;
userId: string;
customerCode: string;
createdAt: string;
deletedAt: string;
user: {
id: string;
name: string;
phone: string;
profileImage: string;
};
};
// "merchant": {
// "id": "f53b10bf-1d72-42e6-9d4e-b78032ad051b",
// "name": "eererer",
// "imageUrl": null,
// "addresses": [
// {
// "id": "5476dd01-4b9c-4597-ad0f-1898ad333af6",
// "mapAddress": "Bantra, Jaynagar - I, South 24 Parganas, West Bengal, India",
// "latitude": "22.204311",
// "longitude": "88.509444",
// "city": "New York",
// "landmark": "Near Central Park",
// "phone": "+19999999999"
// }
// ]
// },
orderItems: OrderItem[];
};
}
export interface OrderItem {
id: string;
orderId: string;
productId: string;
itemType: string;
name: string;
quantity: string;
unitPrice: string;
totalAmount: string;
attributes: object;
}

View File

@ -28,14 +28,15 @@ export type AppStackParamList = {
[RouteNames.BottomTabs]: NavigatorScreenParams<BottomTabParamList>;
[RouteNames.NewJobRequest]: undefined;
[RouteNames.OrderAccepted]: undefined;
[RouteNames.ArrivedAtStore]: undefined;
[RouteNames.ArrivedAtStore]: { jobId: string };
[RouteNames.ConfirmPickup]: undefined;
[RouteNames.OrderPickedUp]: undefined;
[RouteNames.LiveTracking]: undefined;
[RouteNames.ArrivedAtCustomer]: undefined;
[RouteNames.ArrivedAtCustomer]: { jobId: string };
[RouteNames.DeliverOrder]: undefined;
[RouteNames.DeliveryCompleted]: undefined;
[RouteNames.Documents]: undefined;
// [RouteNames.JobDetails]: { jobId: string };
};
export type RootStackParamList = {

View File

@ -18,10 +18,10 @@ const onBoardingStack: React.FC = () => {
name={RouteNames.SetLocation}
component={SetLocationScreen}
/> */}
{/* <Stack.Screen
<Stack.Screen
name={RouteNames.CompleteProfile}
component={CompleteProfileScreen}
/> */}
/>
<Stack.Screen name={RouteNames.Kyc} component={KycScreen} />
<Stack.Screen
name={RouteNames.OnboardingComplete}

View File

@ -1,8 +1,9 @@
import React from 'react';
import React, { useRef, useEffect } from 'react';
import {
NavigationContainer,
DefaultTheme,
DarkTheme,
NavigationContainerRef,
} from '@react-navigation/native';
import { createNativeStackNavigator } from '@react-navigation/native-stack';
import { useColorScheme } from 'react-native';
@ -11,6 +12,7 @@ import AuthStack from './authStack';
import AppStack from './appStack';
import onBoardingStack from './onBoardingStack';
import { useAppSelector } from '@store';
import { socketService } from '@services';
const Stack = createNativeStackNavigator<RootStackParamList>();
@ -23,8 +25,22 @@ const RootNavigator: React.FC = () => {
state => state.auth.user?.status === 'ONBOARDING',
);
const navigationRef = useRef<NavigationContainerRef<RootStackParamList>>(null);
// Inject the navigation ref into the socket service so it can navigate
// to NewJobRequest from anywhere when a delivery_offer event arrives.
useEffect(() => {
if (navigationRef.current) {
socketService.bootstrap(
// store is already injected in App.tsx — pass null so it's not overwritten
null as any,
navigationRef.current,
);
}
}, []);
return (
<NavigationContainer theme={theme}>
<NavigationContainer ref={navigationRef} theme={theme}>
<Stack.Navigator screenOptions={{ headerShown: false }}>
{!accessToken ? (
<Stack.Screen name="Auth" component={AuthStack} />

View File

@ -15,7 +15,7 @@ const STORAGE_KEYS = {
} as const;
// ─── Config ──────────────────────────────────────────────────────────────────
const BASE_URL = 'https://720b-202-8-116-13.ngrok-free.app'; // TODO: replace with your actual base URL
const BASE_URL = 'https://8415-202-8-116-13.ngrok-free.app'; // TODO: replace with your actual base URL
// ─── Token Helpers ───────────────────────────────────────────────────────────
export const tokenManager = {

View File

@ -1,2 +1,3 @@
export * from './apiClient';
export * from './locationService';
export * from './socketService';

View File

@ -7,6 +7,9 @@ import type { GeoPosition, GeoError } from 'react-native-geolocation-service';
// ---------------------------------------------------------------------------
export interface LatLng {
heading: number | null;
speed: number | null;
accuracy: number | null;
latitude: number;
longitude: number;
}
@ -26,6 +29,9 @@ const GOOGLE_MAPS_API_KEY = 'AIzaSyDKaKVyQCmSpHMXcMkNCccSXpILpuvwsVM';
export const DEFAULT_LOCATION: LatLng = {
latitude: 12.9352,
longitude: 77.6245,
heading: 0,
speed: 0,
accuracy: 0,
};
// ---------------------------------------------------------------------------
@ -103,10 +109,15 @@ const getPositionOnce = (highAccuracy: boolean): Promise<LatLng> =>
new Promise<LatLng>((resolve, reject) => {
Geolocation.getCurrentPosition(
(position: GeoPosition) => {
// console.log('position', position);
resolve({
latitude: position.coords.latitude,
longitude: position.coords.longitude,
heading: position.coords.heading,
speed: position.coords.speed,
accuracy: position.coords.accuracy,
});
// console.log('-----------------');
},
(error: GeoError) => {
console.log(
@ -231,8 +242,8 @@ export const getCurrentLocationWithAddress =
const coords = await getCurrentPosition();
const address = await reverseGeocode(coords);
console.log('coords', coords);
console.log('address', address);
// console.log('coords', coords);
// console.log('address', address);
return { coords, address };
};

View File

@ -0,0 +1,145 @@
import { io, Socket } from 'socket.io-client';
import { Store } from '@reduxjs/toolkit';
import { SocketEvents } from '@utils/constants';
import { DeliveryOffer } from '@interfaces';
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://8415-202-8-116-13.ngrok-free.app';
type Callback = (data: any) => void;
/**
* Global singleton socket service powered by socket.io-client.
*
* Usage:
* bootstrapSocketService(store, navRef) // call once in App.tsx
* socketService.connect(token) // when user authenticates
* socketService.disconnect() // on logout
*/
class SocketService {
private socket: Socket | null = null;
private store: Store | null = null;
private navigationRef: any = null;
/**
* Must be called once at app startup so the service can dispatch actions
* and navigate globally when a `delivery_offer` event arrives.
* Each parameter is only updated if a non-null value is provided, allowing
* the store and navigationRef to be injected independently.
*/
bootstrap(store: Store | null, navigationRef: any) {
if (store) { this.store = store; }
if (navigationRef) { this.navigationRef = navigationRef; }
}
/**
* Open the socket connection with JWT auth.
*/
connect(token: string) {
if (this.socket?.connected) {
console.log('[Socket] Already connected');
return;
}
this.socket = io(BASE_URL, {
auth: { token },
transports: ['websocket'],
reconnection: true,
reconnectionAttempts: Infinity,
reconnectionDelay: 2000,
});
this.socket.on('connect', () => {
console.log('[Socket] Connected — id:', this.socket?.id);
});
this.socket.on('connect_error', (err: Error) => {
console.error('[Socket] Connection error:', err.message);
});
this.socket.on('disconnect', (reason: string) => {
console.log('[Socket] Disconnected:', reason);
});
// ── Listen for delivery offers ─────────────────────────────────────
this.socket.on(
SocketEvents.DeliveryOffer,
(data: DeliveryOffer) => {
console.log('[Socket] delivery_offer received:', data);
if (this.store) {
this.store.dispatch(setDeliveryOffer(data));
}
// Navigate to NewJobRequest screen regardless of current screen
if (this.navigationRef?.isReady()) {
this.navigationRef.navigate(RouteNames.NewJobRequest);
}
},
);
}
/**
* Gracefully close the socket connection.
*/
disconnect() {
if (this.socket) {
this.socket.removeAllListeners();
this.socket.disconnect();
this.socket = null;
console.log('[Socket] Disconnected (manual)');
}
}
/**
* Notify the server that the partner is available for deliveries.
*/
goOnline() {
this.socket?.emit(SocketEvents.GoOnline);
console.log('[Socket] Emitted go_online');
}
/**
* Notify the server that the partner is no longer available.
*/
goOffline() {
this.socket?.emit(SocketEvents.GoOffline);
console.log('[Socket] Emitted go_offline');
}
/**
* Send a location update to the server.
*/
updateLocation(coords: { latitude: number; longitude: number }) {
this.socket?.emit(SocketEvents.LocationUpdate, coords);
}
/**
* Register a custom listener on the underlying socket.
*/
on(event: string, callback: Callback) {
this.socket?.on(event, callback);
}
/**
* Remove a custom listener from the underlying socket.
*/
off(event: string, callback?: Callback) {
if (callback) {
this.socket?.off(event, callback);
} else {
this.socket?.removeAllListeners(event);
}
}
/**
* Whether the socket is currently connected.
*/
get connected(): boolean {
return this.socket?.connected ?? false;
}
}
export const socketService = new SocketService();

View File

@ -0,0 +1,2 @@
export * from './thunk';
export * from './reducer';

View File

@ -0,0 +1,61 @@
import { createReducer, createAction } from '@reduxjs/toolkit';
import { DeliveryOffer } from '@interfaces';
import { respondToOfferThunk, checkPendingOfferThunk } from './thunk';
export interface DeliveryState {
currentOffer: DeliveryOffer | null;
respondLoading: boolean;
respondError: string | null;
pendingOfferLoading: boolean;
}
const initialState: DeliveryState = {
currentOffer: null,
respondLoading: false,
respondError: null,
pendingOfferLoading: false,
};
/** Dispatched by socketService when a `delivery_offer` event is received. */
export const setDeliveryOffer = createAction<DeliveryOffer>(
'delivery/setDeliveryOffer',
);
/** Dispatched after accept/reject to clear the current offer from state. */
export const clearDeliveryOffer = createAction('delivery/clearDeliveryOffer');
export const deliveryReducer = createReducer(initialState, builder => {
builder
// ── Sync actions ──────────────────────────────────────────────────────
.addCase(setDeliveryOffer, (state, action) => {
state.currentOffer = action.payload;
})
.addCase(clearDeliveryOffer, state => {
state.currentOffer = null;
state.respondError = null;
})
// ── respondToOfferThunk ───────────────────────────────────────────────
.addCase(respondToOfferThunk.pending, state => {
state.respondLoading = true;
state.respondError = null;
})
.addCase(respondToOfferThunk.fulfilled, state => {
state.respondLoading = false;
state.currentOffer = null;
})
.addCase(respondToOfferThunk.rejected, (state, action) => {
state.respondLoading = false;
state.respondError = action.payload as string;
})
// ── checkPendingOfferThunk ────────────────────────────────────────────
.addCase(checkPendingOfferThunk.pending, state => {
state.pendingOfferLoading = true;
})
.addCase(checkPendingOfferThunk.fulfilled, (state, action) => {
state.pendingOfferLoading = false;
state.currentOffer = action.payload.offer;
})
.addCase(checkPendingOfferThunk.rejected, state => {
state.pendingOfferLoading = false;
});
});

View File

@ -0,0 +1,42 @@
import { createAsyncThunk } from '@reduxjs/toolkit';
import {
DeliveryAction,
RespondToOfferResponse,
PendingOfferResponse,
} from '@interfaces';
import * as deliveryApi from '../../../api/deliveryApi';
/**
* Respond to a delivery offer (ACCEPT / REJECT).
*/
export const respondToOfferThunk = createAsyncThunk<
RespondToOfferResponse,
{ deliveryId: string; action: DeliveryAction }
>(
'delivery/respondToOffer',
async ({ deliveryId, action }, { rejectWithValue }) => {
try {
return await deliveryApi.respondToOffer(deliveryId, action);
} catch (error: any) {
return rejectWithValue(
error.message || 'Failed to respond to delivery offer',
);
}
},
);
/**
* Check for any pending delivery offer (e.g. on app launch / reconnect).
*/
export const checkPendingOfferThunk = createAsyncThunk<PendingOfferResponse>(
'delivery/checkPendingOffer',
async (_, { rejectWithValue }) => {
try {
return await deliveryApi.getPendingOffer();
} catch (error: any) {
return rejectWithValue(
error.message || 'Failed to check pending offers',
);
}
},
);

View File

@ -1,3 +1,4 @@
export * from './auth';
export * from './earnings';
export * from './job';
export * from './delivery';

View File

@ -2,20 +2,24 @@ import { combineReducers } from '@reduxjs/toolkit';
import { authReducer } from './commonReducers/auth';
import { earningsReducer } from './commonReducers/earnings';
import { jobReducer } from './commonReducers/job';
import { deliveryReducer } from './commonReducers/delivery';
import setLocationReducer from '@features/screens/setLocationScreen/reducer';
import { completeProfileReducer, kycReducer } from '@features/screens';
import { onBoardingCompleteReducer } from '@features/screens/onboardingCompleteScreen';
import { dashBoardReducer } from '@features/screens/dashboardScreen';
import { activeDeliveriesReducer } from '@features/screens/jobsScreen';
const rootReducer = combineReducers({
auth: authReducer,
earnings: earningsReducer,
job: jobReducer,
delivery: deliveryReducer,
setLocation: setLocationReducer,
onboard: completeProfileReducer,
kyc: kycReducer,
onboardingComplete: onBoardingCompleteReducer,
dashboard: dashBoardReducer,
deliveries: activeDeliveriesReducer,
});
export type RootState = ReturnType<typeof rootReducer>;

View File

@ -36,8 +36,8 @@ export interface JobOffer {
dropName: string;
dropAddress: string;
dropCoordinates: Coordinates;
amount: number;
distance: number;
amount: string;
distance: string;
estimatedTime: number;
paymentType: 'online' | 'cod';
itemCount: number;

View File

@ -45,6 +45,7 @@ export enum JobStatus {
export enum SocketEvents {
NewJob = 'new_job',
DeliveryOffer = 'delivery_offer',
JobCancelled = 'job_cancelled',
LocationUpdate = 'location_update',
GoOnline = 'go_online',

511
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -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",

2120
yarn.lock

File diff suppressed because it is too large Load Diff