feat: implement core delivery partner features including authentication, KYC, navigation, and state management via Redux Toolkit

This commit is contained in:
Tamojit Biswas 2026-07-10 16:27:50 +05:30
parent 72f294d700
commit a6c713ae8c
93 changed files with 3539 additions and 797 deletions

5
ReactotronConfig.js Normal file
View File

@ -0,0 +1,5 @@
import Reactotron from 'reactotron-react-native';
Reactotron.configure() // controls connection & communication settings
.useReactNative() // add all built-in react native plugins
.connect(); // let's connect!

View File

@ -1,6 +1,8 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" />
<application
android:name=".MainApplication"
@ -23,5 +25,8 @@
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
<meta-data
android:name="com.google.android.geo.API_KEY"
android:value="AIzaSyDKaKVyQCmSpHMXcMkNCccSXpILpuvwsVM" />
</application>
</manifest>

View File

@ -6,6 +6,20 @@ buildscript {
targetSdkVersion = 36
ndkVersion = "27.1.12297006"
kotlinVersion = "2.1.20"
// ---------------------------------------------------------------
// Pin Google Play Services versions so all libraries use the same
// artifact. react-native-geolocation-service defaults to 18.0.0
// but react-native-maps requires 21.x where FusedLocationProvider-
// Client is an interface, not a class. Using two different versions
// causes IncompatibleClassChangeError and a hard app crash.
// ---------------------------------------------------------------
googlePlayServicesVersion = "21.3.0" // used by geolocation-service
playServicesVersion = "21.3.0" // fallback alias
playServicesLocationVersion = "21.3.0" // used by geolocation-service
googlePlayServicesLocationVersion = "21.3.0" // used by react-native-maps
googlePlayServicesMapsVersion = "19.1.0"
googlePlayServicesBaseVersion = "18.5.0"
}
repositories {
google()
@ -19,3 +33,13 @@ buildscript {
}
apply plugin: "com.facebook.react.rootproject"
// Force a single version of play-services-location across ALL modules
// to prevent IncompatibleClassChangeError at runtime.
subprojects {
configurations.all {
resolutionStrategy {
force "com.google.android.gms:play-services-location:21.3.0"
}
}
}

View File

@ -5,18 +5,22 @@ import {
} from 'react-native-safe-area-context';
import RootNavigator from '@navigation/rootNavigator';
import { Provider } from 'react-redux';
import { store } from '@store';
// import { store } from '@store';
import { useAppTheme } from '@theme';
import { persistor, store } from '@store';
import { PersistGate } from 'redux-persist/integration/react';
function App() {
const isDarkMode = useColorScheme() === 'dark';
return (
<Provider store={store}>
<SafeAreaProvider>
<StatusBar barStyle={isDarkMode ? 'light-content' : 'dark-content'} />
<AppContent />
</SafeAreaProvider>
<PersistGate loading={null} persistor={persistor}>
<SafeAreaProvider>
<StatusBar barStyle={isDarkMode ? 'light-content' : 'dark-content'} />
<AppContent />
</SafeAreaProvider>
</PersistGate>
</Provider>
);
}

66
app/api/authApi.ts Normal file
View File

@ -0,0 +1,66 @@
import { PartnerProfile } from '@app-types/index';
import { LoginResponse, VerifyOtpResponse } from '@interfaces';
import { apiClient } from '@services';
// import apiClient from '../services/apiClient'; // Used when connecting to real backend
// Mock responses
const mockSendOtpResponse = { requestId: 'req_mock_001' };
const mockVerifyOtpResponse = {
token: 'mock_token_xyz',
partnerId: 'partner_001',
};
const mockProfile: PartnerProfile = {
partnerId: 'partner_001',
fullName: 'Rahul Kumar',
email: 'rahul@example.com',
phone: '9876543210',
locationType: 'home',
location: {
coordinates: { latitude: 12.9716, longitude: 77.5946 },
address: 'Koramangala 4th Block',
city: 'Bengaluru',
state: 'Karnataka',
pincode: '560034',
},
avatarUri: null,
rating: 4.8,
totalDeliveries: 342,
acceptanceRate: 95,
completionRate: 98,
isVerified: true,
};
export const loginApi = async (phone: string) => {
const result = await apiClient.post<LoginResponse>('/auth/otp/request', {
phone,
});
return result;
};
export const verifyOtpApi = async (
phone: string,
code: string,
role: string,
) => {
return await apiClient.post<VerifyOtpResponse>('/auth/otp/verify', {
phone,
code,
role,
});
};
export const getProfile = async (
partnerId: string,
): Promise<PartnerProfile> => {
// return (await apiClient.get(`/profile/${partnerId}`)).data;
await new Promise<void>(r => setTimeout(r, 500));
return mockProfile;
};
export const updateProfile = async (
updates: Partial<PartnerProfile>,
): Promise<PartnerProfile> => {
// return (await apiClient.patch('/profile', updates)).data;
await new Promise<void>(r => setTimeout(r, 800));
return { ...mockProfile, ...updates };
};

12
app/api/dashboardApi.ts Normal file
View File

@ -0,0 +1,12 @@
import { ToggleStatusPayload, ToggleStatusResponse } from '@interfaces';
import { apiClient } from '@services';
export const toggleStatus = async (
payload: ToggleStatusPayload,
): Promise<ToggleStatusResponse> => {
const response = await apiClient.patch<ToggleStatusResponse>(
'/delivery-partners/availability',
payload,
);
return response;
};

15
app/api/earningsApi.ts Normal file
View File

@ -0,0 +1,15 @@
import { DayEarning } from '@app-types/index';
import { mockWeeklyEarnings, mockTodayEarnings } from '@store/mockData/mockEarnings';
// import apiClient from '../services/apiClient'; // Used when connecting to real backend
export const getTodayEarnings = async (): Promise<typeof mockTodayEarnings> => {
// return (await apiClient.get('/earnings/today')).data;
await new Promise<void>((r) => setTimeout(r, 500));
return mockTodayEarnings;
};
export const getWeeklyEarnings = async (): Promise<DayEarning[]> => {
// return (await apiClient.get('/earnings/weekly')).data;
await new Promise<void>((r) => setTimeout(r, 500));
return mockWeeklyEarnings;
};

4
app/api/index.ts Normal file
View File

@ -0,0 +1,4 @@
export * from './authApi';
export * from './onBoardApi';
export * from './kycApi';
export * from './dashboardApi';

38
app/api/jobApi.ts Normal file
View File

@ -0,0 +1,38 @@
import { Job } from '@app-types/index';
import { JobStatus } from '@utils/constants';
import { mockJobOffer } from '@store/mockData/mockJobs';
// import apiClient from '../services/apiClient'; // Used when connecting to real backend
export const acceptJob = async (jobId: string): Promise<Job> => {
// return (await apiClient.post(`/jobs/${jobId}/accept`)).data;
await new Promise<void>((r) => setTimeout(r, 500));
return {
...mockJobOffer,
jobId,
orderId: '#ORD125487',
status: JobStatus.Accepted,
acceptedAt: new Date().toISOString(),
collectAmount: 0,
};
};
export const rejectJob = async (jobId: string): Promise<void> => {
// await apiClient.post(`/jobs/${jobId}/reject`);
await new Promise<void>((r) => setTimeout(r, 300));
};
export const confirmPickup = async (jobId: string): Promise<void> => {
// await apiClient.post(`/jobs/${jobId}/pickup`);
await new Promise<void>((r) => setTimeout(r, 500));
};
export const confirmDelivery = async (jobId: string, rating: number): Promise<{ earnings: number }> => {
// return (await apiClient.post(`/jobs/${jobId}/deliver`, { rating })).data;
await new Promise<void>((r) => setTimeout(r, 800));
return { earnings: 78 };
};
export const reportIssue = async (jobId: string, reason: string): Promise<void> => {
// await apiClient.post(`/jobs/${jobId}/issue`, { reason });
await new Promise<void>((r) => setTimeout(r, 500));
};

41
app/api/kycApi.ts Normal file
View File

@ -0,0 +1,41 @@
import { KycDocUploadResponse, MyKycResponse } from '@interfaces';
import { apiClient } from '@services';
export interface KycUploadFile {
uri: string;
name: string;
type: string;
}
export interface KycUploadMetadata {
docName: string;
docNumber: string;
}
export interface KycUploadPayload {
files: KycUploadFile[];
metadata: KycUploadMetadata[];
}
export const uploadKycDocuments = async (
payload: KycUploadPayload,
): Promise<KycDocUploadResponse> => {
const formData = new FormData();
payload.files.forEach(file => {
// In React Native, FormData requires appending file objects with uri, name, and type properties
formData.append('files', {
uri: file.uri,
name: file.name,
type: file.type,
} as any);
});
formData.append('metadata', JSON.stringify(payload.metadata));
return await apiClient.post<KycDocUploadResponse>('/kyc/upload', formData);
};
export const getMyKyc = async (): Promise<MyKycResponse> => {
return await apiClient.get<MyKycResponse>('/kyc/my-details');
};

12
app/api/onBoardApi.ts Normal file
View File

@ -0,0 +1,12 @@
import { DashboardResponce, OnBoardPayload, UserProfile } from '@interfaces';
import { apiClient } from '@services';
export const onBoardComplete = async (
payload: OnBoardPayload,
): Promise<UserProfile> => {
return await apiClient.post<UserProfile>('/auth/onboard', payload);
};
export const getDashboardApi = async (): Promise<DashboardResponce> => {
return await apiClient.get<DashboardResponce>('/delivery-partners/dashboard');
};

View File

@ -20,3 +20,4 @@ export * from './screenHeader/screenHeader';
export * from './sectionHeader/sectionHeader';
export * from './statusBadge/statusBadge';
export * from './toast/toast';
export * from './kycDocumentCard';

View File

@ -0,0 +1,98 @@
import { StyleSheet } from 'react-native';
import { typography, spacing, borderRadius } from '@theme';
export const getStyles = (colors: any) =>
StyleSheet.create({
cardContainer: {
backgroundColor: colors.cardBg,
borderRadius: borderRadius.md,
borderWidth: 1,
borderColor: colors.border,
padding: spacing.lg,
marginBottom: spacing.xl,
// Subtle elevation
shadowColor: colors.cardShadow,
shadowOffset: { width: 0, height: 2 },
shadowOpacity: 0.1,
shadowRadius: 4,
elevation: 3,
},
headerRow: {
flexDirection: 'row',
justifyContent: 'space-between',
alignItems: 'center',
marginBottom: spacing.md,
borderBottomWidth: 1,
borderBottomColor: colors.border,
paddingBottom: spacing.sm,
},
docNumberText: {
fontSize: typography.fontSize.sm,
fontWeight: typography.fontWeight.bold,
color: colors.primary,
textTransform: 'uppercase',
letterSpacing: 0.5,
},
removeButton: {
paddingVertical: spacing.xs,
paddingHorizontal: spacing.sm,
},
removeButtonText: {
fontSize: typography.fontSize.xs,
color: colors.error,
fontWeight: typography.fontWeight.semibold,
},
fieldContainer: {
marginBottom: spacing.md,
},
label: {
fontSize: 11,
fontWeight: typography.fontWeight.bold,
color: colors.textSecondary,
textTransform: 'uppercase',
marginBottom: spacing.xs,
letterSpacing: 0.3,
},
input: {
backgroundColor: colors.inputBg,
borderWidth: 1,
borderColor: colors.border,
borderRadius: borderRadius.sm,
paddingHorizontal: spacing.md,
height: 48,
color: colors.text,
fontSize: typography.fontSize.sm,
},
uploadBox: {
backgroundColor: colors.inputBg,
borderWidth: 1,
borderColor: colors.border,
borderStyle: 'dashed',
borderRadius: borderRadius.sm,
paddingHorizontal: spacing.md,
height: 48,
flexDirection: 'row',
justifyContent: 'space-between',
alignItems: 'center',
},
uploadBoxActive: {
borderColor: colors.primary,
backgroundColor: colors.primaryLight,
},
uploadText: {
fontSize: typography.fontSize.sm,
color: colors.placeholder,
fontWeight: typography.fontWeight.medium,
},
fileNameText: {
fontSize: typography.fontSize.sm,
color: colors.primary,
fontWeight: typography.fontWeight.semibold,
},
helperText: {
fontSize: 11,
color: colors.textSecondary,
marginTop: spacing.xs,
fontStyle: 'italic',
},
});

View File

@ -0,0 +1,193 @@
import React from 'react';
import { View, Text, TextInput, TouchableOpacity, Alert } from 'react-native';
import { useAppTheme } from '@theme';
import Svg, { Path } from 'react-native-svg';
import { pick, types, errorCodes } from '@react-native-documents/picker';
import { getStyles } from './KycDocumentCard.styles';
export interface KycDocument {
id: string;
name: string;
idNumber: string;
fileName: string | null;
fileUri?: string | null;
fileType?: string | null;
status?: string;
}
interface KycDocumentCardProps {
document: KycDocument;
index: number;
onUpdate?: (updatedFields: Partial<KycDocument>) => void;
onRemove?: () => void;
readOnly?: boolean;
}
const UploadIcon = ({ size = 18, color = '#666' }) => (
<Svg width={size} height={size} viewBox="0 0 24 24" fill="none">
<Path
d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4M17 8l-5-5-5 5M12 3v12"
stroke={color}
strokeWidth="2"
strokeLinecap="round"
strokeLinejoin="round"
/>
</Svg>
);
export const KycDocumentCard: React.FC<KycDocumentCardProps> = ({
document,
index,
onUpdate,
onRemove,
readOnly = false,
}) => {
const { colors } = useAppTheme();
const styles = getStyles(colors);
const handleSelectFile = async () => {
if (readOnly) return;
try {
const [result] = await pick({
type: [types.pdf, types.images],
allowMultiSelection: false,
});
onUpdate?.({
fileName: result.name ?? `document_${Date.now()}`,
fileUri: result.uri,
fileType: result.type ?? 'application/octet-stream',
});
} catch (err: any) {
if (err?.code === errorCodes.OPERATION_CANCELED) {
// User dismissed the picker — no action needed
return;
}
Alert.alert('File Picker Error', err?.message ?? 'Failed to open document picker.');
}
};
const renderStatusBadge = (status?: string) => {
if (!status) return null;
const normalized = status.toLowerCase();
let bgColor = '#FFF9E6';
let textColor = '#B27B00';
let label = status;
if (normalized === 'verified' || normalized === 'approved') {
bgColor = '#E6F4EA';
textColor = '#137333';
label = 'Verified';
} else if (normalized === 'rejected' || normalized === 'failed') {
bgColor = '#FCE8E6';
textColor = '#C5221F';
label = 'Rejected';
} else if (normalized === 'pending' || normalized === 'submitted' || normalized === 'under_review') {
bgColor = '#FFF9E6';
textColor = '#B27B00';
label = 'Pending';
}
return (
<View
style={{
backgroundColor: bgColor,
paddingHorizontal: 8,
paddingVertical: 4,
borderRadius: 4,
alignSelf: 'center',
}}
>
<Text
style={{
color: textColor,
fontSize: 10,
fontWeight: 'bold',
textTransform: 'uppercase',
}}
>
{label}
</Text>
</View>
);
};
return (
<View style={styles.cardContainer}>
<View style={styles.headerRow}>
<Text style={styles.docNumberText}>Document {index + 1}</Text>
{readOnly && renderStatusBadge(document.status)}
{!readOnly && onRemove && (
<TouchableOpacity
style={styles.removeButton}
onPress={onRemove}
activeOpacity={0.7}
>
<Text style={styles.removeButtonText}>Remove</Text>
</TouchableOpacity>
)}
</View>
{/* Document Name / Type */}
<View style={styles.fieldContainer}>
<Text style={styles.label}>Document Name / Type</Text>
<TextInput
style={styles.input}
placeholder="e.g. National ID or Trade License"
placeholderTextColor={colors.placeholder}
value={document.name}
onChangeText={(text) => onUpdate?.({ name: text })}
autoCapitalize="sentences"
editable={!readOnly}
/>
</View>
{/* Document Identification Number */}
<View style={styles.fieldContainer}>
<Text style={styles.label}>Document Identification Number</Text>
<TextInput
style={styles.input}
placeholder="e.g. GSTIN/Aadhar/ID code"
placeholderTextColor={colors.placeholder}
value={document.idNumber}
onChangeText={(text) => onUpdate?.({ idNumber: text })}
autoCapitalize="characters"
editable={!readOnly}
/>
</View>
{/* Document Attachment */}
<View style={styles.fieldContainer}>
<Text style={styles.label}>Document Attachment File (JPG, PNG, PDF)</Text>
<TouchableOpacity
style={[
styles.uploadBox,
document.fileName ? styles.uploadBoxActive : null,
readOnly ? { borderStyle: 'solid', opacity: 0.8 } : null,
]}
onPress={handleSelectFile}
activeOpacity={0.8}
disabled={readOnly}
>
<Text
style={[
styles.uploadText,
document.fileName ? styles.fileNameText : null,
]}
numberOfLines={1}
>
{document.fileName ? document.fileName : 'SELECT DOCUMENT FILE...'}
</Text>
{!readOnly && <UploadIcon color={document.fileName ? colors.primary : colors.textSecondary} />}
</TouchableOpacity>
{!readOnly && (
<Text style={styles.helperText}>
Supported: pdf, png, jpg, or jpeg under 200kb only.
</Text>
)}
</View>
</View>
);
};
export default KycDocumentCard;

View File

@ -0,0 +1 @@
export * from './KycDocumentCard';

View File

@ -16,6 +16,12 @@ import { useNavigation } from '@react-navigation/native';
import { NativeStackNavigationProp } from '@react-navigation/native-stack';
import { AuthStackParamList } from '@navigation/navigationTypes';
import { RouteNames } from '@utils/constants';
import {
loginWithPhone,
RootState,
useAppDispatch,
useAppSelector,
} from '@store';
// Premium SVG Mascot: Delivery boy on scooter
const DeliveryScooterSvg = () => {
@ -138,9 +144,12 @@ export const LoginScreen: React.FC = () => {
const [mobileNumber, setMobileNumber] = useState('');
// const [password, setPassword] = useState('');
const [isLoading, setIsLoading] = useState(false);
// const [isLoading, setIsLoading] = useState(false);
const [errors, setErrors] = useState<{ mobile?: string }>({});
const dispatch = useAppDispatch();
const { loading } = useAppSelector((state: RootState) => state.auth);
const validate = () => {
const newErrors: { mobile?: string } = {};
if (!mobileNumber) {
@ -162,12 +171,14 @@ export const LoginScreen: React.FC = () => {
const handleLogin = () => {
if (!validate()) return;
setIsLoading(true);
// Simulate API request
setTimeout(() => {
setIsLoading(false);
navigation.navigate(RouteNames.Otp, { phone: mobileNumber });
}, 1500);
dispatch(loginWithPhone(mobileNumber))
.unwrap()
.then(() => {
navigation.navigate(RouteNames.Otp, { phone: mobileNumber });
})
.catch((error: any) => {
Alert.alert('Error', error.message);
});
};
// const handleForgotPassword = () => {
@ -224,7 +235,7 @@ export const LoginScreen: React.FC = () => {
<PrimaryButton
title="Login"
onPress={handleLogin}
isLoading={isLoading}
isLoading={loading}
style={styles.loginButton}
/>

View File

@ -2,27 +2,35 @@ import React, { useState, useEffect } from 'react';
import { View, Text, ScrollView, TouchableOpacity, Alert } from 'react-native';
import { OtpInput, NumericKeypad, PrimaryButton } from '@components';
import { useAppTheme } from '@theme';
import { useNavigation } from '@react-navigation/native';
import { CompositeNavigationProp, useNavigation } from '@react-navigation/native';
import { NativeStackNavigationProp } from '@react-navigation/native-stack';
import { AuthStackParamList } from '@navigation/navigationTypes';
import { AuthStackParamList, RootStackParamList } from '@navigation/navigationTypes';
import { RouteNames } from '@utils/constants';
import { getStyles } from './OtpScreen.styles';
import { RootState, useAppDispatch, useAppSelector, verifyOtp } from '@store';
export const OtpScreen: React.FC<{ route: any }> = ({ route }) => {
const { colors } = useAppTheme();
const styles = getStyles(colors);
const navigation = useNavigation<NativeStackNavigationProp<AuthStackParamList>>();
const navigation = useNavigation<
CompositeNavigationProp<
NativeStackNavigationProp<AuthStackParamList, RouteNames.Otp>,
NativeStackNavigationProp<RootStackParamList>
>
>();
const phone = route?.params?.phone || '+91 98765 43210';
const [otp, setOtp] = useState('');
const [timer, setTimer] = useState(30);
const [isLoading, setIsLoading] = useState(false);
// const [isLoading, setIsLoading] = useState(false);
const [error, setError] = useState(false);
const dispatch = useAppDispatch();
const { loading } = useAppSelector((state: RootState) => state.auth);
useEffect(() => {
if (timer <= 0) return;
const interval = setInterval(() => {
setTimer((prev) => prev - 1);
setTimer(prev => prev - 1);
}, 1000);
return () => clearInterval(interval);
}, [timer]);
@ -30,43 +38,46 @@ export const OtpScreen: React.FC<{ route: any }> = ({ route }) => {
const handleKeyPress = (digit: string) => {
if (otp.length < 6) {
setError(false);
setOtp((prev) => prev + digit);
setOtp(prev => prev + digit);
}
};
const handleBackspace = () => {
setOtp((prev) => prev.slice(0, -1));
setOtp(prev => prev.slice(0, -1));
};
const handleVerify = () => {
const handleVerify = async () => {
if (otp.length < 6) {
setError(true);
Alert.alert('Invalid OTP', 'Please enter a 6-digit verification code.');
return;
}
setIsLoading(true);
setTimeout(() => {
setIsLoading(false);
if (otp === '123456' || otp.length === 6) {
navigation.navigate(RouteNames.SetLocation);
} else {
setError(true);
Alert.alert('Verification Failed', 'The code you entered is incorrect. Try 123456.');
}
}, 1200);
try {
await dispatch(
verifyOtp({ phone, code: otp, role: 'DELIVERY' }),
).unwrap();
navigation.navigate('OnBoarding', { screen: RouteNames.CompleteProfile });
} catch (error) {
Alert.alert('Error', error as string);
}
};
const handleResend = () => {
if (timer > 0) return;
setOtp('');
setTimer(30);
Alert.alert('OTP Resent', 'A new verification code has been sent to your mobile number.');
Alert.alert(
'OTP Resent',
'A new verification code has been sent to your mobile number.',
);
};
return (
<View style={styles.container}>
<ScrollView contentContainerStyle={styles.scrollContainer} bounces={false}>
<ScrollView
contentContainerStyle={styles.scrollContainer}
bounces={false}
>
<View style={styles.headerContainer}>
<Text style={styles.title}>Verify Phone</Text>
<Text style={styles.subtitle}>
@ -83,7 +94,10 @@ export const OtpScreen: React.FC<{ route: any }> = ({ route }) => {
<View style={styles.resendContainer}>
{timer > 0 ? (
<Text style={styles.resendText}>
Resend code in <Text style={{ color: colors.primary, fontWeight: 'bold' }}>00:{timer < 10 ? `0${timer}` : timer}</Text>
Resend code in{' '}
<Text style={{ color: colors.primary, fontWeight: 'bold' }}>
00:{timer < 10 ? `0${timer}` : timer}
</Text>
</Text>
) : (
<TouchableOpacity onPress={handleResend} activeOpacity={0.7}>
@ -95,7 +109,7 @@ export const OtpScreen: React.FC<{ route: any }> = ({ route }) => {
<PrimaryButton
title="Verify & Continue"
onPress={handleVerify}
isLoading={isLoading}
isLoading={loading}
style={styles.verifyButton}
/>
</View>
@ -104,7 +118,7 @@ export const OtpScreen: React.FC<{ route: any }> = ({ route }) => {
<NumericKeypad
onPress={handleKeyPress}
onBackspace={handleBackspace}
disabled={isLoading}
disabled={loading}
/>
</View>
</ScrollView>

View File

@ -1 +1 @@
export { default } from "./arrivedAtCustomerScreen";
export * from './arrivedAtCustomerScreen';

View File

@ -1 +1 @@
export { default } from "./arrivedAtStoreScreen";
export * from './arrivedAtStoreScreen';

View File

@ -111,4 +111,10 @@ export const getStyles = (colors: any) =>
marginTop: spacing.xl,
width: '100%',
},
dropdownLabel: {
fontSize: typography.fontSize.sm,
fontWeight: typography.fontWeight.semibold,
color: colors.textSecondary,
marginBottom: spacing.sm,
},
});

View File

@ -1,63 +1,120 @@
import React, { useState } from 'react';
import { View, Text, ScrollView, TouchableOpacity, Alert } from 'react-native';
import { useAppTheme } from '@theme';
import { CustomInput, PrimaryButton } from '@components';
import { ClipboardIcon, ChevronRightIcon, PersonIcon } from '@icons';
import { CustomInput, PrimaryButton, ChipSelector } from '@components';
import { PersonIcon } from '@icons';
import { useNavigation } from '@react-navigation/native';
import { NativeStackNavigationProp } from '@react-navigation/native-stack';
import { AuthStackParamList } from '@navigation/navigationTypes';
import { OnBoardingParamList } from '@navigation/navigationTypes';
import { RouteNames } from '@utils/constants';
import { Gender, VehicleType } from '@interfaces';
import { getStyles } from './completeProfileScreen.styles';
import { useAppDispatch, useAppSelector } from '@store';
import { completeOnboard } from './thunk';
export const CompleteProfileScreen: React.FC = () => {
const { colors } = useAppTheme();
const styles = getStyles(colors);
const navigation = useNavigation<NativeStackNavigationProp<AuthStackParamList>>();
const navigation =
useNavigation<NativeStackNavigationProp<OnBoardingParamList>>();
const [fullName, setFullName] = useState('Rahul Kumar');
const [email, setEmail] = useState('rahul.kumar@gmail.com');
const [vehicleNo, setVehicleNo] = useState('KA-01-MJ-8845');
const [isLoading, setIsLoading] = useState(false);
const [fullName, setFullName] = useState('');
const [email, setEmail] = useState('');
const [vehicleNo, setVehicleNo] = useState('');
const [gender, setGender] = useState<Gender | ''>('');
const [vehicleType, setVehicleType] = useState<VehicleType | ''>('');
// const [isLoading, setIsLoading] = useState(false);
const dispatch = useAppDispatch();
const { isLoading } = useAppSelector(state => state.onboard);
const documents = [
{ id: 'dl', title: 'Driving License', sub: 'Verified', status: 'verified' },
{ id: 'aadhaar', title: 'Aadhaar Card', sub: 'Verified', status: 'verified' },
{ id: 'rc', title: 'Vehicle RC', sub: 'Verified', status: 'verified' },
{ id: 'insurance', title: 'Insurance Policy', sub: 'Expires on 12 May 2026', status: 'expiring' },
];
const genderOptions = Object.values(Gender).map(v => ({
label: v,
value: v,
}));
const vehicleTypeOptions = Object.values(VehicleType || '').map(v => ({
label: v,
value: v,
}));
const handleDocumentPress = (docTitle: string) => {
Alert.alert('Document Details', `Viewing or uploading ${docTitle}...`);
};
// const documents = [
// { id: 'dl', title: 'Driving License', sub: 'Verified', status: 'verified' },
// {
// id: 'aadhaar',
// title: 'Aadhaar Card',
// sub: 'Verified',
// status: 'verified',
// },
// { id: 'rc', title: 'Vehicle RC', sub: 'Verified', status: 'verified' },
// {
// id: 'insurance',
// title: 'Insurance Policy',
// sub: 'Expires on 12 May 2026',
// status: 'expiring',
// },
// ];
// const handleDocumentPress = (docTitle: string) => {
// Alert.alert('Document Details', `Viewing or uploading ${docTitle}...`);
// };
const handleSubmit = () => {
if (!fullName || !email || !vehicleNo) {
Alert.alert('Required Fields', 'Please fill out all profile information.');
if (!fullName || !email || !vehicleNo || !gender || !vehicleType) {
Alert.alert(
'Required Fields',
'Please fill out all profile information including gender and vehicle type.',
);
return;
}
setIsLoading(true);
setTimeout(() => {
setIsLoading(false);
navigation.navigate(RouteNames.OnboardingComplete);
}, 1500);
dispatch(
completeOnboard({
name: fullName,
email: email,
vehicleNumber: vehicleNo,
gender: gender,
vehicleType: vehicleType,
}),
)
.unwrap()
.then(() => {
navigation.navigate(RouteNames.Kyc);
})
.catch(error => {
Alert.alert('Error', error.message);
});
};
return (
<View style={styles.container}>
<ScrollView contentContainerStyle={styles.scrollContainer} showsVerticalScrollIndicator={false}>
<ScrollView
contentContainerStyle={styles.scrollContainer}
showsVerticalScrollIndicator={false}
>
<View style={styles.headerContainer}>
<Text style={styles.title}>Complete Profile</Text>
<Text style={styles.subtitle}>Enter details & upload documents to get verified</Text>
<Text style={styles.subtitle}>
Enter details & upload documents to get verified
</Text>
</View>
{/* Profile Avatar Upload Mock */}
<View style={styles.profileImageContainer}>
<TouchableOpacity style={styles.avatarPlaceholder} activeOpacity={0.8}>
<TouchableOpacity
style={styles.avatarPlaceholder}
activeOpacity={0.8}
>
<PersonIcon size={40} color={colors.textSecondary} />
<Text style={styles.avatarText}>Add Photo</Text>
<View style={styles.cameraBadge}>
<Text style={{ color: colors.white, fontSize: 12, fontWeight: 'bold' }}>+</Text>
<Text
style={{
color: colors.white,
fontSize: 12,
fontWeight: 'bold',
}}
>
+
</Text>
</View>
</TouchableOpacity>
</View>
@ -87,8 +144,28 @@ export const CompleteProfileScreen: React.FC = () => {
/>
</View>
{/* Required Documents Checklist */}
{/* Gender Selection */}
<View style={styles.section}>
<Text style={styles.sectionTitle}>Gender</Text>
<ChipSelector
options={genderOptions}
selected={gender}
onChange={(val: Gender) => setGender(val)}
/>
</View>
{/* Vehicle Type Selection */}
<View style={styles.section}>
<Text style={styles.sectionTitle}>Vehicle Type</Text>
<ChipSelector
options={vehicleTypeOptions}
selected={vehicleType}
onChange={(val: VehicleType) => setVehicleType(val)}
/>
</View>
{/* Required Documents Checklist */}
{/* <View style={styles.section}>
<Text style={styles.sectionTitle}>Required Documents</Text>
{documents.map((doc) => (
<TouchableOpacity
@ -135,7 +212,7 @@ export const CompleteProfileScreen: React.FC = () => {
</View>
</TouchableOpacity>
))}
</View>
</View> */}
<PrimaryButton
title="Submit & Verify"

View File

@ -1 +1,3 @@
export { default } from './completeProfileScreen';
export * from './completeProfileScreen';
export * from './reducer';
export * from './thunk';

View File

@ -0,0 +1,34 @@
import { UserProfile } from '@interfaces';
import { completeOnboard } from './thunk';
import { createReducer } from '@reduxjs/toolkit';
export interface completeProfileState {
isLoading: boolean;
isSubmitting?: boolean;
error: string | null;
submitError: string | null;
userProfile: UserProfile | null;
}
const initialState: completeProfileState = {
isLoading: false,
error: null,
submitError: null,
userProfile: null,
};
export const completeProfileReducer = createReducer(initialState, builder => {
builder
.addCase(completeOnboard.pending, state => {
state.isLoading = true;
state.submitError = null;
})
.addCase(completeOnboard.fulfilled, (state, action) => {
state.isLoading = false;
state.userProfile = action.payload;
})
.addCase(completeOnboard.rejected, (state, action) => {
state.isLoading = false;
state.submitError = action.payload as string;
});
});

View File

@ -0,0 +1,17 @@
import { onBoardComplete } from '@api';
import { OnBoardPayload } from '@interfaces';
import { createAsyncThunk } from '@reduxjs/toolkit';
export const completeOnboard = createAsyncThunk(
'preferences/completeOnboard',
async (payload: OnBoardPayload, { rejectWithValue }) => {
try {
const response = await onBoardComplete(payload);
return response;
} catch (error: unknown) {
const message =
error instanceof Error ? error.message : 'Failed to complete onboard';
return rejectWithValue(message);
}
},
);

View File

@ -3,7 +3,7 @@ import { View, Text, ScrollView, TouchableOpacity, Alert } from 'react-native';
import { useAppTheme } from '@theme';
import { ScreenHeader, PrimaryButton } from '@components';
import { useAppDispatch, useAppSelector } from '@store';
import { advanceJobStatus } from '@store/slices/jobSlice';
import { advanceJobStatus } from '@store/commonReducers/job';
import { useNavigation } from '@react-navigation/native';
import { NativeStackNavigationProp } from '@react-navigation/native-stack';
import { AppStackParamList } from '@navigation/navigationTypes';

View File

@ -1 +1 @@
export { default } from "./confirmPickupScreen";
export * from './confirmPickupScreen';

View File

@ -1,11 +1,18 @@
import React, { useEffect, useRef } from 'react';
import { View, Text, ScrollView, TouchableOpacity, Alert, Switch } from 'react-native';
import {
View,
Text,
ScrollView,
TouchableOpacity,
Alert,
Switch,
} 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/slices/authSlice';
import { setNewJobOffer } from '@store/slices/jobSlice';
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';
@ -13,18 +20,27 @@ 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';
const DashboardScreen: React.FC = () => {
export const DashboardScreen: React.FC = () => {
const { colors } = useAppTheme();
const styles = getStyles(colors);
const dispatch = useAppDispatch();
const navigation = useNavigation<NativeStackNavigationProp<AppStackParamList>>();
const navigation =
useNavigation<NativeStackNavigationProp<AppStackParamList>>();
const isOnline = useAppSelector((state) => state.auth.isOnline);
const { todayEarnings, completedCount, onlineMinutes, weeklyData } = useAppSelector(
(state) => state.earnings
// Derive online status from API response stored in dashboard slice.
// Falls back to auth.isOnline during the initial load before any toggle API call.
const { availabiltyStatus, loading: toggleLoading } = useAppSelector(
state => state.dashboard,
);
const { jobStatus, activeJob } = useAppSelector((state) => state.job);
const authIsOnline = useAppSelector(state => state.auth.isOnline);
const isOnline = availabiltyStatus
? availabiltyStatus.availabilityStatus === 'ONLINE'
: authIsOnline;
const { todayEarnings, completedCount, onlineMinutes, weeklyData } =
useAppSelector(state => state.earnings);
const { jobStatus, activeJob } = useAppSelector(state => state.job);
const timerRef = useRef<ReturnType<typeof setTimeout> | null>(null);
@ -50,7 +66,26 @@ const DashboardScreen: React.FC = () => {
}, [isOnline, jobStatus, dispatch, navigation]);
const handleToggleOnline = () => {
const newStatus = isOnline ? 'OFFLINE' : 'ONLINE';
// Optimistically update the auth flag so the timer and UI react immediately
dispatch(setOnlineStatus(!isOnline));
dispatch(toggleStatus({ availabilityStatus: newStatus }))
.unwrap()
.then(response => {
// Confirm state from server — setOnlineStatus stays in sync
const isNowOnline = response.availabilityStatus === 'ONLINE';
dispatch(setOnlineStatus(isNowOnline));
})
.catch(() => {
// Revert the optimistic update on API failure
dispatch(setOnlineStatus(isOnline));
Alert.alert(
'Status Update Failed',
'Could not update your availability. Please try again.',
);
});
};
const handleSimulateOffer = () => {
@ -59,7 +94,10 @@ const DashboardScreen: React.FC = () => {
return;
}
if (jobStatus !== JobStatus.Idle) {
Alert.alert('Active Delivery', 'You already have an active job offer or order.');
Alert.alert(
'Active Delivery',
'You already have an active job offer or order.',
);
return;
}
dispatch(setNewJobOffer(mockJobOffer));
@ -95,33 +133,44 @@ const DashboardScreen: React.FC = () => {
const formatOnlineTime = (mins: number) => {
const hours = Math.floor(mins / 60);
const remainingMins = mins % 60;
return `${hours}h ${remainingMins < 10 ? `0${remainingMins}` : remainingMins}m`;
return `${hours}h ${
remainingMins < 10 ? `0${remainingMins}` : remainingMins
}m`;
};
// Helper to render chart bar heights dynamically based on amounts
const maxWeeklyAmount = Math.max(...weeklyData.map((d) => d.amount), 1);
const maxWeeklyAmount = Math.max(...weeklyData.map(d => d.amount), 1);
return (
<View style={styles.container}>
<ScrollView contentContainerStyle={styles.scrollContainer} showsVerticalScrollIndicator={false}>
<ScrollView
contentContainerStyle={styles.scrollContainer}
showsVerticalScrollIndicator={false}
>
{/* Header Section */}
<View style={styles.header}>
<View>
<Text style={styles.greetingText}>Good Morning </Text>
<Text style={styles.nameText}>Rahul Kumar</Text>
</View>
<View style={styles.toggleWrapper}>
<View
style={[
styles.statusDot,
{ backgroundColor: isOnline ? colors.statusOnline : colors.statusOffline },
{
backgroundColor: isOnline
? colors.statusOnline
: colors.statusOffline,
},
]}
/>
<Text
style={[
styles.statusText,
{ color: isOnline ? colors.statusOnline : colors.statusOffline },
{
color: isOnline ? colors.statusOnline : colors.statusOffline,
},
]}
>
{isOnline ? 'Online' : 'Offline'}
@ -131,7 +180,8 @@ const DashboardScreen: React.FC = () => {
onValueChange={handleToggleOnline}
trackColor={{ false: colors.border, true: colors.primaryLight }}
thumbColor={isOnline ? colors.primary : colors.placeholder}
style={{ marginLeft: 8 }}
disabled={toggleLoading}
style={{ marginLeft: 8, opacity: toggleLoading ? 0.5 : 1 }}
/>
</View>
</View>
@ -155,11 +205,13 @@ const DashboardScreen: React.FC = () => {
</Text>
<Text style={styles.statLabel}>Earnings</Text>
</View>
<View style={styles.statDivider} />
<View style={styles.statBox}>
<Text style={styles.statVal}>{isOnline ? completedCount : 0}</Text>
<Text style={styles.statVal}>
{isOnline ? completedCount : 0}
</Text>
<Text style={styles.statLabel}>Completed</Text>
</View>
@ -182,7 +234,9 @@ const DashboardScreen: React.FC = () => {
<Text style={styles.perfTitle}>Your Performance</Text>
<View style={styles.perfRow}>
<View style={styles.perfBox}>
<Text style={[styles.perfVal, { color: colors.warning }]}>4.8 </Text>
<Text style={[styles.perfVal, { color: colors.warning }]}>
4.8
</Text>
<Text style={styles.perfLabel}>Rating</Text>
</View>
<View style={styles.perfBox}>
@ -202,7 +256,9 @@ const DashboardScreen: React.FC = () => {
<View style={styles.chartRow}>
{weeklyData.map((data, index) => {
// Bar height percent
const heightPercent = `${(data.amount / maxWeeklyAmount) * 85}%`;
const heightPercent = `${
(data.amount / maxWeeklyAmount) * 85
}%`;
return (
<View key={index} style={styles.barContainer}>
<Text style={styles.barVal}>{data.amount}</Text>
@ -211,7 +267,10 @@ const DashboardScreen: React.FC = () => {
styles.bar,
{
height: heightPercent as any,
backgroundColor: index === weeklyData.length - 1 ? colors.primary : colors.placeholder,
backgroundColor:
index === weeklyData.length - 1
? colors.primary
: colors.placeholder,
},
]}
/>
@ -228,7 +287,9 @@ const DashboardScreen: React.FC = () => {
activeOpacity={0.8}
onPress={handleSimulateOffer}
>
<Text style={styles.simulateBtnText}> Simulate Immediate Job Offer</Text>
<Text style={styles.simulateBtnText}>
Simulate Immediate Job Offer
</Text>
</TouchableOpacity>
<PrimaryButton
@ -239,9 +300,12 @@ const DashboardScreen: React.FC = () => {
</>
) : (
<View style={styles.promptCard}>
<Text style={styles.promptTitle}>Go Online to start receiving delivery requests</Text>
<Text style={styles.promptTitle}>
Go Online to start receiving delivery requests
</Text>
<Text style={styles.promptDesc}>
Make sure you are in a high-demand delivery zone to maximize your earnings.
Make sure you are in a high-demand delivery zone to maximize your
earnings.
</Text>
<PrimaryButton
title="Go Online"
@ -257,7 +321,12 @@ const DashboardScreen: React.FC = () => {
<View style={styles.helpRow}>
<TouchableOpacity
style={styles.helpItem}
onPress={() => Alert.alert('Emergency SOS', 'Calling nearest security/emergency dispatch...')}
onPress={() =>
Alert.alert(
'Emergency SOS',
'Calling nearest security/emergency dispatch...',
)
}
>
<BellIcon size={24} color={colors.error} />
<Text style={styles.helpLabel}>Emergency SOS</Text>
@ -266,7 +335,11 @@ const DashboardScreen: React.FC = () => {
<TouchableOpacity
style={styles.helpItem}
onPress={() => navigation.navigate(RouteNames.BottomTabs, { screen: RouteNames.Profile })}
onPress={() =>
navigation.navigate(RouteNames.BottomTabs, {
screen: RouteNames.Profile,
})
}
>
<ClipboardIcon size={24} color={colors.primary} />
<Text style={styles.helpLabel}>FAQ Center</Text>
@ -275,7 +348,12 @@ const DashboardScreen: React.FC = () => {
<TouchableOpacity
style={styles.helpItem}
onPress={() => Alert.alert('Support Chat', 'Initiating live helpdesk agent chat...')}
onPress={() =>
Alert.alert(
'Support Chat',
'Initiating live helpdesk agent chat...',
)
}
>
<PersonIcon size={24} color={colors.statusOnDelivery} />
<Text style={styles.helpLabel}>Live Support</Text>
@ -286,47 +364,83 @@ const DashboardScreen: React.FC = () => {
</ScrollView>
{/* Active Job Floating Bottom Banner */}
{jobStatus !== JobStatus.Idle && jobStatus !== JobStatus.NewRequest && activeJob && (
<TouchableOpacity
style={{
position: 'absolute',
bottom: spacing.xs,
left: spacing.md,
right: spacing.md,
backgroundColor: colors.primary,
borderRadius: borderRadius.md,
paddingVertical: spacing.md,
paddingHorizontal: spacing.lg,
flexDirection: 'row',
alignItems: 'center',
justifyContent: 'space-between',
...shadow.lg,
}}
activeOpacity={0.9}
onPress={handleActiveJobBannerPress}
>
<View style={{ flexDirection: 'row', alignItems: 'center', flex: 1 }}>
<ClipboardIcon size={20} color={colors.white} />
<View style={{ marginLeft: spacing.sm, flex: 1 }}>
<Text style={{ color: colors.white, fontWeight: 'bold', fontSize: typography.fontSize.sm }}>
Order {activeJob.orderId}
</Text>
<Text style={{ color: colors.primaryLight, fontSize: typography.fontSize.xs }} numberOfLines={1}>
{jobStatus === JobStatus.Accepted && 'Heading to pickup store...'}
{jobStatus === JobStatus.ArrivedAtStore && 'At pickup store, check items...'}
{jobStatus === JobStatus.PickedUp && 'Order picked up, routing to customer...'}
{jobStatus === JobStatus.EnRoute && 'On the way to customer...'}
{jobStatus === JobStatus.ArrivedAtCustomer && 'Arrived at customer location...'}
{jobStatus !== JobStatus.Idle &&
jobStatus !== JobStatus.NewRequest &&
activeJob && (
<TouchableOpacity
style={{
position: 'absolute',
bottom: spacing.xs,
left: spacing.md,
right: spacing.md,
backgroundColor: colors.primary,
borderRadius: borderRadius.md,
paddingVertical: spacing.md,
paddingHorizontal: spacing.lg,
flexDirection: 'row',
alignItems: 'center',
justifyContent: 'space-between',
...shadow.lg,
}}
activeOpacity={0.9}
onPress={handleActiveJobBannerPress}
>
<View
style={{ flexDirection: 'row', alignItems: 'center', flex: 1 }}
>
<ClipboardIcon size={20} color={colors.white} />
<View style={{ marginLeft: spacing.sm, flex: 1 }}>
<Text
style={{
color: colors.white,
fontWeight: 'bold',
fontSize: typography.fontSize.sm,
}}
>
Order {activeJob.orderId}
</Text>
<Text
style={{
color: colors.primaryLight,
fontSize: typography.fontSize.xs,
}}
numberOfLines={1}
>
{jobStatus === JobStatus.Accepted &&
'Heading to pickup store...'}
{jobStatus === JobStatus.ArrivedAtStore &&
'At pickup store, check items...'}
{jobStatus === JobStatus.PickedUp &&
'Order picked up, routing to customer...'}
{jobStatus === JobStatus.EnRoute &&
'On the way to customer...'}
{jobStatus === JobStatus.ArrivedAtCustomer &&
'Arrived at customer location...'}
</Text>
</View>
</View>
<View
style={{
backgroundColor: colors.white,
paddingHorizontal: spacing.md,
paddingVertical: 6,
borderRadius: borderRadius.sm,
}}
>
<Text
style={{
color: colors.primary,
fontWeight: 'bold',
fontSize: typography.fontSize.xs,
}}
>
View
</Text>
</View>
</View>
<View style={{ backgroundColor: colors.white, paddingHorizontal: spacing.md, paddingVertical: 6, borderRadius: borderRadius.sm }}>
<Text style={{ color: colors.primary, fontWeight: 'bold', fontSize: typography.fontSize.xs }}>View</Text>
</View>
</TouchableOpacity>
)}
</TouchableOpacity>
)}
</View>
);
};
export default DashboardScreen;
// export default DashboardScreen;

View File

@ -1 +1,3 @@
export { default } from './dashboardScreen';
export * from './dashboardScreen';
export * from './reducer';
export * from './thunk';

View File

@ -0,0 +1,31 @@
import { ToggleStatusResponse } from '@interfaces';
import { createReducer } from '@reduxjs/toolkit';
import { toggleStatus } from './thunk';
export interface ToggleState {
loading: boolean;
error: string | null;
availabiltyStatus: ToggleStatusResponse | null;
}
export const initialState: ToggleState = {
loading: false,
error: null,
availabiltyStatus: null,
};
export const dashBoardReducer = createReducer(initialState, builder => {
builder
.addCase(toggleStatus.pending, state => {
state.loading = true;
state.error = null;
})
.addCase(toggleStatus.fulfilled, (state, action) => {
state.loading = false;
state.availabiltyStatus = action.payload;
})
.addCase(toggleStatus.rejected, (state, action) => {
state.loading = false;
state.error = action.payload as string;
});
});

View File

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

View File

@ -3,7 +3,7 @@ import { View, Text, ScrollView, Alert } from 'react-native';
import { useAppTheme } from '@theme';
import { ScreenHeader, PrimaryButton } from '@components';
import { useAppDispatch, useAppSelector } from '@store';
import { completeJob } from '@store/slices/jobSlice';
import { completeJob } from '@store/commonReducers/job';
import { useNavigation } from '@react-navigation/native';
import { NativeStackNavigationProp } from '@react-navigation/native-stack';
import { AppStackParamList } from '@navigation/navigationTypes';

View File

@ -1 +1 @@
export { default } from "./deliverOrderScreen";
export * from './deliverOrderScreen';

View File

@ -1 +1 @@
export { default } from "./deliveryCompletedScreen";
export * from './deliveryCompletedScreen';

View File

@ -0,0 +1 @@
export * from './documentsScreen';

View File

@ -1,20 +1,22 @@
export { default as LoginScreen } from './LoginScreen/LoginScreen';
export { default as OtpScreen } from './OtpScreen/OtpScreen';
export { default as SetLocationScreen } from './setLocationScreen/setLocationScreen';
export { default as CompleteProfileScreen } from './completeProfileScreen/completeProfileScreen';
export { default as OnboardingCompleteScreen } from './onboardingCompleteScreen/onboardingCompleteScreen';
export { default as DashboardScreen } from './dashboardScreen/dashboardScreen';
export { default as EarningsScreen } from './earningsScreen/earningsScreen';
export { default as JobsScreen } from './jobsScreen/jobsScreen';
export { default as InboxScreen } from './inboxScreen/inboxScreen';
export { default as ProfileScreen } from './profileScreen/profileScreen';
export { default as DocumentsScreen } from './documentsScreen/documentsScreen';
export { default as NewJobRequestScreen } from './newJobRequestScreen/newJobRequestScreen';
export { default as OrderAcceptedScreen } from './orderAcceptedScreen/orderAcceptedScreen';
export { default as ArrivedAtStoreScreen } from './arrivedAtStoreScreen/arrivedAtStoreScreen';
export { default as ConfirmPickupScreen } from './confirmPickupScreen/confirmPickupScreen';
export { default as OrderPickedUpScreen } from './orderPickedUpScreen/orderPickedUpScreen';
export { default as LiveTrackingScreen } from './liveTrackingScreen/liveTrackingScreen';
export { default as ArrivedAtCustomerScreen } from './arrivedAtCustomerScreen/arrivedAtCustomerScreen';
export { default as DeliverOrderScreen } from './deliverOrderScreen/deliverOrderScreen';
export { default as DeliveryCompletedScreen } from './deliveryCompletedScreen/deliveryCompletedScreen';
export * from './LoginScreen/LoginScreen';
export * from './OtpScreen/OtpScreen';
export * from './setLocationScreen/setLocationScreen';
export * from './onboardingCompleteScreen/onboardingCompleteScreen';
export * from './dashboardScreen/dashboardScreen';
export * from './earningsScreen/earningsScreen';
export * from './jobsScreen/jobsScreen';
export * from './inboxScreen/inboxScreen';
export * from './profileScreen/profileScreen';
export * from './documentsScreen/documentsScreen';
export * from './newJobRequestScreen/newJobRequestScreen';
export * from './orderAcceptedScreen/orderAcceptedScreen';
export * from './arrivedAtStoreScreen/arrivedAtStoreScreen';
export * from './confirmPickupScreen/confirmPickupScreen';
export * from './orderPickedUpScreen/orderPickedUpScreen';
export * from './liveTrackingScreen/liveTrackingScreen';
export * from './arrivedAtCustomerScreen/arrivedAtCustomerScreen';
export * from './deliverOrderScreen/deliverOrderScreen';
export * from './deliveryCompletedScreen/deliveryCompletedScreen';
export * from './setLocationScreen';
export * from './completeProfileScreen';
export * from './kycScreen';

View File

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

View File

@ -0,0 +1,130 @@
import { StyleSheet } from 'react-native';
import { typography, spacing, borderRadius } from '@theme';
export const getStyles = (colors: any) =>
StyleSheet.create({
container: {
flex: 1,
backgroundColor: colors.background,
},
scrollContainer: {
paddingHorizontal: spacing.lg,
paddingTop: spacing.md,
paddingBottom: spacing.xxxl,
},
// Top Compliance banner card
bannerCard: {
flexDirection: 'row',
backgroundColor: colors.primaryLight,
borderWidth: 1,
borderColor: colors.border,
borderRadius: borderRadius.md,
padding: spacing.md,
marginBottom: spacing.lg,
alignItems: 'center',
},
bannerIconWrapper: {
width: 44,
height: 44,
borderRadius: 22,
backgroundColor: colors.white,
justifyContent: 'center',
alignItems: 'center',
marginRight: spacing.md,
shadowColor: '#000',
shadowOffset: { width: 0, height: 1 },
shadowOpacity: 0.05,
shadowRadius: 2,
elevation: 1,
},
bannerTextContainer: {
flex: 1,
},
bannerTitle: {
fontSize: 12,
fontWeight: typography.fontWeight.bold,
color: colors.text,
letterSpacing: 0.5,
textTransform: 'uppercase',
},
bannerSubtitle: {
fontSize: typography.fontSize.xs,
color: colors.textSecondary,
marginTop: 2,
lineHeight: 16,
},
// Main Card
mainCard: {
backgroundColor: colors.cardBg,
borderRadius: borderRadius.md,
borderWidth: 1,
borderColor: colors.border,
padding: spacing.md,
marginBottom: spacing.lg,
},
mainCardHeader: {
marginBottom: spacing.md,
},
mainCardTitle: {
fontSize: typography.fontSize.md,
fontWeight: typography.fontWeight.bold,
color: colors.text,
},
mainCardSubtitle: {
fontSize: typography.fontSize.xs,
color: colors.textSecondary,
marginTop: 2,
},
divider: {
height: 1,
backgroundColor: colors.border,
marginVertical: spacing.md,
},
// Add Document Button
addButtonContainer: {
alignItems: 'flex-start',
marginBottom: spacing.xl,
},
addBtn: {
flexDirection: 'row',
alignItems: 'center',
borderWidth: 1,
borderColor: colors.border,
borderRadius: borderRadius.sm,
paddingVertical: spacing.sm,
paddingHorizontal: spacing.md,
backgroundColor: colors.inputBg,
},
addBtnText: {
fontSize: typography.fontSize.sm,
fontWeight: typography.fontWeight.semibold,
color: colors.text,
marginLeft: spacing.xs,
},
// Submit Button
submitBtn: {
flexDirection: 'row',
backgroundColor: colors.primary,
borderRadius: borderRadius.sm,
height: 48,
alignItems: 'center',
justifyContent: 'center',
marginTop: spacing.sm,
shadowColor: colors.primary,
shadowOffset: { width: 0, height: 2 },
shadowOpacity: 0.2,
shadowRadius: 4,
elevation: 4,
},
submitBtnDisabled: {
backgroundColor: colors.border,
shadowOpacity: 0,
elevation: 0,
},
submitBtnText: {
fontSize: typography.fontSize.md,
fontWeight: typography.fontWeight.bold,
color: colors.white,
marginLeft: spacing.sm,
},
});

View File

@ -0,0 +1,355 @@
import React, { useState } from 'react';
import {
View,
Text,
ScrollView,
TouchableOpacity,
Alert,
ActivityIndicator,
} from 'react-native';
import { useAppTheme } from '@theme';
import { ScreenHeader, KycDocumentCard, LoadingOverlay } from '@components';
import { useNavigation, useFocusEffect } from '@react-navigation/native';
import { NativeStackNavigationProp } from '@react-navigation/native-stack';
import { OnBoardingParamList } from '@navigation/navigationTypes';
import { RouteNames } from '@utils/constants';
import Svg, { Path } from 'react-native-svg';
import { useAppDispatch, useAppSelector } from '@store';
import { uploadKyc, fetchMyKyc } from './thunk';
import { getStyles } from './kycScreen.styles';
// Interface for Document state matching KycDocumentCard structure
interface KycDocument {
id: string;
name: string;
idNumber: string;
fileName: string | null;
fileUri: string | null;
fileType: string | null;
status?: string;
}
// Inline Cloud Upload SVG representation for the top banner card
const CloudUploadIcon = ({ size = 22, color = '#666' }) => (
<Svg width={size} height={size} viewBox="0 0 24 24" fill="none">
<Path
d="M17.5 19A5.5 5.5 0 0 0 18 8h-1.26A8 8 0 1 0 3 16.28M12 12v9m0-9l-3 3m3-3l3 3"
stroke={color}
strokeWidth="2"
strokeLinecap="round"
strokeLinejoin="round"
/>
</Svg>
);
// Inline Plus icon for adding a new document
const PlusIcon = ({ size = 16, color = '#333' }) => (
<Svg width={size} height={size} viewBox="0 0 24 24" fill="none">
<Path
d="M12 5v14M5 12h14"
stroke={color}
strokeWidth="2.5"
strokeLinecap="round"
strokeLinejoin="round"
/>
</Svg>
);
// Inline Submit/Disk icon for the submit button
const SubmitIcon = ({ size = 18, color = '#fff' }) => (
<Svg width={size} height={size} viewBox="0 0 24 24" fill="none">
<Path
d="M19 21H5a2 2 0 0 1-2-2v-5a2 2 0 0 1 2-2h11l5 5v11a2 2 0 0 1-2 2z"
stroke={color}
strokeWidth="2"
strokeLinecap="round"
strokeLinejoin="round"
/>
<Path
d="M17 21v-8H7v8M7 3v5h8"
stroke={color}
strokeWidth="2"
strokeLinecap="round"
strokeLinejoin="round"
/>
</Svg>
);
export const KycScreen: React.FC = () => {
const { colors } = useAppTheme();
const styles = getStyles(colors);
const navigation =
useNavigation<NativeStackNavigationProp<OnBoardingParamList>>();
const dispatch = useAppDispatch();
// Get upload loading status from KYC store slice
const { isLoading, myKycResponse } = useAppSelector(state => state.kyc);
const [documents, setDocuments] = useState<KycDocument[]>([
{
id: '1',
name: '',
idNumber: '',
fileName: null,
fileUri: null,
fileType: null,
},
]);
// Fetch current KYC verification status when screen is focused
useFocusEffect(
React.useCallback(() => {
dispatch(fetchMyKyc());
}, [dispatch]),
);
const handleAddDocument = () => {
setDocuments([
...documents,
{
id: Date.now().toString(),
name: '',
idNumber: '',
fileName: null,
fileUri: null,
fileType: null,
},
]);
};
const handleRemoveDocument = (id: string) => {
setDocuments(documents.filter(doc => doc.id !== id));
};
const handleUpdateDocument = (
id: string,
updatedFields: Partial<KycDocument>,
) => {
setDocuments(
documents.map(doc =>
doc.id === id ? { ...doc, ...updatedFields } : doc,
),
);
};
const handleSubmit = () => {
// Validation
for (let i = 0; i < documents.length; i++) {
const doc = documents[i];
if (!doc.name.trim()) {
Alert.alert(
'Required Field',
`Please fill in the Document Name/Type for Document ${i + 1}.`,
);
return;
}
if (!doc.idNumber.trim()) {
Alert.alert(
'Required Field',
`Please fill in the Document Identification Number for Document ${
i + 1
}.`,
);
return;
}
if (!doc.fileName || !doc.fileUri) {
Alert.alert(
'Required Field',
`Please select and attach a document file for Document ${i + 1}.`,
);
return;
}
}
// Build files and metadata array payloads
const payloadFiles = documents.map(doc => ({
uri: doc.fileUri || '',
name: doc.fileName || '',
type: doc.fileName?.endsWith('.pdf') ? 'application/pdf' : 'image/png',
}));
const payloadMetadata = documents.map(doc => ({
docName: doc.name,
docNumber: doc.idNumber,
}));
dispatch(
uploadKyc({
files: payloadFiles,
metadata: payloadMetadata,
}),
)
.unwrap()
.then(() => {
Alert.alert('Success', 'KYC Documents submitted successfully!', [
{
text: 'OK',
onPress: () => {
dispatch(fetchMyKyc());
navigation.navigate(RouteNames.OnboardingComplete);
},
},
]);
})
.catch(err => {
Alert.alert(
'Upload Failed',
err || 'Failed to submit KYC documents. Please try again.',
);
});
};
// Determine if documents exist to render in read-only mode
const hasSubmittedDocs = !!(
myKycResponse &&
myKycResponse.documents &&
myKycResponse.documents.length > 0
);
// Map backend documents if submitted, else use local state
const displayedDocuments: KycDocument[] = hasSubmittedDocs
? myKycResponse.documents.map(doc => ({
id: doc.id,
name: doc.documentType,
idNumber: doc.documentNumber,
fileName: doc.fileName,
fileUri: doc.fileUrl,
fileType: doc.fileName.endsWith('.pdf')
? 'application/pdf'
: 'image/png',
status: doc.status,
}))
: documents;
return (
<View style={styles.container}>
<ScreenHeader
title="KYC Verification"
showBack={true}
onBack={() => navigation.goBack()}
/>
<ScrollView
contentContainerStyle={styles.scrollContainer}
showsVerticalScrollIndicator={false}
>
{/* Compliance Check Required Banner */}
<View style={styles.bannerCard}>
<View style={styles.bannerIconWrapper}>
<CloudUploadIcon size={22} color={colors.primary} />
</View>
<View style={styles.bannerTextContainer}>
<Text style={styles.bannerTitle}>
KYC Compliance Check Required
</Text>
<Text style={styles.bannerSubtitle}>
Please upload store identity documents to configure payouts,
deliveries, and operational scopes.
</Text>
</View>
</View>
{/* Upload KYC verification documents main card header info */}
<View style={styles.mainCardHeader}>
<Text style={styles.mainCardTitle}>
{hasSubmittedDocs
? 'Submitted KYC Documents'
: 'Upload KYC Verification Documents'}
</Text>
<Text style={styles.mainCardSubtitle}>
{hasSubmittedDocs
? 'Below are the KYC documents submitted for verification.'
: 'Provide details and attachments to initiate compliance verification.'}
</Text>
<View style={styles.divider} />
</View>
{/* Render each document card */}
{displayedDocuments.map((doc, idx) => (
<KycDocumentCard
key={doc.id}
document={doc}
index={idx}
onUpdate={
!hasSubmittedDocs
? fields => handleUpdateDocument(doc.id, fields)
: undefined
}
onRemove={
!hasSubmittedDocs && idx > 0
? () => handleRemoveDocument(doc.id)
: undefined
}
readOnly={hasSubmittedDocs}
/>
))}
{!hasSubmittedDocs && (
<>
{/* Add another document button */}
<View style={styles.addButtonContainer}>
<TouchableOpacity
style={styles.addBtn}
onPress={handleAddDocument}
activeOpacity={0.7}
>
<PlusIcon size={14} color={colors.text} />
<Text style={styles.addBtnText}>Add Another Document</Text>
</TouchableOpacity>
</View>
{/* Upload & Submit KYC Action Button */}
<TouchableOpacity
style={[
styles.submitBtn,
isLoading ? styles.submitBtnDisabled : null,
]}
onPress={handleSubmit}
disabled={isLoading}
activeOpacity={0.8}
>
{isLoading ? (
<ActivityIndicator color={colors.white} />
) : (
<>
<SubmitIcon size={18} color={colors.white} />
<Text style={styles.submitBtnText}>Upload & Submit KYC</Text>
</>
)}
</TouchableOpacity>
</>
)}
{hasSubmittedDocs && (
<TouchableOpacity
style={[
styles.submitBtn,
isLoading ? styles.submitBtnDisabled : null,
]}
onPress={() => {
navigation.navigate(RouteNames.OnboardingComplete);
}}
disabled={isLoading}
activeOpacity={0.8}
>
{isLoading ? (
<ActivityIndicator color={colors.white} />
) : (
<>
<SubmitIcon size={18} color={colors.white} />
<Text style={styles.submitBtnText}>Next Page</Text>
</>
)}
</TouchableOpacity>
)}
</ScrollView>
{/* Loading Overlay for the focus fetch check */}
<LoadingOverlay
visible={isLoading && !myKycResponse}
message="Checking KYC status..."
/>
</View>
);
};
export default KycScreen;

View File

@ -0,0 +1,48 @@
import { createReducer } from '@reduxjs/toolkit';
import { KycDocUploadResponse } from '@interfaces';
import { fetchMyKyc, uploadKyc } from './thunk';
import { MyKycResponse } from '@interfaces';
export interface KycState {
isLoading: boolean;
error: string | null;
uploadResponse: KycDocUploadResponse | null;
myKycResponse: MyKycResponse | null;
}
const initialState: KycState = {
isLoading: false,
error: null,
uploadResponse: null,
myKycResponse: null,
};
export const kycReducer = createReducer(initialState, builder => {
builder
.addCase(uploadKyc.pending, state => {
state.isLoading = true;
state.error = null;
})
.addCase(uploadKyc.fulfilled, (state, action) => {
state.isLoading = false;
state.uploadResponse = action.payload;
})
.addCase(uploadKyc.rejected, (state, action) => {
state.isLoading = false;
state.error = action.payload as string;
});
builder
.addCase(fetchMyKyc.pending, state => {
state.isLoading = true;
state.error = null;
})
.addCase(fetchMyKyc.fulfilled, (state, action) => {
state.isLoading = false;
state.myKycResponse = action.payload;
})
.addCase(fetchMyKyc.rejected, (state, action) => {
state.isLoading = false;
state.error = action.payload as string;
});
});

View File

@ -0,0 +1,34 @@
import { uploadKycDocuments, KycUploadPayload, getMyKyc } from '@api';
import { createAsyncThunk } from '@reduxjs/toolkit';
export const uploadKyc = createAsyncThunk(
'kyc/uploadKyc',
async (payload: KycUploadPayload, { rejectWithValue }) => {
try {
const response = await uploadKycDocuments(payload);
return response;
} catch (error: unknown) {
const message =
error instanceof Error
? error.message
: 'Failed to upload KYC documents';
return rejectWithValue(message);
}
},
);
export const fetchMyKyc = createAsyncThunk(
'kyc/fetchMyKyc',
async (_, { rejectWithValue }) => {
try {
const response = await getMyKyc();
return response;
} catch (error: unknown) {
const message =
error instanceof Error
? error.message
: 'Failed to fetch KYC documents';
return rejectWithValue(message);
}
},
);

View File

@ -3,7 +3,7 @@ import { View, Text, Alert } from 'react-native';
import { useAppTheme } from '@theme';
import { PrimaryButton } from '@components';
import { useAppDispatch, useAppSelector } from '@store';
import { advanceJobStatus } from '@store/slices/jobSlice';
import { advanceJobStatus } from '@store/commonReducers/job';
import { useNavigation } from '@react-navigation/native';
import { NativeStackNavigationProp } from '@react-navigation/native-stack';
import { AppStackParamList } from '@navigation/navigationTypes';

View File

@ -3,7 +3,7 @@ import { View, Text, Alert } from 'react-native';
import { useAppTheme } from '@theme';
import { PrimaryButton, LocationRow } from '@components';
import { useAppDispatch, useAppSelector } from '@store';
import { acceptJob, rejectJob } from '@store/slices/jobSlice';
import { acceptJobThunk, rejectJobThunk } from '@store/commonReducers/job';
import { useNavigation } from '@react-navigation/native';
import { NativeStackNavigationProp } from '@react-navigation/native-stack';
import { AppStackParamList } from '@navigation/navigationTypes';
@ -32,7 +32,9 @@ export const NewJobRequestScreen: React.FC = () => {
}, [secondsLeft]);
const handleAccept = () => {
dispatch(acceptJob());
if (newJobOffer?.jobId) {
dispatch(acceptJobThunk(newJobOffer.jobId));
}
Alert.alert('Job Accepted', 'Heading to Cafe Coffee Day pickup location.', [
{
text: 'OK',
@ -42,7 +44,9 @@ export const NewJobRequestScreen: React.FC = () => {
};
const handleReject = () => {
dispatch(rejectJob());
if (newJobOffer?.jobId) {
dispatch(rejectJobThunk(newJobOffer.jobId));
}
navigation.navigate(RouteNames.BottomTabs, { screen: RouteNames.Dashboard });
};

View File

@ -1 +1,3 @@
export { default } from './onboardingCompleteScreen';
export * from './onboardingCompleteScreen';
export * from './thunk';
export * from './reducer';

View File

@ -1,18 +1,30 @@
import React from 'react';
import React, { useCallback } from 'react';
import { View, Text } from 'react-native';
import Svg, { Circle, Path } from 'react-native-svg';
import { useAppTheme } from '@theme';
import { PrimaryButton } from '@components';
import { useNavigation } from '@react-navigation/native';
import { useFocusEffect, useNavigation } from '@react-navigation/native';
import { NativeStackNavigationProp } from '@react-navigation/native-stack';
import { RootStackParamList } from '@navigation/navigationTypes';
import { OnBoardingParamList } from '@navigation/navigationTypes';
import { RouteNames } from '@utils/constants';
import { getStyles } from './onboardingCompleteScreen.styles';
import { getDashboard } from './thunk';
import { useAppDispatch, useAppSelector } from '@store';
const OnboardingCompleteScreen: React.FC = () => {
export const OnboardingCompleteScreen: React.FC = () => {
const { colors } = useAppTheme();
const navigation =
useNavigation<NativeStackNavigationProp<OnBoardingParamList>>();
const styles = getStyles(colors);
const navigation = useNavigation<NativeStackNavigationProp<RootStackParamList>>();
const dispatch = useAppDispatch();
useFocusEffect(
useCallback(() => {
dispatch(getDashboard());
}, [dispatch]),
);
const { isKycApproved } = useAppSelector(state => state.onboardingComplete);
return (
<View style={styles.container}>
@ -34,21 +46,23 @@ const OnboardingCompleteScreen: React.FC = () => {
<Text style={styles.successTitle}>Onboarding Completed!</Text>
<Text style={styles.successDescription}>
Welcome to the SG Delivery family! Your account has been successfully verified. You are now ready to go online and start earning.
Welcome to the SG Delivery family! Your account has been successfully
verified. You are now ready to go online and start earning.
</Text>
<PrimaryButton
title="Get Started"
onPress={() =>
navigation.navigate('App', {
screen: RouteNames.BottomTabs,
params: { screen: RouteNames.Dashboard },
})
}
onPress={() => {
// If KYC is not yet approved, send user to KYC upload screen.
// If KYC is approved, the auth reducer has already updated
// user.status to ACTIVE via getDashboard.fulfilled, so the
// rootNavigator will automatically swap to AppStack.
if (!isKycApproved) {
navigation.navigate(RouteNames.Kyc);
}
}}
style={styles.startButton}
/>
</View>
);
};
export default OnboardingCompleteScreen;

View File

@ -0,0 +1,38 @@
import { DashboardResponce } from '@interfaces';
import { createReducer, createSlice } from '@reduxjs/toolkit';
import { getDashboard } from './thunk';
export interface OnBoardingCompleteState {
dashboardResponce: DashboardResponce | null;
isKycApproved: boolean | null;
isLoading: boolean;
error: string | null;
}
const initialState: OnBoardingCompleteState = {
dashboardResponce: null,
isKycApproved: null,
isLoading: false,
error: null,
};
export const onBoardingCompleteReducer = createReducer(
initialState,
builder => {
builder
.addCase(getDashboard.pending, state => {
state.isLoading = true;
state.error = null;
})
.addCase(getDashboard.fulfilled, (state, action) => {
state.isLoading = false;
state.isKycApproved = true;
state.dashboardResponce = action.payload;
})
.addCase(getDashboard.rejected, (state, action) => {
state.isLoading = false;
state.isKycApproved = false;
state.error = (action.payload as string) || 'Failed to get dashboard';
});
},
);

View File

@ -0,0 +1,16 @@
import { getDashboardApi } from '@api';
import { createAsyncThunk } from '@reduxjs/toolkit';
export const getDashboard = createAsyncThunk(
'onboardingComplete/getDashboard',
async (_, { rejectWithValue }) => {
try {
const response = await getDashboardApi();
return response;
} catch (error: unknown) {
const message =
error instanceof Error ? error.message : 'Failed to get dashboard';
return rejectWithValue(message);
}
},
);

View File

@ -3,7 +3,7 @@ import { View, Text } from 'react-native';
import { useAppTheme } from '@theme';
import { PrimaryButton } from '@components';
import { useAppDispatch, useAppSelector } from '@store';
import { advanceJobStatus } from '@store/slices/jobSlice';
import { advanceJobStatus } from '@store/commonReducers/job';
import { useNavigation } from '@react-navigation/native';
import { NativeStackNavigationProp } from '@react-navigation/native-stack';
import { AppStackParamList } from '@navigation/navigationTypes';

View File

@ -3,7 +3,7 @@ import { View, Text } from 'react-native';
import { useAppTheme } from '@theme';
import { PrimaryButton } from '@components';
import { useAppDispatch, useAppSelector } from '@store';
import { advanceJobStatus } from '@store/slices/jobSlice';
import { advanceJobStatus } from '@store/commonReducers/job';
import { useNavigation } from '@react-navigation/native';
import { NativeStackNavigationProp } from '@react-navigation/native-stack';
import { AppStackParamList } from '@navigation/navigationTypes';

View File

@ -3,7 +3,7 @@ import { View, Text, ScrollView, TouchableOpacity, Alert } from 'react-native';
import { useAppTheme } from '@theme';
import { PersonIcon, ChevronRightIcon, ClipboardIcon, StarIcon } from '@icons';
import { useAppDispatch } from '@store';
import { logout } from '@store/slices/authSlice';
import { logout } from '@store/commonReducers/auth';
import { useNavigation, CommonActions } from '@react-navigation/native';
import { NativeStackNavigationProp } from '@react-navigation/native-stack';
import { AppStackParamList } from '@navigation/navigationTypes';

View File

@ -1 +1,2 @@
export { default } from "./setLocationScreen";
export * from './setLocationScreen';
export * from './reducer';

View File

@ -0,0 +1,43 @@
import { createReducer, createAction } from '@reduxjs/toolkit';
// ─── Actions ──────────────────────────────────────────────────────────────────
export const setLocationData = createAction<{
latitude: number;
longitude: number;
mapAddress: string;
}>('setLocation/setLocationData');
export const clearLocationData = createAction('setLocation/clearLocationData');
// ─── State ────────────────────────────────────────────────────────────────────
export interface SetLocationState {
latitude: number | null;
longitude: number | null;
mapAddress: string;
}
const initialState: SetLocationState = {
latitude: null,
longitude: null,
mapAddress: '',
};
// ─── Reducer ──────────────────────────────────────────────────────────────────
const setLocationReducer = createReducer(initialState, builder => {
builder
.addCase(setLocationData, (state, action) => {
state.latitude = action.payload.latitude;
state.longitude = action.payload.longitude;
state.mapAddress = action.payload.mapAddress;
})
.addCase(clearLocationData, state => {
state.latitude = null;
state.longitude = null;
state.mapAddress = '';
});
});
export default setLocationReducer;

View File

@ -1,5 +1,5 @@
import { StyleSheet, Dimensions } from 'react-native';
import { typography, spacing, borderRadius, shadow } from '@theme';
import { StyleSheet, Platform, Dimensions } from 'react-native';
import { typography } from '@theme';
const { width } = Dimensions.get('window');
@ -9,87 +9,119 @@ export const getStyles = (colors: any) =>
flex: 1,
backgroundColor: colors.background,
},
mapContainer: {
// ---------- Map ----------
map: {
flex: 1,
backgroundColor: colors.inputBg,
justifyContent: 'center',
alignItems: 'center',
},
placeholderMap: {
// ---------- Locate me FAB ----------
locateButton: {
position: 'absolute',
right: 16,
bottom: Platform.OS === 'ios' ? 310 : 290,
width: 48,
height: 48,
borderRadius: 24,
backgroundColor: '#FFFFFF',
alignItems: 'center',
justifyContent: 'center',
...Platform.select({
ios: {
shadowColor: '#000',
shadowOffset: { width: 0, height: 3 },
shadowOpacity: 0.15,
shadowRadius: 6,
},
android: { elevation: 5 },
}),
},
mapText: {
fontSize: typography.fontSize.lg,
color: colors.textSecondary,
fontWeight: typography.fontWeight.semibold,
marginTop: spacing.md,
locateIcon: {
fontSize: 22,
},
mapSubText: {
fontSize: typography.fontSize.sm,
color: colors.placeholder,
marginTop: spacing.xs,
},
markerOverlay: {
// ---------- Bottom sheet ----------
bottomSheet: {
position: 'absolute',
justifyContent: 'center',
alignItems: 'center',
left: 0,
right: 0,
bottom: 0,
backgroundColor: colors.background,
borderTopLeftRadius: 24,
borderTopRightRadius: 24,
paddingHorizontal: 20,
paddingBottom: Platform.OS === 'ios' ? 34 : 20,
...Platform.select({
ios: {
shadowColor: '#000',
shadowOffset: { width: 0, height: -4 },
shadowOpacity: 0.1,
shadowRadius: 12,
},
android: { elevation: 12 },
}),
},
pulseCircle: {
position: 'absolute',
width: 80,
height: 80,
borderRadius: 40,
backgroundColor: colors.primary,
opacity: 0.15,
sheetHandle: {
alignSelf: 'center',
width: 40,
height: 4,
borderRadius: 2,
backgroundColor: colors.border,
marginTop: 10,
marginBottom: 16,
},
bottomCard: {
position: 'absolute',
bottom: spacing.xxl,
left: spacing.lg,
right: spacing.lg,
// ---------- Location card ----------
card: {
backgroundColor: colors.cardBg,
borderRadius: borderRadius.lg,
padding: spacing.xl,
...shadow.lg,
borderRadius: 16,
padding: 16,
marginBottom: 16,
borderWidth: 1,
borderColor: colors.border,
},
cardHeader: {
flexDirection: 'row',
alignItems: 'center',
marginBottom: spacing.md,
marginBottom: 8,
},
pinIconContainer: {
width: 36,
height: 36,
borderRadius: borderRadius.full,
backgroundColor: colors.primaryLight,
justifyContent: 'center',
alignItems: 'center',
marginRight: spacing.sm,
cardIcon: {
fontSize: 18,
marginRight: 8,
},
cardTitle: {
fontSize: typography.fontSize.lg,
fontWeight: typography.fontWeight.bold,
color: colors.text,
},
addressContainer: {
marginBottom: spacing.xl,
},
addressLabel: {
fontSize: typography.fontSize.xs,
fontWeight: typography.fontWeight.bold,
fontSize: typography.fontSize.sm,
fontWeight: typography.fontWeight.semibold,
color: colors.textSecondary,
textTransform: 'uppercase',
marginBottom: spacing.xs,
},
addressText: {
cardAddress: {
fontSize: typography.fontSize.md,
fontWeight: typography.fontWeight.semibold,
color: colors.text,
lineHeight: 22,
marginBottom: 8,
},
changeLink: {
alignSelf: 'flex-end',
},
changeText: {
fontSize: typography.fontSize.sm,
color: colors.primary,
fontWeight: typography.fontWeight.semibold,
},
// ---------- Buttons ----------
confirmButton: {
width: '100%',
borderRadius: 14,
marginBottom: 8,
},
manualButton: {
alignItems: 'center',
paddingVertical: 14,
},
manualButtonText: {
fontSize: typography.fontSize.sm,
color: colors.primary,
fontWeight: typography.fontWeight.semibold,
},
});

View File

@ -1,57 +1,180 @@
import React from 'react';
import { View, Text } from 'react-native';
import { useAppTheme } from '@theme';
import { PrimaryButton } from '@components';
import { LocationPinIcon } from '@icons';
import React, { useState, useEffect, useRef, useCallback } from 'react';
import {
View,
Text,
TouchableOpacity,
ActivityIndicator,
InteractionManager,
} from 'react-native';
import MapView, { Marker, Region } from 'react-native-maps';
import { useNavigation } from '@react-navigation/native';
import { NativeStackNavigationProp } from '@react-navigation/native-stack';
import { AuthStackParamList } from '@navigation/navigationTypes';
import { RouteNames } from '@utils/constants';
import { StackNavigationProp } from '@react-navigation/stack';
import { getStyles } from './setLocationScreen.styles';
import { PrimaryButton } from '@components';
import { useAppTheme } from '@theme';
const SetLocationScreen: React.FC = () => {
import { useAppDispatch } from '@store';
import { setLocationData } from './reducer';
import { OnBoardingParamList } from '@navigation/navigationTypes';
import { RouteNames } from '@utils/constants';
import {
DEFAULT_LOCATION,
getCurrentLocationWithAddress,
reverseGeocode,
showPermissionDeniedAlert,
LatLng,
} from '@services/locationService';
type NavProp = StackNavigationProp<OnBoardingParamList, RouteNames.SetLocation>;
const DELTA = { latitudeDelta: 0.006, longitudeDelta: 0.006 };
export const SetLocationScreen: React.FC = () => {
const { colors } = useAppTheme();
const styles = getStyles(colors);
const navigation = useNavigation<NativeStackNavigationProp<AuthStackParamList>>();
const navigation = useNavigation<NavProp>();
const dispatch = useAppDispatch();
const mapRef = useRef<MapView>(null);
// Guard: never call setState after the component has unmounted
const isMounted = useRef(true);
const [coords, setCoords] = useState<LatLng>(DEFAULT_LOCATION);
const [address, setAddress] = useState('Fetching your location…');
const [loading, setLoading] = useState(true);
// ------------------------------------------------------------------
// Fetch current location on mount
// ------------------------------------------------------------------
const fetchCurrentLocation = useCallback(async () => {
if (!isMounted.current) return;
setLoading(true);
try {
console.log('--------------');
const result = await getCurrentLocationWithAddress();
console.log('result', result);
if (!isMounted.current) return;
setCoords(result.coords);
setAddress(result.address);
// Animate the map to the user's location
mapRef.current?.animateToRegion({ ...result.coords, ...DELTA }, 600);
} catch (err: unknown) {
const message = err instanceof Error ? err.message : '';
if (message === 'Location permission denied') {
// Show the alert here — safe, outside the Promise chain
showPermissionDeniedAlert();
}
console.log('--------error------', err);
if (!isMounted.current) return;
setAddress('Could not determine location');
} finally {
if (isMounted.current) setLoading(false);
}
}, []);
useEffect(() => {
// Wait for the screen-transition animation to fully finish before
// calling PermissionsAndroid.request(). Calling it while the Activity
// is still transitioning causes a native crash on Android.
const task = InteractionManager.runAfterInteractions(() => {
fetchCurrentLocation();
});
return () => {
task.cancel();
isMounted.current = false;
};
}, [fetchCurrentLocation]);
// ------------------------------------------------------------------
// When user drags the map, update the marker + address
// ------------------------------------------------------------------
const handleRegionChangeComplete = useCallback(async (region: Region) => {
const newCoords: LatLng = {
latitude: region.latitude,
longitude: region.longitude,
};
setCoords(newCoords);
setAddress('Fetching address…');
const newAddress = await reverseGeocode(newCoords);
setAddress(newAddress);
}, []);
return (
<View style={styles.container}>
{/* Blank Map Placeholder Area */}
<View style={styles.mapContainer}>
<View style={styles.placeholderMap}>
{/* Pulsing marker simulation */}
<View style={styles.pulseCircle} />
<LocationPinIcon size={40} color={colors.primary} />
<Text style={styles.mapText}>Select Location</Text>
<Text style={styles.mapSubText}>Drag the map to pinpoint your location</Text>
</View>
</View>
{/* ---------- Map ---------- */}
<MapView
ref={mapRef}
style={styles.map}
initialRegion={{ ...DEFAULT_LOCATION, ...DELTA }}
onRegionChangeComplete={handleRegionChangeComplete}
>
<Marker
coordinate={coords}
title="Your Location"
description={address}
/>
</MapView>
{/* Bottom Sheet Location Selector Card */}
<View style={styles.bottomCard}>
<View style={styles.cardHeader}>
<View style={styles.pinIconContainer}>
<LocationPinIcon size={20} color={colors.primary} />
{/* ---------- "Locate me" floating button ---------- */}
<TouchableOpacity
style={styles.locateButton}
activeOpacity={0.8}
onPress={fetchCurrentLocation}
>
<Text style={styles.locateIcon}>📍</Text>
</TouchableOpacity>
{/* ---------- Bottom sheet ---------- */}
<View style={styles.bottomSheet}>
<View style={styles.sheetHandle} />
<View style={styles.card}>
<View style={styles.cardHeader}>
<Text style={styles.cardIcon}>📍</Text>
<Text style={styles.cardTitle}>My Location</Text>
</View>
<Text style={styles.cardTitle}>Choose Delivery Area</Text>
</View>
<View style={styles.addressContainer}>
<Text style={styles.addressLabel}>Current Address</Text>
<Text style={styles.addressText}>
MG Road, Ashok Nagar, Bengaluru, Karnataka 560001
</Text>
{loading ? (
<ActivityIndicator
size="small"
color={colors.primary}
style={{ marginVertical: 12 }}
/>
) : (
<Text style={styles.cardAddress} numberOfLines={2}>
{address}
</Text>
)}
<TouchableOpacity style={styles.changeLink} activeOpacity={0.7}>
<Text style={styles.changeText}>Change</Text>
</TouchableOpacity>
</View>
<PrimaryButton
title="Confirm Location"
onPress={() => navigation.navigate(RouteNames.CompleteProfile)}
onPress={() => {
dispatch(
setLocationData({
latitude: coords.latitude,
longitude: coords.longitude,
mapAddress: address,
}),
);
navigation.navigate(RouteNames.CompleteProfile);
}}
style={styles.confirmButton}
/>
<TouchableOpacity
style={styles.manualButton}
onPress={() => navigation.navigate(RouteNames.CompleteProfile)}
activeOpacity={0.7}
>
<Text style={styles.manualButtonText}>Enter address manually</Text>
</TouchableOpacity>
</View>
</View>
);
};
export default SetLocationScreen;

71
app/interfaces/auth.ts Normal file
View File

@ -0,0 +1,71 @@
export interface LoginResponse {
message: string;
phone: string;
code: string;
}
export interface VerifyOtpResponse {
message: string;
isNewUser: boolean;
accessToken: string;
refreshToken: string;
user: User;
}
export interface User {
id: string;
roleId: string;
roleEnum: RoleEnum;
name: string | null;
email: string | null;
phone: string;
profileImage: string | null;
status: UserStatus;
mfaEnabled: boolean;
createdAt: string;
updatedAt: string;
deletedAt: string | null;
isDeleted: boolean;
lastLogoutAt: string | null;
loginAt: string;
refreshTokenId: string;
refreshTokenHash: string;
refreshTokenExpiresAt: string;
categoryPreferences: CategoryPreference[];
gender: Gender | null;
dateOfBirth: string | null;
role: Role;
// merchants: Merchant[];
}
export interface Role {
id: string;
code: string;
scope: string;
isSystem: boolean;
}
export interface CategoryPreference {
// Extend when backend starts returning data
}
export interface Merchant {
name: string;
imageUrl: string | null;
}
export type RoleEnum = 'CUSTOMER' | 'DELIVERY';
export type UserStatus = 'ONBOARDING' | 'ACTIVE' | 'INACTIVE' | 'BLOCKED';
export enum Gender {
MALE = 'MALE',
FEMALE = 'FEMALE',
OTHER = 'OTHER',
}
export enum VehicleType {
BIKE = 'Bike',
CAR = 'Car',
SCOOTER = 'Scooter',
}

View File

@ -0,0 +1,9 @@
export interface ToggleStatusPayload {
availabilityStatus: string;
}
export interface ToggleStatusResponse {
id: string;
availabilityStatus: string;
message: string;
}

4
app/interfaces/index.ts Normal file
View File

@ -0,0 +1,4 @@
export * from './auth';
export * from './onboard';
export * from './kyc';
export * from './dashboard';

21
app/interfaces/kyc.ts Normal file
View File

@ -0,0 +1,21 @@
export interface KycDocUploadResponse {
message: string;
documents: KycDocUploadResponseDocument[];
}
export interface KycDocUploadResponseDocument {
id: string;
userId: string;
documentType: string;
documentNumber: string;
fileUrl: string;
fileName: string;
status: string;
createdAt: string;
verifiedAt: string | null;
}
export interface MyKycResponse {
userId: string;
documents: KycDocUploadResponseDocument[];
}

62
app/interfaces/onboard.ts Normal file
View File

@ -0,0 +1,62 @@
import { Gender, Role, VehicleType } from './auth';
export interface OnBoardPayload {
name: string;
email: string;
vehicleNumber: string;
vehicleType: VehicleType;
gender: Gender;
}
export interface UserProfile {
id: string;
roleId: string;
roleEnum:
| 'CUSTOMER'
| 'MERCHANT'
| 'BUSINESS_ADMIN'
| 'SUPER_ADMIN'
| 'DELIVERY';
name: string;
email: string;
phone: string;
profileImage: string | null;
status: 'ACTIVE' | 'INACTIVE' | 'SUSPENDED';
mfaEnabled: boolean;
createdAt: string;
updatedAt: string;
deletedAt: string | null;
isDeleted: boolean;
lastLogoutAt: string | null;
loginAt: string;
refreshTokenId: string;
refreshTokenHash: string;
refreshTokenExpiresAt: string;
categoryPreferences: string[];
gender: 'MALE' | 'FEMALE' | 'OTHER';
dateOfBirth: string;
role: Role;
}
export interface DashboardResponce {
message: string;
isOnboarded?: boolean;
isKycApproved?: boolean;
deliveryPartner: {
id: string;
userId: string;
partnerCode: string;
kycStatus: string;
status: string;
availabilityStatus: string;
rating: string;
currentLatitude: string;
currentLongitude: string;
lastLocationAt: string;
user: {
id: string;
name: string;
status: string;
};
};
}

View File

@ -17,18 +17,6 @@ const AuthStack: React.FC = () => {
<Stack.Navigator screenOptions={{ headerShown: false }}>
<Stack.Screen name={RouteNames.Login} component={LoginScreen} />
<Stack.Screen name={RouteNames.Otp} component={OtpScreen} />
<Stack.Screen
name={RouteNames.SetLocation}
component={SetLocationScreen}
/>
<Stack.Screen
name={RouteNames.CompleteProfile}
component={CompleteProfileScreen}
/>
<Stack.Screen
name={RouteNames.OnboardingComplete}
component={OnboardingCompleteScreen}
/>
</Stack.Navigator>
);
};

View File

@ -2,11 +2,11 @@ import React from 'react';
import { createBottomTabNavigator } from '@react-navigation/bottom-tabs';
import { BottomTabParamList } from '@navigation/navigationTypes';
import {
DashboardScreen,
EarningsScreen,
JobsScreen,
InboxScreen,
ProfileScreen,
DashboardScreen,
} from '@features/screens';
import { useAppTheme } from '@theme';
import { RouteNames } from '@utils/constants';
@ -25,29 +25,29 @@ const BottomTabNavigator: React.FC = () => {
tabBarStyle: { backgroundColor: colors.background },
}}
>
<Tab.Screen
name={RouteNames.Dashboard}
component={DashboardScreen}
<Tab.Screen
name={RouteNames.Dashboard}
component={DashboardScreen}
options={{ tabBarLabel: 'Home' }}
/>
<Tab.Screen
name={RouteNames.Earnings}
component={EarningsScreen}
<Tab.Screen
name={RouteNames.Earnings}
component={EarningsScreen}
options={{ tabBarLabel: 'Earning' }}
/>
<Tab.Screen
name={RouteNames.Jobs}
component={JobsScreen}
<Tab.Screen
name={RouteNames.Jobs}
component={JobsScreen}
options={{ tabBarLabel: 'Jobs' }}
/>
<Tab.Screen
name={RouteNames.Inbox}
component={InboxScreen}
<Tab.Screen
name={RouteNames.Inbox}
component={InboxScreen}
options={{ tabBarLabel: 'Inbox' }}
/>
<Tab.Screen
name={RouteNames.Profile}
component={ProfileScreen}
<Tab.Screen
name={RouteNames.Profile}
component={ProfileScreen}
options={{ tabBarLabel: 'Account' }}
/>
</Tab.Navigator>

View File

@ -4,8 +4,15 @@ import { RouteNames } from '@utils/constants';
export type AuthStackParamList = {
[RouteNames.Login]: undefined;
[RouteNames.Otp]: { phone: string };
// [RouteNames.SetLocation]: undefined;
// [RouteNames.CompleteProfile]: undefined;
// [RouteNames.OnboardingComplete]: undefined;
};
export type OnBoardingParamList = {
[RouteNames.SetLocation]: undefined;
[RouteNames.CompleteProfile]: undefined;
[RouteNames.Kyc]: undefined;
[RouteNames.OnboardingComplete]: undefined;
};
@ -34,4 +41,5 @@ export type AppStackParamList = {
export type RootStackParamList = {
Auth: NavigatorScreenParams<AuthStackParamList>;
App: NavigatorScreenParams<AppStackParamList>;
OnBoarding: NavigatorScreenParams<OnBoardingParamList>;
};

View File

@ -0,0 +1,34 @@
import React from 'react';
import { createNativeStackNavigator } from '@react-navigation/native-stack';
import { OnBoardingParamList } from '@navigation/navigationTypes';
import {
SetLocationScreen,
CompleteProfileScreen,
OnboardingCompleteScreen,
KycScreen,
} from '@features/screens';
import { RouteNames } from '@utils/constants';
const Stack = createNativeStackNavigator<OnBoardingParamList>();
const onBoardingStack: React.FC = () => {
return (
<Stack.Navigator screenOptions={{ headerShown: false }}>
{/* <Stack.Screen
name={RouteNames.SetLocation}
component={SetLocationScreen}
/> */}
{/* <Stack.Screen
name={RouteNames.CompleteProfile}
component={CompleteProfileScreen}
/> */}
<Stack.Screen name={RouteNames.Kyc} component={KycScreen} />
<Stack.Screen
name={RouteNames.OnboardingComplete}
component={OnboardingCompleteScreen}
/>
</Stack.Navigator>
);
};
export default onBoardingStack;

View File

@ -1,10 +1,16 @@
import React from 'react';
import { NavigationContainer, DefaultTheme, DarkTheme } from '@react-navigation/native';
import {
NavigationContainer,
DefaultTheme,
DarkTheme,
} from '@react-navigation/native';
import { createNativeStackNavigator } from '@react-navigation/native-stack';
import { useColorScheme } from 'react-native';
import { RootStackParamList } from '@navigation/navigationTypes';
import AuthStack from './authStack';
import AppStack from './appStack';
import onBoardingStack from './onBoardingStack';
import { useAppSelector } from '@store';
const Stack = createNativeStackNavigator<RootStackParamList>();
@ -12,11 +18,21 @@ const RootNavigator: React.FC = () => {
const scheme = useColorScheme();
const theme = scheme === 'dark' ? DarkTheme : DefaultTheme;
const accessToken = useAppSelector(state => state.auth.accessToken);
const isOnboarding = useAppSelector(
state => state.auth.user?.status === 'ONBOARDING',
);
return (
<NavigationContainer theme={theme}>
<Stack.Navigator screenOptions={{ headerShown: false }}>
<Stack.Screen name="Auth" component={AuthStack} />
<Stack.Screen name="App" component={AppStack} />
{!accessToken ? (
<Stack.Screen name="Auth" component={AuthStack} />
) : isOnboarding ? (
<Stack.Screen name="OnBoarding" component={onBoardingStack} />
) : (
<Stack.Screen name="App" component={AppStack} />
)}
</Stack.Navigator>
</NavigationContainer>
);

221
app/services/apiClient.ts Normal file
View File

@ -0,0 +1,221 @@
import axios, {
AxiosInstance,
AxiosRequestConfig,
InternalAxiosRequestConfig,
AxiosResponse,
AxiosError,
} from 'axios';
import AsyncStorage from '@react-native-async-storage/async-storage';
import { Store } from '@reduxjs/toolkit';
// ─── Storage Keys ────────────────────────────────────────────────────────────
const STORAGE_KEYS = {
ACCESS_TOKEN: '@auth_access_token',
REFRESH_TOKEN: '@auth_refresh_token',
} as const;
// ─── Config ──────────────────────────────────────────────────────────────────
const BASE_URL = 'https://720b-202-8-116-13.ngrok-free.app'; // TODO: replace with your actual base URL
// ─── Token Helpers ───────────────────────────────────────────────────────────
export const tokenManager = {
async getAccessToken(): Promise<string | null> {
return AsyncStorage.getItem(STORAGE_KEYS.ACCESS_TOKEN);
},
async getRefreshToken(): Promise<string | null> {
return AsyncStorage.getItem(STORAGE_KEYS.REFRESH_TOKEN);
},
async setTokens(accessToken: string, refreshToken: string): Promise<void> {
await AsyncStorage.setItem(STORAGE_KEYS.ACCESS_TOKEN, accessToken);
await AsyncStorage.setItem(STORAGE_KEYS.REFRESH_TOKEN, refreshToken);
},
async clearTokens(): Promise<void> {
await AsyncStorage.removeItem(STORAGE_KEYS.ACCESS_TOKEN);
await AsyncStorage.removeItem(STORAGE_KEYS.REFRESH_TOKEN);
},
};
// ─── Axios Instance ──────────────────────────────────────────────────────────
const axiosInstance: AxiosInstance = axios.create({
baseURL: BASE_URL,
timeout: 15000,
headers: {
'Content-Type': 'application/json',
Accept: 'application/json',
},
});
// ─── Request Interceptor — Attach Access Token ──────────────────────────────
axiosInstance.interceptors.request.use(
async (config: InternalAxiosRequestConfig) => {
// Skip attaching token for auth endpoints (login, register, refresh)
const publicPaths = [
'/auth/otp/request',
'/auth/otp/verify',
'/auth/refresh-token',
];
const isPublic = publicPaths.some(path => config.url?.includes(path));
if (!isPublic) {
const token = await tokenManager.getAccessToken();
if (token) {
config.headers.Authorization = `Bearer ${token}`;
}
}
// When the body is FormData, delete the Content-Type header entirely.
// This forces the native XMLHttpRequest layer to auto-generate the correct
// header: 'multipart/form-data; boundary=------xxxx'. The boundary is
// required for the server to parse the multipart body — it cannot be set
// manually because only XMLHttpRequest knows the generated boundary string.
if (config.data instanceof FormData) {
delete config.headers['Content-Type'];
}
// Debug logging (remove in production)
if (__DEV__) {
// console.log(`🚀 [API] ${config.method?.toUpperCase()} ${config.url}`);
}
return config;
},
(error: AxiosError) => {
return Promise.reject(error);
},
);
// ─── Response Interceptor — Handle Token Refresh ────────────────────────────
let isRefreshing = false;
let failedQueue: Array<{
resolve: (token: string) => void;
reject: (error: unknown) => void;
}> = [];
const processQueue = (error: unknown, token: string | null = null) => {
failedQueue.forEach(({ resolve, reject }) => {
if (error) {
reject(error);
} else if (token) {
resolve(token);
}
});
failedQueue = [];
};
axiosInstance.interceptors.response.use(
(response: AxiosResponse) => {
return response;
},
async (error: AxiosError) => {
const originalRequest = error.config as InternalAxiosRequestConfig & {
_retry?: boolean;
};
// If 401 and we haven't already retried this request
if (error.response?.status === 401 && !originalRequest._retry) {
// If already refreshing, queue this request
if (isRefreshing) {
return new Promise<AxiosResponse>((resolve, reject) => {
failedQueue.push({
resolve: (token: string) => {
originalRequest.headers.Authorization = `Bearer ${token}`;
resolve(axiosInstance(originalRequest));
},
reject,
});
});
}
originalRequest._retry = true;
isRefreshing = true;
try {
const refreshToken = await tokenManager.getRefreshToken();
if (!refreshToken) {
throw new Error('No refresh token available');
}
// Call refresh endpoint (uses a fresh axios call to avoid interceptor loop)
const { data } = await axios.post(`${BASE_URL}/auth/refresh-token`, {
refreshToken,
});
const { accessToken: newAccessToken, refreshToken: newRefreshToken } =
data;
await tokenManager.setTokens(newAccessToken, newRefreshToken);
// Retry all queued requests with the new token
processQueue(null, newAccessToken);
// Retry the original request
originalRequest.headers.Authorization = `Bearer ${newAccessToken}`;
return axiosInstance(originalRequest);
} catch (refreshError) {
processQueue(refreshError, null);
await tokenManager.clearTokens();
// TODO: Navigate to login screen or dispatch a logout action
// e.g. store.dispatch(logout());
return Promise.reject(refreshError);
} finally {
isRefreshing = false;
}
}
// For all other errors, reject as-is
return Promise.reject(error);
},
);
let storeInstance: Store | null = null;
export const injectStore = (_store: Store | null): void => {
storeInstance = _store;
};
// ─── API Methods ─────────────────────────────────────────────────────────────
export const apiClient = {
async get<T>(url: string, config?: AxiosRequestConfig): Promise<T> {
const response = await axiosInstance.get<T>(url, config);
return response.data;
},
async post<T>(
url: string,
body?: unknown,
config?: AxiosRequestConfig,
): Promise<T> {
const response = await axiosInstance.post<T>(url, body, config);
return response.data;
},
async put<T>(
url: string,
body?: unknown,
config?: AxiosRequestConfig,
): Promise<T> {
const response = await axiosInstance.put<T>(url, body, config);
return response.data;
},
async patch<T>(
url: string,
body?: unknown,
config?: AxiosRequestConfig,
): Promise<T> {
const response = await axiosInstance.patch<T>(url, body, config);
return response.data;
},
async delete<T>(url: string, config?: AxiosRequestConfig): Promise<T> {
const response = await axiosInstance.delete<T>(url, config);
return response.data;
},
};
// Export the raw instance if needed for advanced usage
export { axiosInstance };

2
app/services/index.ts Normal file
View File

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

View File

@ -1,42 +1,238 @@
import { Platform, PermissionsAndroid } from 'react-native';
import { request, PERMISSIONS, RESULTS } from 'react-native-permissions';
import { Coordinates } from '@app-types/index';
import { Platform, PermissionsAndroid, Alert, Linking } from 'react-native';
import Geolocation from 'react-native-geolocation-service';
import type { GeoPosition, GeoError } from 'react-native-geolocation-service';
class LocationService {
async requestPermission(): Promise<boolean> {
if (Platform.OS === 'android') {
const granted = await PermissionsAndroid.request(
PermissionsAndroid.PERMISSIONS.ACCESS_FINE_LOCATION,
{
title: 'Location Permission',
message: 'This app needs access to your location to track deliveries.',
buttonNeutral: 'Ask Me Later',
buttonNegative: 'Cancel',
buttonPositive: 'OK',
}
);
return granted === PermissionsAndroid.RESULTS.GRANTED;
} else {
const result = await request(PERMISSIONS.IOS.LOCATION_WHEN_IN_USE);
return result === RESULTS.GRANTED;
// ---------------------------------------------------------------------------
// Types
// ---------------------------------------------------------------------------
export interface LatLng {
latitude: number;
longitude: number;
}
export interface LocationResult {
coords: LatLng;
address: string;
}
// ---------------------------------------------------------------------------
// Constants
// ---------------------------------------------------------------------------
const GOOGLE_MAPS_API_KEY = 'AIzaSyDKaKVyQCmSpHMXcMkNCccSXpILpuvwsVM';
/** Default region (Bengaluru) used as a fallback before the user's location loads. */
export const DEFAULT_LOCATION: LatLng = {
latitude: 12.9352,
longitude: 77.6245,
};
// ---------------------------------------------------------------------------
// Permission helpers
// ---------------------------------------------------------------------------
/**
* Request location permission from the user.
* Returns `true` if permission was granted, `false` otherwise.
*/
export const requestLocationPermission = async (): Promise<boolean> => {
if (Platform.OS === 'ios') {
try {
const result = await Geolocation.requestAuthorization('whenInUse');
return result === 'granted';
} catch {
return false;
}
}
// Mocked location resolver
async getCurrentLocation(): Promise<{ coordinates: Coordinates; address: string; city: string; state: string; pincode: string }> {
// Return mock coordinates and address after a delay
await new Promise<void>(resolve => setTimeout(resolve, 800));
return {
coordinates: {
latitude: 12.9716,
longitude: 77.5946,
},
address: 'Koramangala 4th Block, Bengaluru',
city: 'Bengaluru',
state: 'Karnataka',
pincode: '560034',
};
}
}
// Android — check first to avoid showing the dialog when already granted
try {
const alreadyGranted = await PermissionsAndroid.check(
PermissionsAndroid.PERMISSIONS.ACCESS_FINE_LOCATION,
);
if (alreadyGranted) {
return true;
}
export const locationService = new LocationService();
const granted = await PermissionsAndroid.request(
PermissionsAndroid.PERMISSIONS.ACCESS_FINE_LOCATION,
{
title: 'Location Permission',
message: 'This app needs access to your location to set it on the map.',
buttonNeutral: 'Ask Me Later',
buttonNegative: 'Cancel',
buttonPositive: 'OK',
},
);
console.log('granted', granted);
return granted === PermissionsAndroid.RESULTS.GRANTED;
} catch {
return false;
}
};
/**
* Show an alert when the user denies location permission, with an option
* to open app settings.
*/
export const showPermissionDeniedAlert = (): void => {
Alert.alert(
'Location Permission Required',
'Please enable location permission in your device settings to use this feature.',
[
{ text: 'Cancel', style: 'cancel' },
{ text: 'Open Settings', onPress: () => Linking.openSettings() },
],
);
};
// ---------------------------------------------------------------------------
// Position helpers
// ---------------------------------------------------------------------------
/**
* Get the device's current GPS position.
* Wraps the callback-based Geolocation API in a Promise.
*/
/**
* Wraps Geolocation.getCurrentPosition in a Promise.
* Tries high-accuracy first; falls back to low-accuracy on failure.
*/
const getPositionOnce = (highAccuracy: boolean): Promise<LatLng> =>
new Promise<LatLng>((resolve, reject) => {
Geolocation.getCurrentPosition(
(position: GeoPosition) => {
resolve({
latitude: position.coords.latitude,
longitude: position.coords.longitude,
});
},
(error: GeoError) => {
console.log(
'Geolocation error (highAccuracy=' + highAccuracy + '):',
error,
);
reject(error);
},
{
enableHighAccuracy: highAccuracy,
timeout: highAccuracy ? 5000 : 10000,
maximumAge: 10000,
forceRequestLocation: false, // avoid native dialog that can crash
showLocationDialog: true,
},
);
});
export const getCurrentPosition = async (): Promise<LatLng> => {
console.log('getCurrentPosition called');
try {
// First attempt: high accuracy (GPS)
return await getPositionOnce(true);
} catch (highAccuracyError) {
console.warn(
'High-accuracy location failed, retrying with low accuracy…',
highAccuracyError,
);
try {
// Fallback: network / cell-tower accuracy
return await getPositionOnce(false);
} catch (lowAccuracyError) {
console.error('Failed to get current position:', lowAccuracyError);
throw lowAccuracyError;
}
}
};
// ---------------------------------------------------------------------------
// Geocoding
// ---------------------------------------------------------------------------
/**
* Reverse-geocode a lat/lng pair into a human-readable address string
* using the Google Maps Geocoding API.
*/
export const reverseGeocode = async (coords: LatLng): Promise<string> => {
try {
const url =
`https://maps.googleapis.com/maps/api/geocode/json` +
`?latlng=${coords.latitude},${coords.longitude}` +
`&key=${GOOGLE_MAPS_API_KEY}`;
const response = await fetch(url);
const data = await response.json();
if (data.status === 'OK' && data.results.length > 0) {
// Return the most specific formatted address.
return data.results[0].formatted_address;
}
console.warn(
'Google Maps Geocoding failed:',
data.status,
data.error_message || '',
);
// Fallback to OpenStreetMap Nominatim
const osmUrl = `https://nominatim.openstreetmap.org/reverse?format=jsonv2&lat=${coords.latitude}&lon=${coords.longitude}`;
const osmResponse = await fetch(osmUrl, {
headers: {
'User-Agent': 'SGDeliveryCustomerApp/1.0',
},
});
const osmData = await osmResponse.json();
if (osmData && osmData.display_name) {
return osmData.display_name;
}
return 'Address not found';
} catch (error) {
console.log('reverseGeocode error', error);
try {
// Fallback to OpenStreetMap Nominatim on catch
const osmUrl = `https://nominatim.openstreetmap.org/reverse?format=jsonv2&lat=${coords.latitude}&lon=${coords.longitude}`;
const osmResponse = await fetch(osmUrl, {
headers: {
'User-Agent': 'SGDeliveryCustomerApp/1.0',
},
});
const osmData = await osmResponse.json();
if (osmData && osmData.display_name) {
return osmData.display_name;
}
} catch (osmError) {
console.log('OSM fallback error', osmError);
}
return 'Unable to fetch address';
}
};
// ---------------------------------------------------------------------------
// Combined helper
// ---------------------------------------------------------------------------
/**
* All-in-one helper: request permissions get position reverse geocode.
* Returns both the coordinates and the human-readable address.
*
* Throws if permission is denied or position cannot be obtained.
*/
export const getCurrentLocationWithAddress =
async (): Promise<LocationResult> => {
const hasPermission = await requestLocationPermission();
console.log('hasPermission', hasPermission);
if (!hasPermission) {
// Alert shown by the caller so it doesn't fire inside a Promise chain
// on mount (which can cause a crash on some Android versions).
throw new Error('Location permission denied');
}
const coords = await getCurrentPosition();
const address = await reverseGeocode(coords);
console.log('coords', coords);
console.log('address', address);
return { coords, address };
};

View File

@ -1,73 +0,0 @@
import { createApi, fakeBaseQuery } from '@reduxjs/toolkit/query/react';
import { PartnerProfile } from '@app-types/index';
// Mock responses
const mockSendOtpResponse = { requestId: 'req_mock_001' };
const mockVerifyOtpResponse = { token: 'mock_token_xyz', partnerId: 'partner_001' };
const mockProfile: PartnerProfile = {
partnerId: 'partner_001',
fullName: 'Rahul Kumar',
email: 'rahul@example.com',
phone: '9876543210',
locationType: 'home',
location: {
coordinates: { latitude: 12.9716, longitude: 77.5946 },
address: 'Koramangala 4th Block',
city: 'Bengaluru',
state: 'Karnataka',
pincode: '560034',
},
avatarUri: null,
rating: 4.8,
totalDeliveries: 342,
acceptanceRate: 95,
completionRate: 98,
isVerified: true,
};
export const authApi = createApi({
reducerPath: 'authApi',
baseQuery: fakeBaseQuery(),
endpoints: (builder) => ({
sendOtp: builder.mutation<{ requestId: string }, { phone: string }>({
queryFn: async ({ phone: _phone }) => {
await new Promise<void>((r) => setTimeout(r, 1000));
return { data: mockSendOtpResponse };
},
}),
verifyOtp: builder.mutation<
{ token: string; partnerId: string },
{ requestId: string; otp: string }
>({
queryFn: async ({ otp }) => {
await new Promise<void>((r) => setTimeout(r, 1000));
if (otp === '000000') {
return { error: { status: 'CUSTOM_ERROR', error: 'Invalid OTP' } };
}
return { data: mockVerifyOtpResponse };
},
}),
getProfile: builder.query<PartnerProfile, string>({
queryFn: async (_partnerId) => {
await new Promise<void>((r) => setTimeout(r, 500));
return { data: mockProfile };
},
}),
updateProfile: builder.mutation<
PartnerProfile,
Partial<PartnerProfile>
>({
queryFn: async (updates) => {
await new Promise<void>((r) => setTimeout(r, 800));
return { data: { ...mockProfile, ...updates } };
},
}),
}),
});
export const {
useSendOtpMutation,
useVerifyOtpMutation,
useGetProfileQuery,
useUpdateProfileMutation,
} = authApi;

View File

@ -1,24 +0,0 @@
import { createApi, fakeBaseQuery } from '@reduxjs/toolkit/query/react';
import { DayEarning } from '@app-types/index';
import { mockWeeklyEarnings, mockTodayEarnings } from '@store/mockData/mockEarnings';
export const earningsApi = createApi({
reducerPath: 'earningsApi',
baseQuery: fakeBaseQuery(),
endpoints: (builder) => ({
getTodayEarnings: builder.query<typeof mockTodayEarnings, void>({
queryFn: async () => {
await new Promise<void>((r) => setTimeout(r, 500));
return { data: mockTodayEarnings };
},
}),
getWeeklyEarnings: builder.query<DayEarning[], void>({
queryFn: async () => {
await new Promise<void>((r) => setTimeout(r, 500));
return { data: mockWeeklyEarnings };
},
}),
}),
});
export const { useGetTodayEarningsQuery, useGetWeeklyEarningsQuery } = earningsApi;

View File

@ -1,61 +0,0 @@
import { createApi, fakeBaseQuery } from '@reduxjs/toolkit/query/react';
import { Job } from '@app-types/index';
import { JobStatus } from '@utils/constants';
import { mockJobOffer } from '@store/mockData/mockJobs';
export const jobApi = createApi({
reducerPath: 'jobApi',
baseQuery: fakeBaseQuery(),
endpoints: (builder) => ({
acceptJob: builder.mutation<Job, string>({
queryFn: async (jobId) => {
await new Promise<void>((r) => setTimeout(r, 500));
return {
data: {
...mockJobOffer,
jobId,
orderId: '#ORD125487',
status: JobStatus.Accepted,
acceptedAt: new Date().toISOString(),
collectAmount: 0,
},
};
},
}),
rejectJob: builder.mutation<void, string>({
queryFn: async (_jobId) => {
await new Promise<void>((r) => setTimeout(r, 300));
return { data: undefined };
},
}),
confirmPickup: builder.mutation<void, string>({
queryFn: async (_jobId) => {
await new Promise<void>((r) => setTimeout(r, 500));
return { data: undefined };
},
}),
confirmDelivery: builder.mutation<
{ earnings: number },
{ jobId: string; rating: number }
>({
queryFn: async ({ jobId: _jobId }) => {
await new Promise<void>((r) => setTimeout(r, 800));
return { data: { earnings: 78 } };
},
}),
reportIssue: builder.mutation<void, { jobId: string; reason: string }>({
queryFn: async (_args) => {
await new Promise<void>((r) => setTimeout(r, 500));
return { data: undefined };
},
}),
}),
});
export const {
useAcceptJobMutation,
useRejectJobMutation,
useConfirmPickupMutation,
useConfirmDeliveryMutation,
useReportIssueMutation,
} = jobApi;

View File

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

View File

@ -0,0 +1,148 @@
import { createReducer, createAction, PayloadAction } from '@reduxjs/toolkit';
import { PartnerProfile, PartnerLocation } from '@app-types/index';
import {
getProfileThunk,
updateProfileThunk,
loginWithPhone,
verifyOtp,
} from './thunk';
import { LoginResponse, User } from '@interfaces';
import { getDashboard } from '@features/screens/onboardingCompleteScreen/thunk';
export interface AuthState {
isAuthenticated: boolean;
loginData: LoginResponse | null;
user: User | null;
accessToken: string | null;
refreshToken: string | null;
isNewUser: boolean | null;
hasCompletedOnboarding: boolean;
isOnline: boolean;
token: string | null;
partnerId: string | null;
partnerProfile: PartnerProfile | null;
requestId: string | null;
loading: boolean;
error: string | null;
}
const initialState: AuthState = {
isAuthenticated: false,
user: null,
accessToken: null,
refreshToken: null,
isNewUser: null,
hasCompletedOnboarding: false,
isOnline: false,
token: null,
partnerId: null,
partnerProfile: null,
requestId: null,
loading: false,
error: null,
loginData: null,
};
export const setAuthenticated = createAction<{
token: string;
partnerId: string;
}>('auth/setAuthenticated');
export const setRequestId = createAction<string>('auth/setRequestId');
export const setOnboardingComplete = createAction('auth/setOnboardingComplete');
export const setOnlineStatus = createAction<boolean>('auth/setOnlineStatus');
export const setProfile = createAction<PartnerProfile>('auth/setProfile');
export const setLocation = createAction<PartnerLocation>('auth/setLocation');
export const logout = createAction('auth/logout');
export const authReducer = createReducer(initialState, builder => {
builder
// Sync Actions
.addCase(setAuthenticated, (state, action) => {
state.isAuthenticated = true;
state.token = action.payload.token;
state.partnerId = action.payload.partnerId;
})
.addCase(setRequestId, (state, action) => {
state.requestId = action.payload;
})
.addCase(setOnboardingComplete, state => {
state.hasCompletedOnboarding = true;
})
.addCase(setOnlineStatus, (state, action) => {
state.isOnline = action.payload;
})
.addCase(setProfile, (state, action) => {
state.partnerProfile = action.payload;
})
.addCase(setLocation, (state, action) => {
if (state.partnerProfile) {
state.partnerProfile.location = action.payload;
}
})
.addCase(logout, () => initialState)
// Async Thunks
// Login
.addCase(loginWithPhone.pending, state => {
state.loading = true;
state.error = null;
})
.addCase(loginWithPhone.fulfilled, (state, action) => {
state.loginData = action.payload;
state.loading = false;
state.error = null;
})
.addCase(loginWithPhone.rejected, (state, action) => {
state.loading = false;
state.error = action.payload as string;
})
// Verify OTP
.addCase(verifyOtp.pending, state => {
state.loading = true;
state.error = null;
})
.addCase(verifyOtp.fulfilled, (state, action) => {
const { user, accessToken, refreshToken, isNewUser } = action.payload;
state.user = user;
state.accessToken = accessToken;
state.refreshToken = refreshToken;
state.isNewUser = isNewUser;
state.loading = false;
state.error = null;
})
.addCase(verifyOtp.rejected, (state, action) => {
state.loading = false;
state.error = action.payload as string;
})
.addCase(getProfileThunk.pending, state => {
state.loading = true;
state.error = null;
})
.addCase(getProfileThunk.fulfilled, (state, action) => {
state.loading = false;
state.partnerProfile = action.payload;
})
.addCase(getProfileThunk.rejected, (state, action) => {
state.loading = false;
state.error = action.payload as string;
})
.addCase(updateProfileThunk.pending, state => {
state.loading = true;
state.error = null;
})
.addCase(updateProfileThunk.fulfilled, (state, action) => {
state.loading = false;
state.partnerProfile = action.payload;
})
.addCase(updateProfileThunk.rejected, (state, action) => {
state.loading = false;
state.error = action.payload as string;
})
// When dashboard is fetched, sync user.status so rootNavigator
// automatically swaps from OnBoardingStack → AppStack
.addCase(getDashboard.fulfilled, (state, action) => {
const serverStatus = action.payload?.deliveryPartner?.user?.status;
if (state.user && serverStatus) {
state.user.status = serverStatus as User['status'];
}
});
});

View File

@ -0,0 +1,66 @@
import { createAsyncThunk } from '@reduxjs/toolkit';
import { PartnerProfile } from '@app-types/index';
import { getProfile, loginApi, updateProfile, verifyOtpApi } from '@api';
import { tokenManager } from '@services';
export const loginWithPhone = createAsyncThunk(
'auth/loginWithPhone',
async (phone: string, { rejectWithValue }) => {
try {
const response = await loginApi(phone);
console.log(response);
return response;
} catch (error: unknown) {
const message = error instanceof Error ? error.message : 'Login failed';
return rejectWithValue(message);
}
},
);
export const verifyOtp = createAsyncThunk(
'auth/verifyOtp',
async (
{ phone, code, role }: { phone: string; code: string; role: string },
{ rejectWithValue },
) => {
try {
const response = await verifyOtpApi(phone, code, role);
// Persist tokens on successful verification
if (response.accessToken && response.refreshToken) {
await tokenManager.setTokens(
response.accessToken,
response.refreshToken,
);
}
return response;
} catch (error: unknown) {
const message =
error instanceof Error ? error.message : 'OTP verification failed';
return rejectWithValue(message);
}
},
);
export const getProfileThunk = createAsyncThunk<PartnerProfile, string>(
'auth/getProfile',
async (partnerId, { rejectWithValue }) => {
try {
return await getProfile(partnerId);
} catch (error: any) {
return rejectWithValue(error.message || 'Failed to get profile');
}
},
);
export const updateProfileThunk = createAsyncThunk<
PartnerProfile,
Partial<PartnerProfile>
>('auth/updateProfile', async (updates, { rejectWithValue }) => {
try {
return await updateProfile(updates);
} catch (error: any) {
return rejectWithValue(error.message || 'Failed to update profile');
}
});

View File

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

View File

@ -0,0 +1,74 @@
import { createReducer, createAction } from '@reduxjs/toolkit';
import { DayEarning } from '@app-types/index';
import { mockWeeklyEarnings, mockTodayEarnings } from '../../mockData/mockEarnings';
import { getTodayEarningsThunk, getWeeklyEarningsThunk } from './thunk';
export interface EarningsState {
todayEarnings: number;
completedCount: number;
onlineMinutes: number;
weeklyData: DayEarning[];
loading: boolean;
error: string | null;
}
const initialState: EarningsState = {
todayEarnings: mockTodayEarnings.total,
completedCount: mockTodayEarnings.completedCount,
onlineMinutes: mockTodayEarnings.onlineMinutes,
weeklyData: mockWeeklyEarnings,
loading: false,
error: null,
};
export const addEarning = createAction<number>('earnings/addEarning');
export const setEarnings = createAction<Partial<EarningsState>>('earnings/setEarnings');
export const incrementOnlineMinutes = createAction('earnings/incrementOnlineMinutes');
export const resetTodayEarnings = createAction('earnings/resetTodayEarnings');
export const earningsReducer = createReducer(initialState, (builder) => {
builder
// Sync Actions
.addCase(addEarning, (state, action) => {
state.todayEarnings += action.payload;
state.completedCount += 1;
})
.addCase(setEarnings, (state, action) => {
return { ...state, ...action.payload };
})
.addCase(incrementOnlineMinutes, (state) => {
state.onlineMinutes += 1;
})
.addCase(resetTodayEarnings, (state) => {
state.todayEarnings = 0;
state.completedCount = 0;
state.onlineMinutes = 0;
})
// Async Thunks
.addCase(getTodayEarningsThunk.pending, (state) => {
state.loading = true;
state.error = null;
})
.addCase(getTodayEarningsThunk.fulfilled, (state, action) => {
state.loading = false;
state.todayEarnings = action.payload.total;
state.completedCount = action.payload.completedCount;
state.onlineMinutes = action.payload.onlineMinutes;
})
.addCase(getTodayEarningsThunk.rejected, (state, action) => {
state.loading = false;
state.error = action.payload as string;
})
.addCase(getWeeklyEarningsThunk.pending, (state) => {
state.loading = true;
state.error = null;
})
.addCase(getWeeklyEarningsThunk.fulfilled, (state, action) => {
state.loading = false;
state.weeklyData = action.payload;
})
.addCase(getWeeklyEarningsThunk.rejected, (state, action) => {
state.loading = false;
state.error = action.payload as string;
});
});

View File

@ -0,0 +1,26 @@
import { createAsyncThunk } from '@reduxjs/toolkit';
import { DayEarning } from '@app-types/index';
import * as earningsApi from '../../../api/earningsApi';
import { mockTodayEarnings } from '../../mockData/mockEarnings';
export const getTodayEarningsThunk = createAsyncThunk<typeof mockTodayEarnings, void>(
'earnings/getTodayEarnings',
async (_, { rejectWithValue }) => {
try {
return await earningsApi.getTodayEarnings();
} catch (error: any) {
return rejectWithValue(error.message || 'Failed to get today earnings');
}
}
);
export const getWeeklyEarningsThunk = createAsyncThunk<DayEarning[], void>(
'earnings/getWeeklyEarnings',
async (_, { rejectWithValue }) => {
try {
return await earningsApi.getWeeklyEarnings();
} catch (error: any) {
return rejectWithValue(error.message || 'Failed to get weekly earnings');
}
}
);

View File

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

View File

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

View File

@ -0,0 +1,151 @@
import { createReducer, createAction } from '@reduxjs/toolkit';
import { Job, JobOffer } from '@app-types/index';
import { JobStatus } from '@utils/constants';
import { acceptJobThunk, rejectJobThunk, confirmPickupThunk, confirmDeliveryThunk } from './thunk';
export interface JobState {
activeJob: Job | null;
jobStatus: JobStatus;
newJobOffer: JobOffer | null;
jobHistory: Job[];
loading: boolean;
error: string | null;
}
const initialState: JobState = {
activeJob: null,
jobStatus: JobStatus.Idle,
newJobOffer: null,
jobHistory: [],
loading: false,
error: null,
};
export const setNewJobOffer = createAction<JobOffer>('job/setNewJobOffer');
export const acceptJobSync = createAction('job/acceptJobSync');
export const rejectJobSync = createAction('job/rejectJobSync');
export const advanceJobStatus = createAction<JobStatus>('job/advanceJobStatus');
export const completeJob = createAction('job/completeJob');
export const clearJob = createAction('job/clearJob');
export const setJobHistory = createAction<Job[]>('job/setJobHistory');
export const jobReducer = createReducer(initialState, (builder) => {
builder
// Sync Actions
.addCase(setNewJobOffer, (state, action) => {
state.newJobOffer = action.payload;
state.jobStatus = JobStatus.NewRequest;
})
.addCase(acceptJobSync, (state) => {
if (state.newJobOffer) {
state.activeJob = {
...state.newJobOffer,
orderId: `#ORD${Date.now().toString().slice(-6)}`,
status: JobStatus.Accepted,
acceptedAt: new Date().toISOString(),
collectAmount: state.newJobOffer.paymentType === 'cod' ? state.newJobOffer.amount : 0,
};
state.jobStatus = JobStatus.Accepted;
state.newJobOffer = null;
}
})
.addCase(rejectJobSync, (state) => {
state.newJobOffer = null;
state.jobStatus = JobStatus.Idle;
})
.addCase(advanceJobStatus, (state, action) => {
state.jobStatus = action.payload;
if (state.activeJob) {
state.activeJob.status = action.payload;
if (action.payload === JobStatus.PickedUp) {
state.activeJob.pickedUpAt = new Date().toISOString();
}
}
})
.addCase(completeJob, (state) => {
if (state.activeJob) {
const completedJob: Job = {
...state.activeJob,
status: JobStatus.Delivered,
deliveredAt: new Date().toISOString(),
};
state.jobHistory = [completedJob, ...state.jobHistory];
}
state.activeJob = null;
state.jobStatus = JobStatus.Idle;
})
.addCase(clearJob, (state) => {
state.activeJob = null;
state.newJobOffer = null;
state.jobStatus = JobStatus.Idle;
})
.addCase(setJobHistory, (state, action) => {
state.jobHistory = action.payload;
})
// Async Thunks
.addCase(acceptJobThunk.pending, (state) => {
state.loading = true;
state.error = null;
})
.addCase(acceptJobThunk.fulfilled, (state, action) => {
state.loading = false;
state.activeJob = action.payload;
state.jobStatus = JobStatus.Accepted;
state.newJobOffer = null;
})
.addCase(acceptJobThunk.rejected, (state, action) => {
state.loading = false;
state.error = action.payload as string;
})
.addCase(rejectJobThunk.pending, (state) => {
state.loading = true;
state.error = null;
})
.addCase(rejectJobThunk.fulfilled, (state) => {
state.loading = false;
state.newJobOffer = null;
state.jobStatus = JobStatus.Idle;
})
.addCase(rejectJobThunk.rejected, (state, action) => {
state.loading = false;
state.error = action.payload as string;
})
.addCase(confirmPickupThunk.pending, (state) => {
state.loading = true;
state.error = null;
})
.addCase(confirmPickupThunk.fulfilled, (state) => {
state.loading = false;
state.jobStatus = JobStatus.PickedUp;
if (state.activeJob) {
state.activeJob.status = JobStatus.PickedUp;
state.activeJob.pickedUpAt = new Date().toISOString();
}
})
.addCase(confirmPickupThunk.rejected, (state, action) => {
state.loading = false;
state.error = action.payload as string;
})
.addCase(confirmDeliveryThunk.pending, (state) => {
state.loading = true;
state.error = null;
})
.addCase(confirmDeliveryThunk.fulfilled, (state) => {
state.loading = false;
// Complete Job logic can be triggered here or separately
if (state.activeJob) {
const completedJob: Job = {
...state.activeJob,
status: JobStatus.Delivered,
deliveredAt: new Date().toISOString(),
};
state.jobHistory = [completedJob, ...state.jobHistory];
}
state.activeJob = null;
state.jobStatus = JobStatus.Idle;
})
.addCase(confirmDeliveryThunk.rejected, (state, action) => {
state.loading = false;
state.error = action.payload as string;
});
});

View File

@ -0,0 +1,58 @@
import { createAsyncThunk } from '@reduxjs/toolkit';
import { Job } from '@app-types/index';
import * as jobApi from '../../../api/jobApi';
export const acceptJobThunk = createAsyncThunk<Job, string>(
'job/acceptJob',
async (jobId, { rejectWithValue }) => {
try {
return await jobApi.acceptJob(jobId);
} catch (error: any) {
return rejectWithValue(error.message || 'Failed to accept job');
}
}
);
export const rejectJobThunk = createAsyncThunk<void, string>(
'job/rejectJob',
async (jobId, { rejectWithValue }) => {
try {
return await jobApi.rejectJob(jobId);
} catch (error: any) {
return rejectWithValue(error.message || 'Failed to reject job');
}
}
);
export const confirmPickupThunk = createAsyncThunk<void, string>(
'job/confirmPickup',
async (jobId, { rejectWithValue }) => {
try {
return await jobApi.confirmPickup(jobId);
} catch (error: any) {
return rejectWithValue(error.message || 'Failed to confirm pickup');
}
}
);
export const confirmDeliveryThunk = createAsyncThunk<{ earnings: number }, { jobId: string; rating: number }>(
'job/confirmDelivery',
async ({ jobId, rating }, { rejectWithValue }) => {
try {
return await jobApi.confirmDelivery(jobId, rating);
} catch (error: any) {
return rejectWithValue(error.message || 'Failed to confirm delivery');
}
}
);
export const reportIssueThunk = createAsyncThunk<void, { jobId: string; reason: string }>(
'job/reportIssue',
async ({ jobId, reason }, { rejectWithValue }) => {
try {
return await jobApi.reportIssue(jobId, reason);
} catch (error: any) {
return rejectWithValue(error.message || 'Failed to report issue');
}
}
);

View File

@ -1,36 +1,4 @@
import { configureStore } from '@reduxjs/toolkit';
import { setupListeners } from '@reduxjs/toolkit/query';
import { TypedUseSelectorHook, useDispatch, useSelector } from 'react-redux';
import authReducer from './slices/authSlice';
import jobReducer from './slices/jobSlice';
import earningsReducer from './slices/earningsSlice';
import { authApi } from './api/authApi';
import { jobApi } from './api/jobApi';
import { earningsApi } from './api/earningsApi';
export const store = configureStore({
reducer: {
auth: authReducer,
job: jobReducer,
earnings: earningsReducer,
[authApi.reducerPath]: authApi.reducer,
[jobApi.reducerPath]: jobApi.reducer,
[earningsApi.reducerPath]: earningsApi.reducer,
},
middleware: (getDefaultMiddleware) =>
getDefaultMiddleware({
serializableCheck: false,
}).concat(
authApi.middleware,
jobApi.middleware,
earningsApi.middleware
),
});
setupListeners(store.dispatch);
export type RootState = ReturnType<typeof store.getState>;
export type AppDispatch = typeof store.dispatch;
export const useAppDispatch = () => useDispatch<AppDispatch>();
export const useAppSelector: TypedUseSelectorHook<RootState> = useSelector;
export * from './commonReducers';
export * from './migration';
export * from './rootReducer';
export * from './store';

10
app/store/migration.ts Normal file
View File

@ -0,0 +1,10 @@
/* eslint-disable @typescript-eslint/explicit-module-boundary-types */
// Here we avoid typing things as the more migrations we have the more complex types we will have to create
export const migrations = {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
1: (state: any) => state,
};
const migrationVersions: number[] = Object.keys(migrations).map(k => Number(k));
export const persistVersion = migrationVersions[migrationVersions.length - 1];

22
app/store/rootReducer.ts Normal file
View File

@ -0,0 +1,22 @@
import { combineReducers } from '@reduxjs/toolkit';
import { authReducer } from './commonReducers/auth';
import { earningsReducer } from './commonReducers/earnings';
import { jobReducer } from './commonReducers/job';
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';
const rootReducer = combineReducers({
auth: authReducer,
earnings: earningsReducer,
job: jobReducer,
setLocation: setLocationReducer,
onboard: completeProfileReducer,
kyc: kycReducer,
onboardingComplete: onBoardingCompleteReducer,
dashboard: dashBoardReducer,
});
export type RootState = ReturnType<typeof rootReducer>;
export default rootReducer;

View File

@ -1,67 +0,0 @@
import { createSlice, PayloadAction } from '@reduxjs/toolkit';
import { PartnerProfile, PartnerLocation } from '@app-types/index';
interface AuthState {
isAuthenticated: boolean;
hasCompletedOnboarding: boolean;
isOnline: boolean;
token: string | null;
partnerId: string | null;
partnerProfile: PartnerProfile | null;
requestId: string | null; // OTP request ID
}
const initialState: AuthState = {
isAuthenticated: false,
hasCompletedOnboarding: false,
isOnline: false,
token: null,
partnerId: null,
partnerProfile: null,
requestId: null,
};
const authSlice = createSlice({
name: 'auth',
initialState,
reducers: {
setAuthenticated: (
state,
action: PayloadAction<{ token: string; partnerId: string }>,
) => {
state.isAuthenticated = true;
state.token = action.payload.token;
state.partnerId = action.payload.partnerId;
},
setRequestId: (state, action: PayloadAction<string>) => {
state.requestId = action.payload;
},
setOnboardingComplete: (state) => {
state.hasCompletedOnboarding = true;
},
setOnlineStatus: (state, action: PayloadAction<boolean>) => {
state.isOnline = action.payload;
},
setProfile: (state, action: PayloadAction<PartnerProfile>) => {
state.partnerProfile = action.payload;
},
setLocation: (state, action: PayloadAction<PartnerLocation>) => {
if (state.partnerProfile) {
state.partnerProfile.location = action.payload;
}
},
logout: () => initialState,
},
});
export const {
setAuthenticated,
setRequestId,
setOnboardingComplete,
setOnlineStatus,
setProfile,
setLocation,
logout,
} = authSlice.actions;
export default authSlice.reducer;

View File

@ -1,48 +0,0 @@
import { createSlice, PayloadAction } from '@reduxjs/toolkit';
import { DayEarning } from '@app-types/index';
import { mockWeeklyEarnings, mockTodayEarnings } from '@store/mockData/mockEarnings';
interface EarningsState {
todayEarnings: number;
completedCount: number;
onlineMinutes: number;
weeklyData: DayEarning[];
}
const initialState: EarningsState = {
todayEarnings: mockTodayEarnings.total,
completedCount: mockTodayEarnings.completedCount,
onlineMinutes: mockTodayEarnings.onlineMinutes,
weeklyData: mockWeeklyEarnings,
};
const earningsSlice = createSlice({
name: 'earnings',
initialState,
reducers: {
addEarning: (state, action: PayloadAction<number>) => {
state.todayEarnings += action.payload;
state.completedCount += 1;
},
setEarnings: (state, action: PayloadAction<Partial<EarningsState>>) => {
return { ...state, ...action.payload };
},
incrementOnlineMinutes: (state) => {
state.onlineMinutes += 1;
},
resetTodayEarnings: (state) => {
state.todayEarnings = 0;
state.completedCount = 0;
state.onlineMinutes = 0;
},
},
});
export const {
addEarning,
setEarnings,
incrementOnlineMinutes,
resetTodayEarnings,
} = earningsSlice.actions;
export default earningsSlice.reducer;

View File

@ -1,86 +0,0 @@
import { createSlice, PayloadAction } from '@reduxjs/toolkit';
import { Job, JobOffer } from '@app-types/index';
import { JobStatus } from '@utils/constants';
interface JobState {
activeJob: Job | null;
jobStatus: JobStatus;
newJobOffer: JobOffer | null;
jobHistory: Job[];
}
const initialState: JobState = {
activeJob: null,
jobStatus: JobStatus.Idle,
newJobOffer: null,
jobHistory: [],
};
const jobSlice = createSlice({
name: 'job',
initialState,
reducers: {
setNewJobOffer: (state, action: PayloadAction<JobOffer>) => {
state.newJobOffer = action.payload;
state.jobStatus = JobStatus.NewRequest;
},
acceptJob: (state) => {
if (state.newJobOffer) {
state.activeJob = {
...state.newJobOffer,
orderId: `#ORD${Date.now().toString().slice(-6)}`,
status: JobStatus.Accepted,
acceptedAt: new Date().toISOString(),
collectAmount: state.newJobOffer.paymentType === 'cod' ? state.newJobOffer.amount : 0,
};
state.jobStatus = JobStatus.Accepted;
state.newJobOffer = null;
}
},
rejectJob: (state) => {
state.newJobOffer = null;
state.jobStatus = JobStatus.Idle;
},
advanceJobStatus: (state, action: PayloadAction<JobStatus>) => {
state.jobStatus = action.payload;
if (state.activeJob) {
state.activeJob.status = action.payload;
if (action.payload === JobStatus.PickedUp) {
state.activeJob.pickedUpAt = new Date().toISOString();
}
}
},
completeJob: (state) => {
if (state.activeJob) {
const completedJob: Job = {
...state.activeJob,
status: JobStatus.Delivered,
deliveredAt: new Date().toISOString(),
};
state.jobHistory = [completedJob, ...state.jobHistory];
}
state.activeJob = null;
state.jobStatus = JobStatus.Idle;
},
clearJob: (state) => {
state.activeJob = null;
state.newJobOffer = null;
state.jobStatus = JobStatus.Idle;
},
setJobHistory: (state, action: PayloadAction<Job[]>) => {
state.jobHistory = action.payload;
},
},
});
export const {
setNewJobOffer,
acceptJob,
rejectJob,
advanceJobStatus,
completeJob,
clearJob,
setJobHistory,
} = jobSlice.actions;
export default jobSlice.reducer;

53
app/store/store.ts Normal file
View File

@ -0,0 +1,53 @@
import { useDispatch, useSelector, TypedUseSelectorHook } from 'react-redux';
import AsyncStorage from '@react-native-async-storage/async-storage';
import {
createMigrate,
FLUSH,
PAUSE,
PERSIST,
persistReducer,
persistStore,
PURGE,
REGISTER,
REHYDRATE,
} from 'redux-persist';
import { ThunkAction } from 'redux-thunk';
import { Action, configureStore } from '@reduxjs/toolkit';
import { migrations, persistVersion } from './migration';
import rootReducer, { RootState } from './rootReducer';
import { injectStore } from '../services';
const persistConfig = {
key: 'root',
version: persistVersion,
storage: AsyncStorage,
blacklist: [],
migrate: createMigrate(migrations, { debug: __DEV__ }),
};
const persistedReducer = persistReducer(persistConfig, rootReducer);
export const store = configureStore({
reducer: persistedReducer,
devTools: __DEV__,
middleware: getDefaultMiddleware =>
getDefaultMiddleware({
immutableCheck: false,
serializableCheck: {
ignoredActions: [FLUSH, REHYDRATE, PAUSE, PERSIST, PURGE, REGISTER],
},
}),
});
injectStore(store);
export const persistor = persistStore(store);
export type AppDispatch = typeof store.dispatch;
export const useAppDispatch = (): AppDispatch => useDispatch<AppDispatch>();
export const useAppSelector: TypedUseSelectorHook<RootState> = useSelector;
export type AppThunk = ThunkAction<void, RootState, unknown, Action<string>>;
export default store;

View File

@ -29,6 +29,7 @@ export enum RouteNames {
DeliverOrder = 'DeliverOrder',
DeliveryCompleted = 'DeliveryCompleted',
Documents = 'Documents',
Kyc = 'Kyc',
}
export enum JobStatus {

View File

@ -11,11 +11,13 @@ module.exports = {
'@theme': './app/theme',
'@navigation': './app/navigation',
'@store': './app/store',
'@interfaces': './app/interfaces',
'@services': './app/services',
'@hooks': './app/hooks',
'@utils': './app/utils',
'@icons': './app/components/icons',
'@app-types': './app/types',
'@api': './app/api',
},
},
],

View File

@ -2,4 +2,8 @@ import { AppRegistry } from 'react-native';
import App from './app/App';
import { name as appName } from './app.json';
if (__DEV__) {
require('./ReactotronConfig');
}
AppRegistry.registerComponent(appName, () => App);

View File

@ -12,21 +12,29 @@
"dependencies": {
"@react-native-async-storage/async-storage": "^3.1.1",
"@react-native-community/checkbox": "^0.5.20",
"@react-native-documents/picker": "^12.0.1",
"@react-native/new-app-screen": "0.86.0",
"@react-navigation/bottom-tabs": "^7.18.5",
"@react-navigation/native": "^7.3.5",
"@react-navigation/bottom-tabs": "^7.18.3",
"@react-navigation/native": "^7.3.4",
"@react-navigation/native-stack": "^7.17.7",
"@react-navigation/stack": "^7.10.7",
"@react-navigation/stack": "^7.10.6",
"@reduxjs/toolkit": "^2.12.0",
"axios": "^1.18.1",
"react": "19.2.3",
"react-native": "0.86.0",
"react-native-gesture-handler": "^3.0.2",
"react-native-geolocation-service": "^5.3.1",
"react-native-gesture-handler": "^2.32.0",
"react-native-maps": "^1.29.0",
"react-native-permissions": "^5.6.0",
"react-native-reanimated": "^4.5.0",
"react-native-safe-area-context": "^5.8.0",
"react-native-screens": "^4.25.2",
"react-native-svg": "^15.15.5",
"react-redux": "^9.3.0"
"react-native-uuid": "^2.0.4",
"react-native-worklets": "^0.10.0",
"react-redux": "^9.3.0",
"reactotron-react-native": "^5.2.0",
"redux-persist": "^6.0.0",
"redux-thunk": "^3.1.0"
},
"devDependencies": {
"@babel/core": "^7.25.2",

View File

@ -13,12 +13,17 @@
"@navigation/*": ["./app/navigation/*"],
"@store": ["./app/store"],
"@store/*": ["./app/store/*"],
"@services": ["./app/services"],
"@services/*": ["./app/services/*"],
"@hooks/*": ["./app/hooks/*"],
"@utils/*": ["./app/utils/*"],
"@icons": ["./app/components/icons"],
"@icons/*": ["./app/components/icons/*"],
"@app-types/*": ["./app/types/*"]
"@app-types/*": ["./app/types/*"],
"@interfaces": ["./app/interfaces"],
"@interfaces/*": ["./app/interfaces/*"],
"@api": ["./app/api"],
"@api/*": ["./app/api/*"]
}
},
"include": ["**/*.ts", "**/*.tsx"],

268
yarn.lock
View File

@ -429,7 +429,7 @@
"@babel/helper-create-regexp-features-plugin" "^7.18.6"
"@babel/helper-plugin-utils" "^7.18.6"
"@babel/plugin-transform-arrow-functions@^7.29.7":
"@babel/plugin-transform-arrow-functions@^7.27.1", "@babel/plugin-transform-arrow-functions@^7.29.7":
version "7.29.7"
resolved "https://registry.yarnpkg.com/@babel/plugin-transform-arrow-functions/-/plugin-transform-arrow-functions-7.29.7.tgz#d651343f562c03f47951bd1802195d0e10605f27"
integrity sha512-N7zArUXWzAMzm+/N0uPBeVB3Fam5lMxtUwMmDK5f/IBBS7a7p1qeUoxd/6CckXoxUdgsntq1Dh8xNW06maZbDQ==
@ -468,7 +468,7 @@
dependencies:
"@babel/helper-plugin-utils" "^7.29.7"
"@babel/plugin-transform-class-properties@^7.25.4", "@babel/plugin-transform-class-properties@^7.29.7":
"@babel/plugin-transform-class-properties@^7.25.4", "@babel/plugin-transform-class-properties@^7.28.6", "@babel/plugin-transform-class-properties@^7.29.7":
version "7.29.7"
resolved "https://registry.yarnpkg.com/@babel/plugin-transform-class-properties/-/plugin-transform-class-properties-7.29.7.tgz#034897b8a21beec163332fac2de235b14409abdf"
integrity sha512-GtcpjFvanPfzNQi3eTitsCqtRRmmqzpy/A+yhTR1HaZo1Ly3EA8ZXxlPyHdR8/IuRMYc3E4wdGBewB2QKQjAaA==
@ -484,7 +484,7 @@
"@babel/helper-create-class-features-plugin" "^7.29.7"
"@babel/helper-plugin-utils" "^7.29.7"
"@babel/plugin-transform-classes@^7.25.4", "@babel/plugin-transform-classes@^7.29.7":
"@babel/plugin-transform-classes@^7.25.4", "@babel/plugin-transform-classes@^7.28.6", "@babel/plugin-transform-classes@^7.29.7":
version "7.29.7"
resolved "https://registry.yarnpkg.com/@babel/plugin-transform-classes/-/plugin-transform-classes-7.29.7.tgz#61d3e5aaae0c838acc3204d9db7c8dc05c25815b"
integrity sha512-qV0OGGBVacduzQHE649JyCneOFI/maT+YKsO+K4Yi3xv2wTPNjM/W2o2gdzMwEAZz7fXNTHAe0NcSg30bIN69g==
@ -666,7 +666,7 @@
dependencies:
"@babel/helper-plugin-utils" "^7.29.7"
"@babel/plugin-transform-nullish-coalescing-operator@^7.24.7", "@babel/plugin-transform-nullish-coalescing-operator@^7.29.7":
"@babel/plugin-transform-nullish-coalescing-operator@^7.24.7", "@babel/plugin-transform-nullish-coalescing-operator@^7.28.6", "@babel/plugin-transform-nullish-coalescing-operator@^7.29.7":
version "7.29.7"
resolved "https://registry.yarnpkg.com/@babel/plugin-transform-nullish-coalescing-operator/-/plugin-transform-nullish-coalescing-operator-7.29.7.tgz#8a54cdf88c3f50433a6173117a286195b67714cc"
integrity sha512-idmp1dFaekP9GbcMvG24Kvw2BfhFZjHnNJCkV4WuIY4PskJzwI3f1N5OdgYke38T7rftO6ERulFRn2cFeZwRkg==
@ -706,7 +706,7 @@
dependencies:
"@babel/helper-plugin-utils" "^7.29.7"
"@babel/plugin-transform-optional-chaining@^7.24.8", "@babel/plugin-transform-optional-chaining@^7.29.7":
"@babel/plugin-transform-optional-chaining@^7.24.8", "@babel/plugin-transform-optional-chaining@^7.28.6", "@babel/plugin-transform-optional-chaining@^7.29.7":
version "7.29.7"
resolved "https://registry.yarnpkg.com/@babel/plugin-transform-optional-chaining/-/plugin-transform-optional-chaining-7.29.7.tgz#b84a1b574b3c73001023092567e16c492b720e51"
integrity sha512-6GM1dhvK3gNODkXcEcMCOLEDCLSoZ/sBbro2Ax8HURyasQ4NshagQixkRFdh5niI6E4gmA/jYI/4aT7rRos3ZQ==
@ -811,7 +811,7 @@
babel-plugin-polyfill-regenerator "^0.6.5"
semver "^6.3.1"
"@babel/plugin-transform-shorthand-properties@^7.29.7":
"@babel/plugin-transform-shorthand-properties@^7.27.1", "@babel/plugin-transform-shorthand-properties@^7.29.7":
version "7.29.7"
resolved "https://registry.yarnpkg.com/@babel/plugin-transform-shorthand-properties/-/plugin-transform-shorthand-properties-7.29.7.tgz#25c0436b98f4bd9ca4b98e1fbd662743bbaab9bf"
integrity sha512-I+WYbGBAiCn7nA6xBrlgPH+MB7HWb4u8pv5S0Pv7OtwNvIFvCCb24YlttKEeUFVurfBCEaOTnuhlqsb7f0Z5Dg==
@ -833,7 +833,7 @@
dependencies:
"@babel/helper-plugin-utils" "^7.29.7"
"@babel/plugin-transform-template-literals@^7.29.7":
"@babel/plugin-transform-template-literals@^7.27.1", "@babel/plugin-transform-template-literals@^7.29.7":
version "7.29.7"
resolved "https://registry.yarnpkg.com/@babel/plugin-transform-template-literals/-/plugin-transform-template-literals-7.29.7.tgz#ada97d8e0832bca8edb315888aa654b1570f3835"
integrity sha512-NCSEJ4sLFU2gqAub45HYh4fus2yQ36rr6ei6vpU7NdoJqCpxvEG8E6eJpscGyXP3VHD2Ny+fSXr04k1hoUrFqA==
@ -847,7 +847,7 @@
dependencies:
"@babel/helper-plugin-utils" "^7.29.7"
"@babel/plugin-transform-typescript@^7.25.2":
"@babel/plugin-transform-typescript@^7.25.2", "@babel/plugin-transform-typescript@^7.29.7":
version "7.29.7"
resolved "https://registry.yarnpkg.com/@babel/plugin-transform-typescript/-/plugin-transform-typescript-7.29.7.tgz#f0449c3df7037bbe232043476851c38f5e4a7615"
integrity sha512-jK52h8LaLc7JarhQV2ofeFMts4H7vnOXnqZNA6fYglBTZewRBE51KWt3BUltW1P+KoPsYkHoJeXePuz4zo2LMw==
@ -873,7 +873,7 @@
"@babel/helper-create-regexp-features-plugin" "^7.29.7"
"@babel/helper-plugin-utils" "^7.29.7"
"@babel/plugin-transform-unicode-regex@^7.24.7", "@babel/plugin-transform-unicode-regex@^7.29.7":
"@babel/plugin-transform-unicode-regex@^7.24.7", "@babel/plugin-transform-unicode-regex@^7.27.1", "@babel/plugin-transform-unicode-regex@^7.29.7":
version "7.29.7"
resolved "https://registry.yarnpkg.com/@babel/plugin-transform-unicode-regex/-/plugin-transform-unicode-regex-7.29.7.tgz#c3064b293ff7f1794b71f7650eec8db9896d3e59"
integrity sha512-7D/x/23/d/3VqZ0QA+LGbZMlGwZjztBygSWWWsfTPoQ1oQ6Q1P6Mr3d0kk42XabyUVw+fha3LqdRsFqeKqvCyA==
@ -975,6 +975,17 @@
"@babel/types" "^7.4.4"
esutils "^2.0.2"
"@babel/preset-typescript@^7.28.5":
version "7.29.7"
resolved "https://registry.yarnpkg.com/@babel/preset-typescript/-/preset-typescript-7.29.7.tgz#de9be1f47b785c979ec7b3a71f4cd8bae5267b62"
integrity sha512-/Foi8vKY2EVbed/1eZx0gJEEwHAIxogrySI7rULcRIvhZzbvoE/b5qG5Ghc0WKAFKOHA9SD1x7RsFlOYdutIiQ==
dependencies:
"@babel/helper-plugin-utils" "^7.29.7"
"@babel/helper-validator-option" "^7.29.7"
"@babel/plugin-syntax-jsx" "^7.29.7"
"@babel/plugin-transform-modules-commonjs" "^7.29.7"
"@babel/plugin-transform-typescript" "^7.29.7"
"@babel/runtime@^7.25.0":
version "7.29.7"
resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.29.7.tgz#12022450c45a4da6d8d8287b18a4ff2ddb23f768"
@ -1002,7 +1013,7 @@
"@babel/types" "^7.29.7"
debug "^4.3.1"
"@babel/types@^7.0.0", "@babel/types@^7.20.7", "@babel/types@^7.28.2", "@babel/types@^7.29.0", "@babel/types@^7.29.7", "@babel/types@^7.3.3", "@babel/types@^7.4.4":
"@babel/types@^7.0.0", "@babel/types@^7.20.7", "@babel/types@^7.27.1", "@babel/types@^7.28.2", "@babel/types@^7.29.0", "@babel/types@^7.29.7", "@babel/types@^7.3.3", "@babel/types@^7.4.4":
version "7.29.7"
resolved "https://registry.yarnpkg.com/@babel/types/-/types-7.29.7.tgz#8005e31d82712ee7adaef6e23c63b71a62770a92"
integrity sha512-4zBIxpPzowiZpusoFkyGVwakdRJUyuH5PxQ/PrqghfdFWWasvnCdPfQXHrenDai+gyLARulZjZowCOj6fjT4pA==
@ -1015,6 +1026,13 @@
resolved "https://registry.yarnpkg.com/@bcoe/v8-coverage/-/v8-coverage-0.2.3.tgz#75a2e8b51cb758a7553d6804a5932d7aace75c39"
integrity sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw==
"@egjs/hammerjs@^2.0.17":
version "2.0.17"
resolved "https://registry.yarnpkg.com/@egjs/hammerjs/-/hammerjs-2.0.17.tgz#5dc02af75a6a06e4c2db0202cae38c9263895124"
integrity sha512-XQsZgjm2EcVUiZQf11UBJQfmZeEmOW8DpI1gsFeln6w0ae0ii4dMQEQ0kjl6DspdWX1aGY1/loyXnP0JS06e/A==
dependencies:
"@types/hammerjs" "^2.0.36"
"@eslint-community/eslint-utils@^4.2.0", "@eslint-community/eslint-utils@^4.9.1":
version "4.9.1"
resolved "https://registry.yarnpkg.com/@eslint-community/eslint-utils/-/eslint-utils-4.9.1.tgz#4e90af67bc51ddee6cdef5284edf572ec376b595"
@ -1525,6 +1543,11 @@
prompts "^2.4.2"
semver "^7.5.2"
"@react-native-documents/picker@^12.0.1":
version "12.0.1"
resolved "https://registry.yarnpkg.com/@react-native-documents/picker/-/picker-12.0.1.tgz#ea59796175308be9818dd9e8f14a41145c35042b"
integrity sha512-vpJKb4t/5bnxe9+gQl+plJfKrrIsmYwANGhNH2B9E1dS1+6FDBzg4Dwmcq4ueaGfkRKEPJ606mJttVEH1ZKZaA==
"@react-native/assets-registry@0.86.0":
version "0.86.0"
resolved "https://registry.yarnpkg.com/@react-native/assets-registry/-/assets-registry-0.86.0.tgz#560e78135969d1198aabfdc4ae74faae336463d0"
@ -1711,19 +1734,19 @@
invariant "^2.2.4"
nullthrows "^1.1.1"
"@react-navigation/bottom-tabs@^7.18.5":
version "7.18.5"
resolved "https://registry.yarnpkg.com/@react-navigation/bottom-tabs/-/bottom-tabs-7.18.5.tgz#b374eb01c564da7ed15cccd98205a044e16be904"
integrity sha512-BiXkpEprKwE++p64qjfUOhsK+e0VW8r5FsLbDOp8Mcr/vhoT9aqX3a4pHCVQ8ug5YQW+QrgF4/XIXI99CZxMnQ==
"@react-navigation/bottom-tabs@^7.18.3":
version "7.18.8"
resolved "https://registry.yarnpkg.com/@react-navigation/bottom-tabs/-/bottom-tabs-7.18.8.tgz#a611371cbe01f6a84873e1725c269c8746e4510f"
integrity sha512-7KCsBtwCRwQQSTEw9SLglZCEkxWc+EqgdRKyUOgII+6+xEnemOyg8aDlnLIM2yk9ip6uE+Oxvm0jdpQU2vsu2w==
dependencies:
"@react-navigation/elements" "^2.9.27"
"@react-navigation/elements" "^2.9.30"
color "^4.2.3"
sf-symbols-typescript "^2.1.0"
"@react-navigation/core@^7.21.3":
version "7.21.3"
resolved "https://registry.yarnpkg.com/@react-navigation/core/-/core-7.21.3.tgz#a4fc63da29f3dcbcbfe63da95b32c88d969c980c"
integrity sha512-HUoGmT9IdpKpbAl9AZJmXFbid+jQWQWjzww9vV9uBSd+8fa1hTwM3UBsZBHrpMru0s1ikwknoAIwV827fqGlhA==
"@react-navigation/core@^7.21.5":
version "7.21.5"
resolved "https://registry.yarnpkg.com/@react-navigation/core/-/core-7.21.5.tgz#2f2c7f5374c9a20d942688aa37c0e9f6665a9549"
integrity sha512-3hpV7uR41LBW+GHDoLhztZCb/i5ySRJISZ/rez4d7DCHSZo6ej4gNxYclaS6LRguoLiKG7SOCNa6O390AQklZQ==
dependencies:
"@react-navigation/routers" "^7.6.0"
escape-string-regexp "^4.0.0"
@ -1743,6 +1766,15 @@
use-latest-callback "^0.2.4"
use-sync-external-store "^1.5.0"
"@react-navigation/elements@^2.9.30":
version "2.9.30"
resolved "https://registry.yarnpkg.com/@react-navigation/elements/-/elements-2.9.30.tgz#60702877319f06c20b15e3f98446dcc994c67cff"
integrity sha512-2isleieiRMmP4WNMV2Q1u3qP1M47ZqsJ2hJ/Og11FeKXK8YmUTHya7PW7ecsgAh2CXKTxAcbfJDFTSvq2D++Tw==
dependencies:
color "^4.2.3"
use-latest-callback "^0.2.4"
use-sync-external-store "^1.5.0"
"@react-navigation/native-stack@^7.17.7":
version "7.17.7"
resolved "https://registry.yarnpkg.com/@react-navigation/native-stack/-/native-stack-7.17.7.tgz#16e505bbfc953d53272997ac3db1bc7b4daae328"
@ -1753,12 +1785,12 @@
sf-symbols-typescript "^2.1.0"
warn-once "^0.1.1"
"@react-navigation/native@^7.3.5":
version "7.3.5"
resolved "https://registry.yarnpkg.com/@react-navigation/native/-/native-7.3.5.tgz#d56ba57e130844f5eeac4c9d06e57386834c8330"
integrity sha512-lzcRpMoLCIypXXKm8KhpyZ81XpQBO/i8Oy1due4kUR97kG8kBlJxKUeP8yNnT5Fcl85rNdLXWbpwEr+9HPu4Ig==
"@react-navigation/native@^7.3.4":
version "7.3.8"
resolved "https://registry.yarnpkg.com/@react-navigation/native/-/native-7.3.8.tgz#01513734e93ac40fd3b48fd6169836fbf5678ac8"
integrity sha512-zHmQcxWBT8GOwsofEOmHqpdM5twkwE/esa9JFGlW4hpXeQTTe/dRcPSLjwsvePtolbDKz0YZbK+I5KrW3j63LQ==
dependencies:
"@react-navigation/core" "^7.21.3"
"@react-navigation/core" "^7.21.5"
escape-string-regexp "^4.0.0"
fast-deep-equal "^3.1.3"
nanoid "^3.3.11"
@ -1772,12 +1804,12 @@
dependencies:
nanoid "^3.3.11"
"@react-navigation/stack@^7.10.7":
version "7.10.7"
resolved "https://registry.yarnpkg.com/@react-navigation/stack/-/stack-7.10.7.tgz#76b91b693bbc00182090a2c71743cb8c5754409b"
integrity sha512-rkJ9CEJnOHRSBVYNq7M8+0a34munKcTho/HSJiniRM3ZDjWR0aQKDXZcYpKi+IQ8jKv2XyYOvZLGHrBNJc5ipA==
"@react-navigation/stack@^7.10.6":
version "7.10.11"
resolved "https://registry.yarnpkg.com/@react-navigation/stack/-/stack-7.10.11.tgz#75788547aa8c3f43c20cdb87c06342a39f2fb734"
integrity sha512-FNlylwYaGwGArRjmJhmL3FviTjT/j10UVx6TAaqqeLeYEircqMm2kBBz2e9FpcXH2OeCnPinAjZ0B1th2mNmsg==
dependencies:
"@react-navigation/elements" "^2.9.27"
"@react-navigation/elements" "^2.9.30"
color "^4.2.3"
use-latest-callback "^0.2.4"
@ -1884,6 +1916,11 @@
dependencies:
"@types/node" "*"
"@types/hammerjs@^2.0.36":
version "2.0.46"
resolved "https://registry.yarnpkg.com/@types/hammerjs/-/hammerjs-2.0.46.tgz#381daaca1360ff8a7c8dff63f32e69745b9fb1e1"
integrity sha512-ynRvcq6wvqexJ9brDMS4BnBLzmr0e14d6ZJTEShTBWKymQiHwlAyGu0ZPEFI2Fh1U53F7tN9ufClWM5KvqkKOw==
"@types/istanbul-lib-coverage@*", "@types/istanbul-lib-coverage@^2.0.0", "@types/istanbul-lib-coverage@^2.0.1":
version "2.0.6"
resolved "https://registry.yarnpkg.com/@types/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.6.tgz#7739c232a1fee9b4d3ce8985f314c0c6d33549d7"
@ -2093,6 +2130,13 @@ acorn@^8.15.0, acorn@^8.9.0:
resolved "https://registry.yarnpkg.com/acorn/-/acorn-8.17.0.tgz#1785adb84faf8d8add10369b93826fc2bd08f1fe"
integrity sha512-xRQbDb9BnwDafYNn6Vwl839DYVjqXYb1XVGtWAZ1kcDc6iwAL4hg3B1dZlRiuENFeO2H53gFG3in621AdERVAg==
agent-base@6:
version "6.0.2"
resolved "https://registry.yarnpkg.com/agent-base/-/agent-base-6.0.2.tgz#49fff58577cfee3f37176feab4c22e00f86d7f77"
integrity sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==
dependencies:
debug "4"
agent-base@^7.1.2:
version "7.1.4"
resolved "https://registry.yarnpkg.com/agent-base/-/agent-base-7.1.4.tgz#e3cd76d4c548ee895d3c3fd8dc1f6c5b9032e7a8"
@ -2281,6 +2325,11 @@ async-limiter@~1.0.0:
resolved "https://registry.yarnpkg.com/async-limiter/-/async-limiter-1.0.1.tgz#dd379e94f0db8310b08291f9d64c3209766617fd"
integrity sha512-csOlWGAcRFJaI6m+F2WKdnMKr4HhdhFVBk0H/QbJFMCr+uO2kwohwXQPxw/9OCxp05r5ghVBFSyioixx3gfkNQ==
asynckit@^0.4.0:
version "0.4.0"
resolved "https://registry.yarnpkg.com/asynckit/-/asynckit-0.4.0.tgz#c79ed97f7f34cb8f2ba1bc9790bcc366474b4b79"
integrity sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==
available-typed-arrays@^1.0.7:
version "1.0.7"
resolved "https://registry.yarnpkg.com/available-typed-arrays/-/available-typed-arrays-1.0.7.tgz#a5cc375d6a03c2efc87a553f3e0b1522def14846"
@ -2288,6 +2337,16 @@ available-typed-arrays@^1.0.7:
dependencies:
possible-typed-array-names "^1.0.0"
axios@^1.18.1:
version "1.18.1"
resolved "https://registry.yarnpkg.com/axios/-/axios-1.18.1.tgz#d63f9863bcd8938815c86f9e2abd380189d96dfe"
integrity sha512-3nTvFlvpn9Zu/RkHUqtc7/+al4UpRW5az71ap5zccp6e8RAYEzhMTecX8Dz1wWDYrPpUoB1HAQEGEAEvUr7S9g==
dependencies:
follow-redirects "^1.16.0"
form-data "^4.0.5"
https-proxy-agent "^5.0.1"
proxy-from-env "^2.1.0"
babel-jest@^29.7.0:
version "29.7.0"
resolved "https://registry.yarnpkg.com/babel-jest/-/babel-jest-29.7.0.tgz#f4369919225b684c56085998ac63dbd05be020d5"
@ -2710,6 +2769,13 @@ colorette@^1.0.7:
resolved "https://registry.yarnpkg.com/colorette/-/colorette-1.4.0.tgz#5190fbb87276259a86ad700bff2c6d6faa3fca40"
integrity sha512-Y2oEozpomLn7Q3HFP7dpww7AtMJplbM9lGZP6RDfHqmbeRjiwRg4n6VM6j4KLmRke85uWEI7JqF17f3pqdRA0g==
combined-stream@^1.0.8:
version "1.0.8"
resolved "https://registry.yarnpkg.com/combined-stream/-/combined-stream-1.0.8.tgz#c3d45a8b34fd730631a110a8a2520682b31d5a7f"
integrity sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==
dependencies:
delayed-stream "~1.0.0"
command-exists@^1.2.8:
version "1.2.9"
resolved "https://registry.yarnpkg.com/command-exists/-/command-exists-1.2.9.tgz#c50725af3808c8ab0260fd60b01fbfa25b954f69"
@ -2939,6 +3005,11 @@ define-properties@^1.1.3, define-properties@^1.2.1:
has-property-descriptors "^1.0.0"
object-keys "^1.1.1"
delayed-stream@~1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/delayed-stream/-/delayed-stream-1.0.0.tgz#df3ae199acadfb7d440aaae0b29e2272b24ec619"
integrity sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==
depd@2.0.0, depd@~2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/depd/-/depd-2.0.0.tgz#b696163cc757560d09cf22cc8fad1571b79e76df"
@ -3619,6 +3690,11 @@ flow-enums-runtime@^0.0.6:
resolved "https://registry.yarnpkg.com/flow-enums-runtime/-/flow-enums-runtime-0.0.6.tgz#5bb0cd1b0a3e471330f4d109039b7eba5cb3e787"
integrity sha512-3PYnM29RFXwvAN6Pc/scUfkI7RwhQ/xqyLUyPNlXUp9S40zI8nup9tUSrTLSVnWGBN38FNiGWbwZOB6uR4OGdw==
follow-redirects@^1.16.0:
version "1.16.0"
resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.16.0.tgz#28474a159d3b9d11ef62050a14ed60e4df6d61bc"
integrity sha512-y5rN/uOsadFT/JfYwhxRS5R7Qce+g3zG97+JrtFZlC9klX/W5hD7iiLzScI4nZqUS7DNUdhPgw4xI8W2LuXlUw==
for-each@^0.3.3, for-each@^0.3.5:
version "0.3.5"
resolved "https://registry.yarnpkg.com/for-each/-/for-each-0.3.5.tgz#d650688027826920feeb0af747ee7b9421a41d47"
@ -3626,6 +3702,17 @@ for-each@^0.3.3, for-each@^0.3.5:
dependencies:
is-callable "^1.2.7"
form-data@^4.0.5:
version "4.0.6"
resolved "https://registry.yarnpkg.com/form-data/-/form-data-4.0.6.tgz#28e864e1b786dbebb68db1f452f9635278665827"
integrity sha512-vKatAh4SlVfgbv+YtmhiRjhEMJsYpsG1Y2rMQtR+SVSbytsSD1YGzDIcrAJmdFec88u/+VoGmxnl+80gL1tRCQ==
dependencies:
asynckit "^0.4.0"
combined-stream "^1.0.8"
es-set-tostringtag "^2.1.0"
hasown "^2.0.4"
mime-types "^2.1.35"
fresh@~0.5.2:
version "0.5.2"
resolved "https://registry.yarnpkg.com/fresh/-/fresh-0.5.2.tgz#3d8cadd90d976569fa835ab1f8e4b23a105605a7"
@ -3883,6 +3970,13 @@ hermes-parser@^0.25.1:
dependencies:
hermes-estree "0.25.1"
hoist-non-react-statics@^3.3.0:
version "3.3.2"
resolved "https://registry.yarnpkg.com/hoist-non-react-statics/-/hoist-non-react-statics-3.3.2.tgz#ece0acaf71d62c2969c2ec59feff42a4b1a85b45"
integrity sha512-/gGivxi8JPKWNm/W0jSmzcMPpfpPLc3dY/6GxhX2hQ9iGj3aDfklV4ET7NjKpSinLpJ5vafa9iiGIEZg10SfBw==
dependencies:
react-is "^16.7.0"
html-escaper@^2.0.0:
version "2.0.2"
resolved "https://registry.yarnpkg.com/html-escaper/-/html-escaper-2.0.2.tgz#dfd60027da36a36dfcbe236262c00a5822681453"
@ -3899,6 +3993,14 @@ http-errors@~2.0.1:
statuses "~2.0.2"
toidentifier "~1.0.1"
https-proxy-agent@^5.0.1:
version "5.0.1"
resolved "https://registry.yarnpkg.com/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz#c59ef224a04fe8b754f3db0063a25ea30d0005d6"
integrity sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==
dependencies:
agent-base "6"
debug "4"
https-proxy-agent@^7.0.5:
version "7.0.6"
resolved "https://registry.yarnpkg.com/https-proxy-agent/-/https-proxy-agent-7.0.6.tgz#da8dfeac7da130b05c2ba4b59c9b6cd66611a6b9"
@ -5152,6 +5254,13 @@ mime-db@1.52.0:
resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.54.0.tgz#cddb3ee4f9c64530dff640236661d42cb6a314f5"
integrity sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==
mime-types@^2.1.35, mime-types@~2.1.24, mime-types@~2.1.34:
version "2.1.35"
resolved "https://registry.yarnpkg.com/mime-types/-/mime-types-2.1.35.tgz#381a871b62a734450660ae3deee44813f70d959a"
integrity sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==
dependencies:
mime-db "1.52.0"
mime-types@^3.0.0, mime-types@^3.0.1:
version "3.0.2"
resolved "https://registry.yarnpkg.com/mime-types/-/mime-types-3.0.2.tgz#39002d4182575d5af036ffa118100f2524b2e2ab"
@ -5159,13 +5268,6 @@ mime-types@^3.0.0, mime-types@^3.0.1:
dependencies:
mime-db "^1.54.0"
mime-types@~2.1.24, mime-types@~2.1.34:
version "2.1.35"
resolved "https://registry.yarnpkg.com/mime-types/-/mime-types-2.1.35.tgz#381a871b62a734450660ae3deee44813f70d959a"
integrity sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==
dependencies:
mime-db "1.52.0"
mime@1.6.0:
version "1.6.0"
resolved "https://registry.yarnpkg.com/mime/-/mime-1.6.0.tgz#32cd9e5c64553bd58d19a568af452acff04981b1"
@ -5212,6 +5314,11 @@ minipass@^4.2.4:
resolved "https://registry.yarnpkg.com/minipass/-/minipass-7.1.3.tgz#79389b4eb1bb2d003a9bba87d492f2bd37bdc65b"
integrity sha512-tEBHqDnIoM/1rXME1zgka9g6Q2lcoCkxHLuc7ODJ5BxbP5d4c2Z5cGgtXAku59200Cx7diuHTOYfSBD8n6mm8A==
mitt@^3.0.1:
version "3.0.1"
resolved "https://registry.yarnpkg.com/mitt/-/mitt-3.0.1.tgz#ea36cf0cc30403601ae074c8f77b7092cdab36d1"
integrity sha512-vKivATfr97l2/QBCYAkXYDbrIWPM2IIKEl7YPhjCvKlG3kE2gm+uBo6nEXK3M5/Ffh/FLpKExzOQ3JJoJGFKBw==
mkdirp@^1.0.4:
version "1.0.4"
resolved "https://registry.yarnpkg.com/mkdirp/-/mkdirp-1.0.4.tgz#3eb5ed62622756d79a5f0e2a221dfebad75c2f7e"
@ -5631,6 +5738,11 @@ prop-types@^15.8.1:
object-assign "^4.1.1"
react-is "^16.13.1"
proxy-from-env@^2.1.0:
version "2.1.0"
resolved "https://registry.yarnpkg.com/proxy-from-env/-/proxy-from-env-2.1.0.tgz#a7487568adad577cfaaa7e88c49cab3ab3081aba"
integrity sha512-cJ+oHTW1VAEa8cJslgmUZrc+sjRKgAKl3Zyse6+PV38hZe/V6Z14TbCuXcan9F9ghlz4QrFr2c92TNF82UkYHA==
punycode@^2.1.0:
version "2.3.1"
resolved "https://registry.yarnpkg.com/punycode/-/punycode-2.3.1.tgz#027422e2faec0b25e1549c3e1bd8309b9133b6e5"
@ -5699,7 +5811,7 @@ react-freeze@^1.0.0:
resolved "https://registry.yarnpkg.com/react-freeze/-/react-freeze-1.0.4.tgz#cbbea2762b0368b05cbe407ddc9d518c57c6f3ad"
integrity sha512-r4F0Sec0BLxWicc7HEyo2x3/2icUTrRmDjaaRyzzn+7aDyFZliszMDOgLVwSnQnYENOlL1o569Ze2HZefk8clA==
react-is@^16.13.1:
react-is@^16.13.1, react-is@^16.7.0:
version "16.13.1"
resolved "https://registry.yarnpkg.com/react-is/-/react-is-16.13.1.tgz#789729a4dc36de2999dc156dd6c1d9c18cea56a4"
integrity sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==
@ -5714,14 +5826,26 @@ react-is@^19.1.0, react-is@^19.2.3:
resolved "https://registry.yarnpkg.com/react-is/-/react-is-19.2.7.tgz#57668ee86a78574a542b0a539455212b2c086df2"
integrity sha512-kZFnouyVv7eP/Phmrlo9FK+zcAdriZJvzxXHF1Sl1P377WSGe2G/JxVolhTrB/jeV47lKImhNUsijjHAAbcl/A==
react-native-gesture-handler@^3.0.2:
version "3.0.2"
resolved "https://registry.yarnpkg.com/react-native-gesture-handler/-/react-native-gesture-handler-3.0.2.tgz#ea80fd2424d08f8b624977221f0544a5201f6d83"
integrity sha512-XBTgmvr2jh/xrMwlHTV2Z1x6IFUl3zfLxyaCAnvj3GSXK5YlY3ZmiIs57hniPmh3I7RtjPFxtGdRr0i+PzyBrg==
react-native-geolocation-service@^5.3.1:
version "5.3.1"
resolved "https://registry.yarnpkg.com/react-native-geolocation-service/-/react-native-geolocation-service-5.3.1.tgz#4ce1017789da6fdfcf7576eb6f59435622af4289"
integrity sha512-LTXPtPNmrdhx+yeWG47sAaCgQc3nG1z+HLLHlhK/5YfOgfLcAb9HAkhREPjQKPZOUx8pKZMIpdGFUGfJYtimXQ==
react-native-gesture-handler@^2.32.0:
version "2.32.0"
resolved "https://registry.yarnpkg.com/react-native-gesture-handler/-/react-native-gesture-handler-2.32.0.tgz#59b8658a4e1586ce6fb0ca0053ac80f6acf7f882"
integrity sha512-uYIMOKlKENORq2SABE+jIjbPU+h5I/sQKcq2v16zRq848nwEp1fWRVwML4QWqijc8UcXJC25o54S8GQd4Mf2OA==
dependencies:
"@egjs/hammerjs" "^2.0.17"
"@types/react-test-renderer" "^19.1.0"
hoist-non-react-statics "^3.3.0"
invariant "^2.2.4"
react-native-is-edge-to-edge@^1.3.1:
version "1.3.1"
resolved "https://registry.yarnpkg.com/react-native-is-edge-to-edge/-/react-native-is-edge-to-edge-1.3.1.tgz#feb9a6a8faf0874298947edd556e5af22044e139"
integrity sha512-NIXU/iT5+ORyCc7p0z2nnlkouYKX425vuU1OEm6bMMtWWR9yvb+Xg5AZmImTKoF9abxCPqrKC3rOZsKzUYgYZA==
react-native-maps@^1.29.0:
version "1.29.0"
resolved "https://registry.yarnpkg.com/react-native-maps/-/react-native-maps-1.29.0.tgz#7ba5b9f70c9dcedeb53d31847809c56661341742"
@ -5729,10 +5853,13 @@ react-native-maps@^1.29.0:
dependencies:
"@types/geojson" "^7946.0.13"
react-native-permissions@^5.6.0:
version "5.6.0"
resolved "https://registry.yarnpkg.com/react-native-permissions/-/react-native-permissions-5.6.0.tgz#de5df5576c7762bc14da5b418bea025d300dacf6"
integrity sha512-iKq7rgAg9cQSy9e/7A6wCj0GYhw+o29Ob4OKC793fniDdwraDNu7D67X4holGJLChUDNzFZH1Qj+YWgrPNsGLw==
react-native-reanimated@^4.5.0:
version "4.5.1"
resolved "https://registry.yarnpkg.com/react-native-reanimated/-/react-native-reanimated-4.5.1.tgz#be81ed3a96bf70baeeccdc24d3350883099b7dd3"
integrity sha512-RnMvtDuR+68ig864gAvZCOdZehqhC5rFmMo0kn+ARfgVSTvFeF6IFLBVgMPUu0KwihaapEyW24WRi6nEyy1kSA==
dependencies:
react-native-is-edge-to-edge "^1.3.1"
semver "^7.7.3"
react-native-safe-area-context@^5.8.0:
version "5.8.0"
@ -5755,6 +5882,29 @@ react-native-svg@^15.15.5:
css-select "^5.1.0"
css-tree "^1.1.3"
react-native-uuid@^2.0.4:
version "2.0.4"
resolved "https://registry.yarnpkg.com/react-native-uuid/-/react-native-uuid-2.0.4.tgz#0c4f345523c5feac0282d3a51fe19887727df988"
integrity sha512-LSJNeh559qC17fgVPBsWuTSW/OygFp2dwTcf94IQBLYft5FzIQS9pCsuT36OPvyvDOMb6yiGr6TafaJDnz9PPQ==
react-native-worklets@^0.10.0:
version "0.10.2"
resolved "https://registry.yarnpkg.com/react-native-worklets/-/react-native-worklets-0.10.2.tgz#c59e6314d2da4a69f20d66f58bb964ee2275328a"
integrity sha512-LX27ejYI8veeDp59Z3rjo2pYyPa9euzSH8GUlem7cnNqfsDtGum8PQpkbzrqhLsWH0CjdeHR7p3sncCyYbwaVw==
dependencies:
"@babel/plugin-transform-arrow-functions" "^7.27.1"
"@babel/plugin-transform-class-properties" "^7.28.6"
"@babel/plugin-transform-classes" "^7.28.6"
"@babel/plugin-transform-nullish-coalescing-operator" "^7.28.6"
"@babel/plugin-transform-optional-chaining" "^7.28.6"
"@babel/plugin-transform-shorthand-properties" "^7.27.1"
"@babel/plugin-transform-template-literals" "^7.27.1"
"@babel/plugin-transform-unicode-regex" "^7.27.1"
"@babel/preset-typescript" "^7.28.5"
"@babel/types" "^7.27.1"
convert-source-map "^2.0.0"
semver "^7.7.4"
react-native@0.86.0:
version "0.86.0"
resolved "https://registry.yarnpkg.com/react-native/-/react-native-0.86.0.tgz#a5d62adbf350010ad7a61617b9bc300b2ff12870"
@ -5819,6 +5969,27 @@ react@19.2.3:
resolved "https://registry.yarnpkg.com/react/-/react-19.2.3.tgz#d83e5e8e7a258cf6b4fe28640515f99b87cd19b8"
integrity sha512-Ku/hhYbVjOQnXDZFv2+RibmLFGwFdeeKHFcOTlrt7xplBnya5OGn/hIRDsqDiSUcfORsDC7MPxwork8jBwsIWA==
reactotron-core-client@2.10.0:
version "2.10.0"
resolved "https://registry.yarnpkg.com/reactotron-core-client/-/reactotron-core-client-2.10.0.tgz#494e6e56bb018774a0a63c797d48b53ac6986256"
integrity sha512-mOzkc1Rb4sN98OLY/frkP0e1A60bTFjuXUgG8D4m+pwCcjVCWZupdSFV8c5+gqcZBnreoVR20/ko6D8X/7KPvg==
dependencies:
reactotron-core-contract "0.4.0"
reactotron-core-contract@0.4.0:
version "0.4.0"
resolved "https://registry.yarnpkg.com/reactotron-core-contract/-/reactotron-core-contract-0.4.0.tgz#7227fe19be6b564c083318798bdf88af943ca995"
integrity sha512-o7m4mSzaUM/IGDc1dBfe0q6rgSi7mZCP7FCs3kHF7AJQwXBOKx/awjVaeNVqz+w6ADiw7X4dtDdQvEROAXdD3w==
reactotron-react-native@^5.2.0:
version "5.2.0"
resolved "https://registry.yarnpkg.com/reactotron-react-native/-/reactotron-react-native-5.2.0.tgz#e722f92860951b383d17400115825aa3a3b10502"
integrity sha512-7Jd47Eti4wvyn1p5MsQ4mEg2mGG8B5HIZHASjK5RKVfOajEP9Sf4Vume9URG6inB/UQWGpBP6ahCGqcSo4qn3w==
dependencies:
mitt "^3.0.1"
reactotron-core-client "2.10.0"
reactotron-core-contract "0.4.0"
readable-stream@^3.4.0:
version "3.6.2"
resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-3.6.2.tgz#56a9b36ea965c00c5a93ef31eb111a0f11056967"
@ -5828,6 +5999,11 @@ readable-stream@^3.4.0:
string_decoder "^1.1.1"
util-deprecate "^1.0.1"
redux-persist@^6.0.0:
version "6.0.0"
resolved "https://registry.yarnpkg.com/redux-persist/-/redux-persist-6.0.0.tgz#b4d2972f9859597c130d40d4b146fecdab51b3a8"
integrity sha512-71LLMbUq2r02ng2We9S215LtPu3fY0KgaGE0k8WRgl6RkqxtGfl7HUozz1Dftwsb0D/5mZ8dwAaPbtnzfvbEwQ==
redux-thunk@^3.1.0:
version "3.1.0"
resolved "https://registry.yarnpkg.com/redux-thunk/-/redux-thunk-3.1.0.tgz#94aa6e04977c30e14e892eae84978c1af6058ff3"
@ -6044,7 +6220,7 @@ semver@^6.3.0, semver@^6.3.1:
resolved "https://registry.yarnpkg.com/semver/-/semver-6.3.1.tgz#556d2ef8689146e46dcea4bfdd095f3434dffcb4"
integrity sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==
semver@^7.1.3, semver@^7.5.2, semver@^7.5.3, semver@^7.5.4, semver@^7.7.3:
semver@^7.1.3, semver@^7.5.2, semver@^7.5.3, semver@^7.5.4, semver@^7.7.3, semver@^7.7.4:
version "7.8.5"
resolved "https://registry.yarnpkg.com/semver/-/semver-7.8.5.tgz#39b646037dd50c14fb451e7e4cac58ed8b863f69"
integrity sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA==