diff --git a/ReactotronConfig.js b/ReactotronConfig.js
new file mode 100644
index 0000000..20146cb
--- /dev/null
+++ b/ReactotronConfig.js
@@ -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!
diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml
index fb78f39..26d410d 100644
--- a/android/app/src/main/AndroidManifest.xml
+++ b/android/app/src/main/AndroidManifest.xml
@@ -1,6 +1,8 @@
+
+
+
diff --git a/android/build.gradle b/android/build.gradle
index dad99b0..997cc0f 100644
--- a/android/build.gradle
+++ b/android/build.gradle
@@ -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"
+ }
+ }
+}
diff --git a/app/App.tsx b/app/App.tsx
index b5ed7a8..a841347 100644
--- a/app/App.tsx
+++ b/app/App.tsx
@@ -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 (
-
-
-
-
+
+
+
+
+
+
);
}
diff --git a/app/api/authApi.ts b/app/api/authApi.ts
new file mode 100644
index 0000000..66ac0a2
--- /dev/null
+++ b/app/api/authApi.ts
@@ -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('/auth/otp/request', {
+ phone,
+ });
+ return result;
+};
+
+export const verifyOtpApi = async (
+ phone: string,
+ code: string,
+ role: string,
+) => {
+ return await apiClient.post('/auth/otp/verify', {
+ phone,
+ code,
+ role,
+ });
+};
+
+export const getProfile = async (
+ partnerId: string,
+): Promise => {
+ // return (await apiClient.get(`/profile/${partnerId}`)).data;
+ await new Promise(r => setTimeout(r, 500));
+ return mockProfile;
+};
+
+export const updateProfile = async (
+ updates: Partial,
+): Promise => {
+ // return (await apiClient.patch('/profile', updates)).data;
+ await new Promise(r => setTimeout(r, 800));
+ return { ...mockProfile, ...updates };
+};
diff --git a/app/api/dashboardApi.ts b/app/api/dashboardApi.ts
new file mode 100644
index 0000000..6501af4
--- /dev/null
+++ b/app/api/dashboardApi.ts
@@ -0,0 +1,12 @@
+import { ToggleStatusPayload, ToggleStatusResponse } from '@interfaces';
+import { apiClient } from '@services';
+
+export const toggleStatus = async (
+ payload: ToggleStatusPayload,
+): Promise => {
+ const response = await apiClient.patch(
+ '/delivery-partners/availability',
+ payload,
+ );
+ return response;
+};
diff --git a/app/api/earningsApi.ts b/app/api/earningsApi.ts
new file mode 100644
index 0000000..54cbde4
--- /dev/null
+++ b/app/api/earningsApi.ts
@@ -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 => {
+ // return (await apiClient.get('/earnings/today')).data;
+ await new Promise((r) => setTimeout(r, 500));
+ return mockTodayEarnings;
+};
+
+export const getWeeklyEarnings = async (): Promise => {
+ // return (await apiClient.get('/earnings/weekly')).data;
+ await new Promise((r) => setTimeout(r, 500));
+ return mockWeeklyEarnings;
+};
diff --git a/app/api/index.ts b/app/api/index.ts
new file mode 100644
index 0000000..7655466
--- /dev/null
+++ b/app/api/index.ts
@@ -0,0 +1,4 @@
+export * from './authApi';
+export * from './onBoardApi';
+export * from './kycApi';
+export * from './dashboardApi';
diff --git a/app/api/jobApi.ts b/app/api/jobApi.ts
new file mode 100644
index 0000000..858ae67
--- /dev/null
+++ b/app/api/jobApi.ts
@@ -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 => {
+ // return (await apiClient.post(`/jobs/${jobId}/accept`)).data;
+ await new Promise((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 => {
+ // await apiClient.post(`/jobs/${jobId}/reject`);
+ await new Promise((r) => setTimeout(r, 300));
+};
+
+export const confirmPickup = async (jobId: string): Promise => {
+ // await apiClient.post(`/jobs/${jobId}/pickup`);
+ await new Promise((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((r) => setTimeout(r, 800));
+ return { earnings: 78 };
+};
+
+export const reportIssue = async (jobId: string, reason: string): Promise => {
+ // await apiClient.post(`/jobs/${jobId}/issue`, { reason });
+ await new Promise((r) => setTimeout(r, 500));
+};
diff --git a/app/api/kycApi.ts b/app/api/kycApi.ts
new file mode 100644
index 0000000..0ddbe44
--- /dev/null
+++ b/app/api/kycApi.ts
@@ -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 => {
+ 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('/kyc/upload', formData);
+};
+
+export const getMyKyc = async (): Promise => {
+ return await apiClient.get('/kyc/my-details');
+};
diff --git a/app/api/onBoardApi.ts b/app/api/onBoardApi.ts
new file mode 100644
index 0000000..a677a38
--- /dev/null
+++ b/app/api/onBoardApi.ts
@@ -0,0 +1,12 @@
+import { DashboardResponce, OnBoardPayload, UserProfile } from '@interfaces';
+import { apiClient } from '@services';
+
+export const onBoardComplete = async (
+ payload: OnBoardPayload,
+): Promise => {
+ return await apiClient.post('/auth/onboard', payload);
+};
+
+export const getDashboardApi = async (): Promise => {
+ return await apiClient.get('/delivery-partners/dashboard');
+};
diff --git a/app/components/index.ts b/app/components/index.ts
index e7a304b..db07110 100644
--- a/app/components/index.ts
+++ b/app/components/index.ts
@@ -20,3 +20,4 @@ export * from './screenHeader/screenHeader';
export * from './sectionHeader/sectionHeader';
export * from './statusBadge/statusBadge';
export * from './toast/toast';
+export * from './kycDocumentCard';
diff --git a/app/components/kycDocumentCard/KycDocumentCard.styles.ts b/app/components/kycDocumentCard/KycDocumentCard.styles.ts
new file mode 100644
index 0000000..ebfa854
--- /dev/null
+++ b/app/components/kycDocumentCard/KycDocumentCard.styles.ts
@@ -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',
+ },
+ });
diff --git a/app/components/kycDocumentCard/KycDocumentCard.tsx b/app/components/kycDocumentCard/KycDocumentCard.tsx
new file mode 100644
index 0000000..b3a1b4c
--- /dev/null
+++ b/app/components/kycDocumentCard/KycDocumentCard.tsx
@@ -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) => void;
+ onRemove?: () => void;
+ readOnly?: boolean;
+}
+
+const UploadIcon = ({ size = 18, color = '#666' }) => (
+
+);
+
+export const KycDocumentCard: React.FC = ({
+ 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 (
+
+
+ {label}
+
+
+ );
+ };
+
+ return (
+
+
+ Document {index + 1}
+ {readOnly && renderStatusBadge(document.status)}
+ {!readOnly && onRemove && (
+
+ Remove
+
+ )}
+
+
+ {/* Document Name / Type */}
+
+ Document Name / Type
+ onUpdate?.({ name: text })}
+ autoCapitalize="sentences"
+ editable={!readOnly}
+ />
+
+
+ {/* Document Identification Number */}
+
+ Document Identification Number
+ onUpdate?.({ idNumber: text })}
+ autoCapitalize="characters"
+ editable={!readOnly}
+ />
+
+
+ {/* Document Attachment */}
+
+ Document Attachment File (JPG, PNG, PDF)
+
+
+ {document.fileName ? document.fileName : 'SELECT DOCUMENT FILE...'}
+
+ {!readOnly && }
+
+ {!readOnly && (
+
+ Supported: pdf, png, jpg, or jpeg under 200kb only.
+
+ )}
+
+
+ );
+};
+
+export default KycDocumentCard;
diff --git a/app/components/kycDocumentCard/index.ts b/app/components/kycDocumentCard/index.ts
new file mode 100644
index 0000000..a0cd58f
--- /dev/null
+++ b/app/components/kycDocumentCard/index.ts
@@ -0,0 +1 @@
+export * from './KycDocumentCard';
diff --git a/app/features/screens/LoginScreen/LoginScreen.tsx b/app/features/screens/LoginScreen/LoginScreen.tsx
index 582dffc..25797e2 100644
--- a/app/features/screens/LoginScreen/LoginScreen.tsx
+++ b/app/features/screens/LoginScreen/LoginScreen.tsx
@@ -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 = () => {
diff --git a/app/features/screens/OtpScreen/OtpScreen.tsx b/app/features/screens/OtpScreen/OtpScreen.tsx
index 6d05993..b12bd9d 100644
--- a/app/features/screens/OtpScreen/OtpScreen.tsx
+++ b/app/features/screens/OtpScreen/OtpScreen.tsx
@@ -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>();
-
+ const navigation = useNavigation<
+ CompositeNavigationProp<
+ NativeStackNavigationProp,
+ NativeStackNavigationProp
+ >
+ >();
+
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 (
-
+
Verify Phone
@@ -83,7 +94,10 @@ export const OtpScreen: React.FC<{ route: any }> = ({ route }) => {
{timer > 0 ? (
- Resend code in 00:{timer < 10 ? `0${timer}` : timer}
+ Resend code in{' '}
+
+ 00:{timer < 10 ? `0${timer}` : timer}
+
) : (
@@ -95,7 +109,7 @@ export const OtpScreen: React.FC<{ route: any }> = ({ route }) => {
@@ -104,7 +118,7 @@ export const OtpScreen: React.FC<{ route: any }> = ({ route }) => {
diff --git a/app/features/screens/arrivedAtCustomerScreen/index.ts b/app/features/screens/arrivedAtCustomerScreen/index.ts
index 9865456..3480d4a 100644
--- a/app/features/screens/arrivedAtCustomerScreen/index.ts
+++ b/app/features/screens/arrivedAtCustomerScreen/index.ts
@@ -1 +1 @@
-export { default } from "./arrivedAtCustomerScreen";
+export * from './arrivedAtCustomerScreen';
diff --git a/app/features/screens/arrivedAtStoreScreen/index.ts b/app/features/screens/arrivedAtStoreScreen/index.ts
index 68f5ac8..e6c389f 100644
--- a/app/features/screens/arrivedAtStoreScreen/index.ts
+++ b/app/features/screens/arrivedAtStoreScreen/index.ts
@@ -1 +1 @@
-export { default } from "./arrivedAtStoreScreen";
+export * from './arrivedAtStoreScreen';
diff --git a/app/features/screens/completeProfileScreen/completeProfileScreen.styles.ts b/app/features/screens/completeProfileScreen/completeProfileScreen.styles.ts
index 0e95460..8800ff2 100644
--- a/app/features/screens/completeProfileScreen/completeProfileScreen.styles.ts
+++ b/app/features/screens/completeProfileScreen/completeProfileScreen.styles.ts
@@ -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,
+ },
});
diff --git a/app/features/screens/completeProfileScreen/completeProfileScreen.tsx b/app/features/screens/completeProfileScreen/completeProfileScreen.tsx
index 40b5cbc..2610ce2 100644
--- a/app/features/screens/completeProfileScreen/completeProfileScreen.tsx
+++ b/app/features/screens/completeProfileScreen/completeProfileScreen.tsx
@@ -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>();
+ const navigation =
+ useNavigation>();
- 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('');
+ const [vehicleType, setVehicleType] = useState('');
+ // 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 (
-
+
Complete Profile
- Enter details & upload documents to get verified
+
+ Enter details & upload documents to get verified
+
{/* Profile Avatar Upload Mock */}
-
+
Add Photo
- +
+
+ +
+
@@ -87,8 +144,28 @@ export const CompleteProfileScreen: React.FC = () => {
/>
- {/* Required Documents Checklist */}
+ {/* Gender Selection */}
+ Gender
+ setGender(val)}
+ />
+
+
+ {/* Vehicle Type Selection */}
+
+ Vehicle Type
+ setVehicleType(val)}
+ />
+
+
+ {/* Required Documents Checklist */}
+ {/*
Required Documents
{documents.map((doc) => (
{
))}
-
+ */}
{
+ 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;
+ });
+});
diff --git a/app/features/screens/completeProfileScreen/thunk.ts b/app/features/screens/completeProfileScreen/thunk.ts
new file mode 100644
index 0000000..bc78a2e
--- /dev/null
+++ b/app/features/screens/completeProfileScreen/thunk.ts
@@ -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);
+ }
+ },
+);
diff --git a/app/features/screens/confirmPickupScreen/confirmPickupScreen.tsx b/app/features/screens/confirmPickupScreen/confirmPickupScreen.tsx
index d710119..2f6d952 100644
--- a/app/features/screens/confirmPickupScreen/confirmPickupScreen.tsx
+++ b/app/features/screens/confirmPickupScreen/confirmPickupScreen.tsx
@@ -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';
diff --git a/app/features/screens/confirmPickupScreen/index.ts b/app/features/screens/confirmPickupScreen/index.ts
index d5e8e83..c0a2e40 100644
--- a/app/features/screens/confirmPickupScreen/index.ts
+++ b/app/features/screens/confirmPickupScreen/index.ts
@@ -1 +1 @@
-export { default } from "./confirmPickupScreen";
+export * from './confirmPickupScreen';
diff --git a/app/features/screens/dashboardScreen/dashboardScreen.tsx b/app/features/screens/dashboardScreen/dashboardScreen.tsx
index d1576db..f87f8db 100644
--- a/app/features/screens/dashboardScreen/dashboardScreen.tsx
+++ b/app/features/screens/dashboardScreen/dashboardScreen.tsx
@@ -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>();
+ const navigation =
+ useNavigation>();
- 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 | 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 (
-
+
{/* Header Section */}
Good Morning ☀️
Rahul Kumar
-
+
{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 }}
/>
@@ -155,11 +205,13 @@ const DashboardScreen: React.FC = () => {
Earnings
-
+
- {isOnline ? completedCount : 0}
+
+ {isOnline ? completedCount : 0}
+
Completed
@@ -182,7 +234,9 @@ const DashboardScreen: React.FC = () => {
Your Performance
- 4.8 ★
+
+ 4.8 ★
+
Rating
@@ -202,7 +256,9 @@ const DashboardScreen: React.FC = () => {
{weeklyData.map((data, index) => {
// Bar height percent
- const heightPercent = `${(data.amount / maxWeeklyAmount) * 85}%`;
+ const heightPercent = `${
+ (data.amount / maxWeeklyAmount) * 85
+ }%`;
return (
₹{data.amount}
@@ -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}
>
- ⚡ Simulate Immediate Job Offer
+
+ ⚡ Simulate Immediate Job Offer
+
{
>
) : (
- Go Online to start receiving delivery requests
+
+ Go Online to start receiving delivery requests
+
- 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.
{
Alert.alert('Emergency SOS', 'Calling nearest security/emergency dispatch...')}
+ onPress={() =>
+ Alert.alert(
+ 'Emergency SOS',
+ 'Calling nearest security/emergency dispatch...',
+ )
+ }
>
Emergency SOS
@@ -266,7 +335,11 @@ const DashboardScreen: React.FC = () => {
navigation.navigate(RouteNames.BottomTabs, { screen: RouteNames.Profile })}
+ onPress={() =>
+ navigation.navigate(RouteNames.BottomTabs, {
+ screen: RouteNames.Profile,
+ })
+ }
>
FAQ Center
@@ -275,7 +348,12 @@ const DashboardScreen: React.FC = () => {
Alert.alert('Support Chat', 'Initiating live helpdesk agent chat...')}
+ onPress={() =>
+ Alert.alert(
+ 'Support Chat',
+ 'Initiating live helpdesk agent chat...',
+ )
+ }
>
Live Support
@@ -286,47 +364,83 @@ const DashboardScreen: React.FC = () => {
{/* Active Job Floating Bottom Banner */}
- {jobStatus !== JobStatus.Idle && jobStatus !== JobStatus.NewRequest && activeJob && (
-
-
-
-
-
- Order {activeJob.orderId}
-
-
- {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 && (
+
+
+
+
+
+ Order {activeJob.orderId}
+
+
+ {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...'}
+
+
+
+
+
+ View
-
-
- View
-
-
- )}
+
+ )}
);
};
-export default DashboardScreen;
+// export default DashboardScreen;
diff --git a/app/features/screens/dashboardScreen/index.ts b/app/features/screens/dashboardScreen/index.ts
index 4b60b10..b9bcb0a 100644
--- a/app/features/screens/dashboardScreen/index.ts
+++ b/app/features/screens/dashboardScreen/index.ts
@@ -1 +1,3 @@
-export { default } from './dashboardScreen';
+export * from './dashboardScreen';
+export * from './reducer';
+export * from './thunk';
diff --git a/app/features/screens/dashboardScreen/reducer.ts b/app/features/screens/dashboardScreen/reducer.ts
new file mode 100644
index 0000000..0bf2b00
--- /dev/null
+++ b/app/features/screens/dashboardScreen/reducer.ts
@@ -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;
+ });
+});
diff --git a/app/features/screens/dashboardScreen/thunk.ts b/app/features/screens/dashboardScreen/thunk.ts
new file mode 100644
index 0000000..4f07aa8
--- /dev/null
+++ b/app/features/screens/dashboardScreen/thunk.ts
@@ -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);
+ }
+ },
+);
diff --git a/app/features/screens/deliverOrderScreen/deliverOrderScreen.tsx b/app/features/screens/deliverOrderScreen/deliverOrderScreen.tsx
index 4af7e83..36cb220 100644
--- a/app/features/screens/deliverOrderScreen/deliverOrderScreen.tsx
+++ b/app/features/screens/deliverOrderScreen/deliverOrderScreen.tsx
@@ -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';
diff --git a/app/features/screens/deliverOrderScreen/index.ts b/app/features/screens/deliverOrderScreen/index.ts
index aa04fc0..c2755b1 100644
--- a/app/features/screens/deliverOrderScreen/index.ts
+++ b/app/features/screens/deliverOrderScreen/index.ts
@@ -1 +1 @@
-export { default } from "./deliverOrderScreen";
+export * from './deliverOrderScreen';
diff --git a/app/features/screens/deliveryCompletedScreen/index.ts b/app/features/screens/deliveryCompletedScreen/index.ts
index 7614996..c53fd57 100644
--- a/app/features/screens/deliveryCompletedScreen/index.ts
+++ b/app/features/screens/deliveryCompletedScreen/index.ts
@@ -1 +1 @@
-export { default } from "./deliveryCompletedScreen";
+export * from './deliveryCompletedScreen';
diff --git a/app/features/screens/documentsScreen/index.ts b/app/features/screens/documentsScreen/index.ts
new file mode 100644
index 0000000..37cab39
--- /dev/null
+++ b/app/features/screens/documentsScreen/index.ts
@@ -0,0 +1 @@
+export * from './documentsScreen';
diff --git a/app/features/screens/index.ts b/app/features/screens/index.ts
index 011203b..a00bbe4 100644
--- a/app/features/screens/index.ts
+++ b/app/features/screens/index.ts
@@ -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';
diff --git a/app/features/screens/kycScreen/index.ts b/app/features/screens/kycScreen/index.ts
new file mode 100644
index 0000000..6a012a7
--- /dev/null
+++ b/app/features/screens/kycScreen/index.ts
@@ -0,0 +1,3 @@
+export * from './kycScreen';
+export * from './reducer';
+export * from './thunk';
diff --git a/app/features/screens/kycScreen/kycScreen.styles.ts b/app/features/screens/kycScreen/kycScreen.styles.ts
new file mode 100644
index 0000000..7850f60
--- /dev/null
+++ b/app/features/screens/kycScreen/kycScreen.styles.ts
@@ -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,
+ },
+ });
diff --git a/app/features/screens/kycScreen/kycScreen.tsx b/app/features/screens/kycScreen/kycScreen.tsx
new file mode 100644
index 0000000..f5b58b2
--- /dev/null
+++ b/app/features/screens/kycScreen/kycScreen.tsx
@@ -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' }) => (
+
+);
+
+// Inline Plus icon for adding a new document
+const PlusIcon = ({ size = 16, color = '#333' }) => (
+
+);
+
+// Inline Submit/Disk icon for the submit button
+const SubmitIcon = ({ size = 18, color = '#fff' }) => (
+
+);
+
+export const KycScreen: React.FC = () => {
+ const { colors } = useAppTheme();
+ const styles = getStyles(colors);
+ const navigation =
+ useNavigation>();
+ const dispatch = useAppDispatch();
+
+ // Get upload loading status from KYC store slice
+ const { isLoading, myKycResponse } = useAppSelector(state => state.kyc);
+
+ const [documents, setDocuments] = useState([
+ {
+ 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,
+ ) => {
+ 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 (
+
+ navigation.goBack()}
+ />
+
+
+ {/* Compliance Check Required Banner */}
+
+
+
+
+
+
+ KYC Compliance Check Required
+
+
+ Please upload store identity documents to configure payouts,
+ deliveries, and operational scopes.
+
+
+
+
+ {/* Upload KYC verification documents main card header info */}
+
+
+ {hasSubmittedDocs
+ ? 'Submitted KYC Documents'
+ : 'Upload KYC Verification Documents'}
+
+
+ {hasSubmittedDocs
+ ? 'Below are the KYC documents submitted for verification.'
+ : 'Provide details and attachments to initiate compliance verification.'}
+
+
+
+
+ {/* Render each document card */}
+ {displayedDocuments.map((doc, idx) => (
+ handleUpdateDocument(doc.id, fields)
+ : undefined
+ }
+ onRemove={
+ !hasSubmittedDocs && idx > 0
+ ? () => handleRemoveDocument(doc.id)
+ : undefined
+ }
+ readOnly={hasSubmittedDocs}
+ />
+ ))}
+
+ {!hasSubmittedDocs && (
+ <>
+ {/* Add another document button */}
+
+
+
+ Add Another Document
+
+
+
+ {/* Upload & Submit KYC Action Button */}
+
+ {isLoading ? (
+
+ ) : (
+ <>
+
+ Upload & Submit KYC
+ >
+ )}
+
+ >
+ )}
+ {hasSubmittedDocs && (
+ {
+ navigation.navigate(RouteNames.OnboardingComplete);
+ }}
+ disabled={isLoading}
+ activeOpacity={0.8}
+ >
+ {isLoading ? (
+
+ ) : (
+ <>
+
+ Next Page
+ >
+ )}
+
+ )}
+
+
+ {/* Loading Overlay for the focus fetch check */}
+
+
+ );
+};
+
+export default KycScreen;
diff --git a/app/features/screens/kycScreen/reducer.ts b/app/features/screens/kycScreen/reducer.ts
new file mode 100644
index 0000000..933ce66
--- /dev/null
+++ b/app/features/screens/kycScreen/reducer.ts
@@ -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;
+ });
+});
diff --git a/app/features/screens/kycScreen/thunk.ts b/app/features/screens/kycScreen/thunk.ts
new file mode 100644
index 0000000..0c2010e
--- /dev/null
+++ b/app/features/screens/kycScreen/thunk.ts
@@ -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);
+ }
+ },
+);
diff --git a/app/features/screens/liveTrackingScreen/liveTrackingScreen.tsx b/app/features/screens/liveTrackingScreen/liveTrackingScreen.tsx
index 83eef2b..c4298c1 100644
--- a/app/features/screens/liveTrackingScreen/liveTrackingScreen.tsx
+++ b/app/features/screens/liveTrackingScreen/liveTrackingScreen.tsx
@@ -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';
diff --git a/app/features/screens/newJobRequestScreen/newJobRequestScreen.tsx b/app/features/screens/newJobRequestScreen/newJobRequestScreen.tsx
index 9d8d383..d470088 100644
--- a/app/features/screens/newJobRequestScreen/newJobRequestScreen.tsx
+++ b/app/features/screens/newJobRequestScreen/newJobRequestScreen.tsx
@@ -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 });
};
diff --git a/app/features/screens/onboardingCompleteScreen/index.ts b/app/features/screens/onboardingCompleteScreen/index.ts
index 21ead7c..02110f2 100644
--- a/app/features/screens/onboardingCompleteScreen/index.ts
+++ b/app/features/screens/onboardingCompleteScreen/index.ts
@@ -1 +1,3 @@
-export { default } from './onboardingCompleteScreen';
+export * from './onboardingCompleteScreen';
+export * from './thunk';
+export * from './reducer';
diff --git a/app/features/screens/onboardingCompleteScreen/onboardingCompleteScreen.tsx b/app/features/screens/onboardingCompleteScreen/onboardingCompleteScreen.tsx
index 3b47c9a..42d7fba 100644
--- a/app/features/screens/onboardingCompleteScreen/onboardingCompleteScreen.tsx
+++ b/app/features/screens/onboardingCompleteScreen/onboardingCompleteScreen.tsx
@@ -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>();
const styles = getStyles(colors);
- const navigation = useNavigation>();
+ const dispatch = useAppDispatch();
+
+ useFocusEffect(
+ useCallback(() => {
+ dispatch(getDashboard());
+ }, [dispatch]),
+ );
+
+ const { isKycApproved } = useAppSelector(state => state.onboardingComplete);
return (
@@ -34,21 +46,23 @@ const OnboardingCompleteScreen: React.FC = () => {
Onboarding Completed!
- 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.
- 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}
/>
);
};
-
-export default OnboardingCompleteScreen;
diff --git a/app/features/screens/onboardingCompleteScreen/reducer.ts b/app/features/screens/onboardingCompleteScreen/reducer.ts
new file mode 100644
index 0000000..b2e3d5e
--- /dev/null
+++ b/app/features/screens/onboardingCompleteScreen/reducer.ts
@@ -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';
+ });
+ },
+);
diff --git a/app/features/screens/onboardingCompleteScreen/thunk.ts b/app/features/screens/onboardingCompleteScreen/thunk.ts
new file mode 100644
index 0000000..a5e3675
--- /dev/null
+++ b/app/features/screens/onboardingCompleteScreen/thunk.ts
@@ -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);
+ }
+ },
+);
diff --git a/app/features/screens/orderAcceptedScreen/orderAcceptedScreen.tsx b/app/features/screens/orderAcceptedScreen/orderAcceptedScreen.tsx
index 0811b9d..7317483 100644
--- a/app/features/screens/orderAcceptedScreen/orderAcceptedScreen.tsx
+++ b/app/features/screens/orderAcceptedScreen/orderAcceptedScreen.tsx
@@ -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';
diff --git a/app/features/screens/orderPickedUpScreen/orderPickedUpScreen.tsx b/app/features/screens/orderPickedUpScreen/orderPickedUpScreen.tsx
index e9dae57..948e15c 100644
--- a/app/features/screens/orderPickedUpScreen/orderPickedUpScreen.tsx
+++ b/app/features/screens/orderPickedUpScreen/orderPickedUpScreen.tsx
@@ -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';
diff --git a/app/features/screens/profileScreen/profileScreen.tsx b/app/features/screens/profileScreen/profileScreen.tsx
index b04a8e3..ee90c5e 100644
--- a/app/features/screens/profileScreen/profileScreen.tsx
+++ b/app/features/screens/profileScreen/profileScreen.tsx
@@ -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';
diff --git a/app/features/screens/setLocationScreen/index.ts b/app/features/screens/setLocationScreen/index.ts
index 54083bb..d55e480 100644
--- a/app/features/screens/setLocationScreen/index.ts
+++ b/app/features/screens/setLocationScreen/index.ts
@@ -1 +1,2 @@
-export { default } from "./setLocationScreen";
+export * from './setLocationScreen';
+export * from './reducer';
diff --git a/app/features/screens/setLocationScreen/reducer.ts b/app/features/screens/setLocationScreen/reducer.ts
new file mode 100644
index 0000000..f7dc29e
--- /dev/null
+++ b/app/features/screens/setLocationScreen/reducer.ts
@@ -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;
diff --git a/app/features/screens/setLocationScreen/setLocationScreen.styles.ts b/app/features/screens/setLocationScreen/setLocationScreen.styles.ts
index 3386ba9..3202dca 100644
--- a/app/features/screens/setLocationScreen/setLocationScreen.styles.ts
+++ b/app/features/screens/setLocationScreen/setLocationScreen.styles.ts
@@ -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,
},
});
diff --git a/app/features/screens/setLocationScreen/setLocationScreen.tsx b/app/features/screens/setLocationScreen/setLocationScreen.tsx
index f7d2529..7d8d8f0 100644
--- a/app/features/screens/setLocationScreen/setLocationScreen.tsx
+++ b/app/features/screens/setLocationScreen/setLocationScreen.tsx
@@ -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;
+
+const DELTA = { latitudeDelta: 0.006, longitudeDelta: 0.006 };
+
+export const SetLocationScreen: React.FC = () => {
const { colors } = useAppTheme();
const styles = getStyles(colors);
- const navigation = useNavigation>();
+ const navigation = useNavigation();
+ const dispatch = useAppDispatch();
+ const mapRef = useRef(null);
+ // Guard: never call setState after the component has unmounted
+ const isMounted = useRef(true);
+
+ const [coords, setCoords] = useState(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 (
- {/* Blank Map Placeholder Area */}
-
-
- {/* Pulsing marker simulation */}
-
-
-
- Select Location
- Drag the map to pinpoint your location
-
-
+ {/* ---------- Map ---------- */}
+
+
+
- {/* Bottom Sheet Location Selector Card */}
-
-
-
-
+ {/* ---------- "Locate me" floating button ---------- */}
+
+ 📍
+
+
+ {/* ---------- Bottom sheet ---------- */}
+
+
+
+
+
+ 📍
+ My Location
- Choose Delivery Area
-
-
- Current Address
-
- MG Road, Ashok Nagar, Bengaluru, Karnataka 560001
-
+ {loading ? (
+
+ ) : (
+
+ {address}
+
+ )}
+
+
+ Change
+
navigation.navigate(RouteNames.CompleteProfile)}
+ onPress={() => {
+ dispatch(
+ setLocationData({
+ latitude: coords.latitude,
+ longitude: coords.longitude,
+ mapAddress: address,
+ }),
+ );
+ navigation.navigate(RouteNames.CompleteProfile);
+ }}
style={styles.confirmButton}
/>
+
+ navigation.navigate(RouteNames.CompleteProfile)}
+ activeOpacity={0.7}
+ >
+ Enter address manually
+
);
};
-
-export default SetLocationScreen;
diff --git a/app/features/screens/setLocationScreen/thunk.ts b/app/features/screens/setLocationScreen/thunk.ts
new file mode 100644
index 0000000..e69de29
diff --git a/app/interfaces/auth.ts b/app/interfaces/auth.ts
new file mode 100644
index 0000000..317c15e
--- /dev/null
+++ b/app/interfaces/auth.ts
@@ -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',
+}
diff --git a/app/interfaces/dashboard.ts b/app/interfaces/dashboard.ts
new file mode 100644
index 0000000..17040a6
--- /dev/null
+++ b/app/interfaces/dashboard.ts
@@ -0,0 +1,9 @@
+export interface ToggleStatusPayload {
+ availabilityStatus: string;
+}
+
+export interface ToggleStatusResponse {
+ id: string;
+ availabilityStatus: string;
+ message: string;
+}
diff --git a/app/interfaces/index.ts b/app/interfaces/index.ts
new file mode 100644
index 0000000..faa0f63
--- /dev/null
+++ b/app/interfaces/index.ts
@@ -0,0 +1,4 @@
+export * from './auth';
+export * from './onboard';
+export * from './kyc';
+export * from './dashboard';
diff --git a/app/interfaces/kyc.ts b/app/interfaces/kyc.ts
new file mode 100644
index 0000000..d1731a9
--- /dev/null
+++ b/app/interfaces/kyc.ts
@@ -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[];
+}
diff --git a/app/interfaces/onboard.ts b/app/interfaces/onboard.ts
new file mode 100644
index 0000000..1a3472a
--- /dev/null
+++ b/app/interfaces/onboard.ts
@@ -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;
+ };
+ };
+}
diff --git a/app/navigation/authStack.tsx b/app/navigation/authStack.tsx
index 694143a..b295f91 100644
--- a/app/navigation/authStack.tsx
+++ b/app/navigation/authStack.tsx
@@ -17,18 +17,6 @@ const AuthStack: React.FC = () => {
-
-
-
);
};
diff --git a/app/navigation/bottomTabNavigator.tsx b/app/navigation/bottomTabNavigator.tsx
index cac6463..b71814e 100644
--- a/app/navigation/bottomTabNavigator.tsx
+++ b/app/navigation/bottomTabNavigator.tsx
@@ -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 },
}}
>
-
-
-
-
-
diff --git a/app/navigation/navigationTypes.ts b/app/navigation/navigationTypes.ts
index 79b47ed..14c7311 100644
--- a/app/navigation/navigationTypes.ts
+++ b/app/navigation/navigationTypes.ts
@@ -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;
App: NavigatorScreenParams;
+ OnBoarding: NavigatorScreenParams;
};
diff --git a/app/navigation/onBoardingStack.tsx b/app/navigation/onBoardingStack.tsx
new file mode 100644
index 0000000..a38ace6
--- /dev/null
+++ b/app/navigation/onBoardingStack.tsx
@@ -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();
+
+const onBoardingStack: React.FC = () => {
+ return (
+
+ {/* */}
+ {/* */}
+
+
+
+ );
+};
+
+export default onBoardingStack;
diff --git a/app/navigation/rootNavigator.tsx b/app/navigation/rootNavigator.tsx
index 52b0b91..dd71734 100644
--- a/app/navigation/rootNavigator.tsx
+++ b/app/navigation/rootNavigator.tsx
@@ -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();
@@ -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 (
-
-
+ {!accessToken ? (
+
+ ) : isOnboarding ? (
+
+ ) : (
+
+ )}
);
diff --git a/app/services/apiClient.ts b/app/services/apiClient.ts
new file mode 100644
index 0000000..b70f914
--- /dev/null
+++ b/app/services/apiClient.ts
@@ -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 {
+ return AsyncStorage.getItem(STORAGE_KEYS.ACCESS_TOKEN);
+ },
+
+ async getRefreshToken(): Promise {
+ return AsyncStorage.getItem(STORAGE_KEYS.REFRESH_TOKEN);
+ },
+
+ async setTokens(accessToken: string, refreshToken: string): Promise {
+ await AsyncStorage.setItem(STORAGE_KEYS.ACCESS_TOKEN, accessToken);
+ await AsyncStorage.setItem(STORAGE_KEYS.REFRESH_TOKEN, refreshToken);
+ },
+
+ async clearTokens(): Promise {
+ 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((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(url: string, config?: AxiosRequestConfig): Promise {
+ const response = await axiosInstance.get(url, config);
+ return response.data;
+ },
+
+ async post(
+ url: string,
+ body?: unknown,
+ config?: AxiosRequestConfig,
+ ): Promise {
+ const response = await axiosInstance.post(url, body, config);
+ return response.data;
+ },
+
+ async put(
+ url: string,
+ body?: unknown,
+ config?: AxiosRequestConfig,
+ ): Promise {
+ const response = await axiosInstance.put(url, body, config);
+ return response.data;
+ },
+
+ async patch(
+ url: string,
+ body?: unknown,
+ config?: AxiosRequestConfig,
+ ): Promise {
+ const response = await axiosInstance.patch(url, body, config);
+ return response.data;
+ },
+
+ async delete(url: string, config?: AxiosRequestConfig): Promise {
+ const response = await axiosInstance.delete(url, config);
+ return response.data;
+ },
+};
+
+// Export the raw instance if needed for advanced usage
+export { axiosInstance };
diff --git a/app/services/index.ts b/app/services/index.ts
new file mode 100644
index 0000000..773563e
--- /dev/null
+++ b/app/services/index.ts
@@ -0,0 +1,2 @@
+export * from './apiClient';
+export * from './locationService';
diff --git a/app/services/locationService.ts b/app/services/locationService.ts
index 77bc34d..ceaa19d 100644
--- a/app/services/locationService.ts
+++ b/app/services/locationService.ts
@@ -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 {
- 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 => {
+ 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(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 =>
+ new Promise((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 => {
+ 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 => {
+ 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 => {
+ 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 };
+ };
diff --git a/app/store/api/authApi.ts b/app/store/api/authApi.ts
deleted file mode 100644
index b2390d7..0000000
--- a/app/store/api/authApi.ts
+++ /dev/null
@@ -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((r) => setTimeout(r, 1000));
- return { data: mockSendOtpResponse };
- },
- }),
- verifyOtp: builder.mutation<
- { token: string; partnerId: string },
- { requestId: string; otp: string }
- >({
- queryFn: async ({ otp }) => {
- await new Promise((r) => setTimeout(r, 1000));
- if (otp === '000000') {
- return { error: { status: 'CUSTOM_ERROR', error: 'Invalid OTP' } };
- }
- return { data: mockVerifyOtpResponse };
- },
- }),
- getProfile: builder.query({
- queryFn: async (_partnerId) => {
- await new Promise((r) => setTimeout(r, 500));
- return { data: mockProfile };
- },
- }),
- updateProfile: builder.mutation<
- PartnerProfile,
- Partial
- >({
- queryFn: async (updates) => {
- await new Promise((r) => setTimeout(r, 800));
- return { data: { ...mockProfile, ...updates } };
- },
- }),
- }),
-});
-
-export const {
- useSendOtpMutation,
- useVerifyOtpMutation,
- useGetProfileQuery,
- useUpdateProfileMutation,
-} = authApi;
diff --git a/app/store/api/earningsApi.ts b/app/store/api/earningsApi.ts
deleted file mode 100644
index fb69207..0000000
--- a/app/store/api/earningsApi.ts
+++ /dev/null
@@ -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({
- queryFn: async () => {
- await new Promise((r) => setTimeout(r, 500));
- return { data: mockTodayEarnings };
- },
- }),
- getWeeklyEarnings: builder.query({
- queryFn: async () => {
- await new Promise((r) => setTimeout(r, 500));
- return { data: mockWeeklyEarnings };
- },
- }),
- }),
-});
-
-export const { useGetTodayEarningsQuery, useGetWeeklyEarningsQuery } = earningsApi;
diff --git a/app/store/api/jobApi.ts b/app/store/api/jobApi.ts
deleted file mode 100644
index 28766af..0000000
--- a/app/store/api/jobApi.ts
+++ /dev/null
@@ -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({
- queryFn: async (jobId) => {
- await new Promise((r) => setTimeout(r, 500));
- return {
- data: {
- ...mockJobOffer,
- jobId,
- orderId: '#ORD125487',
- status: JobStatus.Accepted,
- acceptedAt: new Date().toISOString(),
- collectAmount: 0,
- },
- };
- },
- }),
- rejectJob: builder.mutation({
- queryFn: async (_jobId) => {
- await new Promise((r) => setTimeout(r, 300));
- return { data: undefined };
- },
- }),
- confirmPickup: builder.mutation({
- queryFn: async (_jobId) => {
- await new Promise((r) => setTimeout(r, 500));
- return { data: undefined };
- },
- }),
- confirmDelivery: builder.mutation<
- { earnings: number },
- { jobId: string; rating: number }
- >({
- queryFn: async ({ jobId: _jobId }) => {
- await new Promise((r) => setTimeout(r, 800));
- return { data: { earnings: 78 } };
- },
- }),
- reportIssue: builder.mutation({
- queryFn: async (_args) => {
- await new Promise((r) => setTimeout(r, 500));
- return { data: undefined };
- },
- }),
- }),
-});
-
-export const {
- useAcceptJobMutation,
- useRejectJobMutation,
- useConfirmPickupMutation,
- useConfirmDeliveryMutation,
- useReportIssueMutation,
-} = jobApi;
diff --git a/app/store/commonReducers/auth/index.ts b/app/store/commonReducers/auth/index.ts
new file mode 100644
index 0000000..c4eb6e2
--- /dev/null
+++ b/app/store/commonReducers/auth/index.ts
@@ -0,0 +1,2 @@
+export * from './thunk';
+export * from './reducer';
diff --git a/app/store/commonReducers/auth/reducer.ts b/app/store/commonReducers/auth/reducer.ts
new file mode 100644
index 0000000..e802582
--- /dev/null
+++ b/app/store/commonReducers/auth/reducer.ts
@@ -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('auth/setRequestId');
+export const setOnboardingComplete = createAction('auth/setOnboardingComplete');
+export const setOnlineStatus = createAction('auth/setOnlineStatus');
+export const setProfile = createAction('auth/setProfile');
+export const setLocation = createAction('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'];
+ }
+ });
+});
diff --git a/app/store/commonReducers/auth/thunk.ts b/app/store/commonReducers/auth/thunk.ts
new file mode 100644
index 0000000..9133c8a
--- /dev/null
+++ b/app/store/commonReducers/auth/thunk.ts
@@ -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(
+ '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
+>('auth/updateProfile', async (updates, { rejectWithValue }) => {
+ try {
+ return await updateProfile(updates);
+ } catch (error: any) {
+ return rejectWithValue(error.message || 'Failed to update profile');
+ }
+});
diff --git a/app/store/commonReducers/earnings/index.ts b/app/store/commonReducers/earnings/index.ts
new file mode 100644
index 0000000..c4eb6e2
--- /dev/null
+++ b/app/store/commonReducers/earnings/index.ts
@@ -0,0 +1,2 @@
+export * from './thunk';
+export * from './reducer';
diff --git a/app/store/commonReducers/earnings/reducer.ts b/app/store/commonReducers/earnings/reducer.ts
new file mode 100644
index 0000000..8cb50bb
--- /dev/null
+++ b/app/store/commonReducers/earnings/reducer.ts
@@ -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('earnings/addEarning');
+export const setEarnings = createAction>('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;
+ });
+});
diff --git a/app/store/commonReducers/earnings/thunk.ts b/app/store/commonReducers/earnings/thunk.ts
new file mode 100644
index 0000000..fa7e543
--- /dev/null
+++ b/app/store/commonReducers/earnings/thunk.ts
@@ -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(
+ '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(
+ 'earnings/getWeeklyEarnings',
+ async (_, { rejectWithValue }) => {
+ try {
+ return await earningsApi.getWeeklyEarnings();
+ } catch (error: any) {
+ return rejectWithValue(error.message || 'Failed to get weekly earnings');
+ }
+ }
+);
diff --git a/app/store/commonReducers/index.ts b/app/store/commonReducers/index.ts
new file mode 100644
index 0000000..a95a718
--- /dev/null
+++ b/app/store/commonReducers/index.ts
@@ -0,0 +1,3 @@
+export * from './auth';
+export * from './earnings';
+export * from './job';
diff --git a/app/store/commonReducers/job/index.ts b/app/store/commonReducers/job/index.ts
new file mode 100644
index 0000000..c4eb6e2
--- /dev/null
+++ b/app/store/commonReducers/job/index.ts
@@ -0,0 +1,2 @@
+export * from './thunk';
+export * from './reducer';
diff --git a/app/store/commonReducers/job/reducer.ts b/app/store/commonReducers/job/reducer.ts
new file mode 100644
index 0000000..a431f6a
--- /dev/null
+++ b/app/store/commonReducers/job/reducer.ts
@@ -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('job/setNewJobOffer');
+export const acceptJobSync = createAction('job/acceptJobSync');
+export const rejectJobSync = createAction('job/rejectJobSync');
+export const advanceJobStatus = createAction('job/advanceJobStatus');
+export const completeJob = createAction('job/completeJob');
+export const clearJob = createAction('job/clearJob');
+export const setJobHistory = createAction('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;
+ });
+});
diff --git a/app/store/commonReducers/job/thunk.ts b/app/store/commonReducers/job/thunk.ts
new file mode 100644
index 0000000..d5ecba7
--- /dev/null
+++ b/app/store/commonReducers/job/thunk.ts
@@ -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/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(
+ '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(
+ '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(
+ 'job/reportIssue',
+ async ({ jobId, reason }, { rejectWithValue }) => {
+ try {
+ return await jobApi.reportIssue(jobId, reason);
+ } catch (error: any) {
+ return rejectWithValue(error.message || 'Failed to report issue');
+ }
+ }
+);
diff --git a/app/store/index.ts b/app/store/index.ts
index 6550061..d549549 100644
--- a/app/store/index.ts
+++ b/app/store/index.ts
@@ -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;
-export type AppDispatch = typeof store.dispatch;
-
-export const useAppDispatch = () => useDispatch();
-export const useAppSelector: TypedUseSelectorHook = useSelector;
+export * from './commonReducers';
+export * from './migration';
+export * from './rootReducer';
+export * from './store';
diff --git a/app/store/migration.ts b/app/store/migration.ts
new file mode 100644
index 0000000..f649179
--- /dev/null
+++ b/app/store/migration.ts
@@ -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];
diff --git a/app/store/rootReducer.ts b/app/store/rootReducer.ts
new file mode 100644
index 0000000..830bfb1
--- /dev/null
+++ b/app/store/rootReducer.ts
@@ -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;
+export default rootReducer;
diff --git a/app/store/slices/authSlice.ts b/app/store/slices/authSlice.ts
deleted file mode 100644
index 9f5b55c..0000000
--- a/app/store/slices/authSlice.ts
+++ /dev/null
@@ -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) => {
- state.requestId = action.payload;
- },
- setOnboardingComplete: (state) => {
- state.hasCompletedOnboarding = true;
- },
- setOnlineStatus: (state, action: PayloadAction) => {
- state.isOnline = action.payload;
- },
- setProfile: (state, action: PayloadAction) => {
- state.partnerProfile = action.payload;
- },
- setLocation: (state, action: PayloadAction) => {
- 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;
diff --git a/app/store/slices/earningsSlice.ts b/app/store/slices/earningsSlice.ts
deleted file mode 100644
index 8fb5c27..0000000
--- a/app/store/slices/earningsSlice.ts
+++ /dev/null
@@ -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) => {
- state.todayEarnings += action.payload;
- state.completedCount += 1;
- },
- setEarnings: (state, action: PayloadAction>) => {
- 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;
diff --git a/app/store/slices/jobSlice.ts b/app/store/slices/jobSlice.ts
deleted file mode 100644
index 0cd0595..0000000
--- a/app/store/slices/jobSlice.ts
+++ /dev/null
@@ -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) => {
- 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) => {
- 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) => {
- state.jobHistory = action.payload;
- },
- },
-});
-
-export const {
- setNewJobOffer,
- acceptJob,
- rejectJob,
- advanceJobStatus,
- completeJob,
- clearJob,
- setJobHistory,
-} = jobSlice.actions;
-
-export default jobSlice.reducer;
diff --git a/app/store/store.ts b/app/store/store.ts
new file mode 100644
index 0000000..e4065d9
--- /dev/null
+++ b/app/store/store.ts
@@ -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();
+export const useAppSelector: TypedUseSelectorHook = useSelector;
+
+export type AppThunk = ThunkAction>;
+
+export default store;
diff --git a/app/utils/constants.ts b/app/utils/constants.ts
index ba92bdd..15281db 100644
--- a/app/utils/constants.ts
+++ b/app/utils/constants.ts
@@ -29,6 +29,7 @@ export enum RouteNames {
DeliverOrder = 'DeliverOrder',
DeliveryCompleted = 'DeliveryCompleted',
Documents = 'Documents',
+ Kyc = 'Kyc',
}
export enum JobStatus {
diff --git a/babel.config.js b/babel.config.js
index f4e4886..e878cc6 100644
--- a/babel.config.js
+++ b/babel.config.js
@@ -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',
},
},
],
diff --git a/index.js b/index.js
index 3187351..06002ff 100644
--- a/index.js
+++ b/index.js
@@ -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);
diff --git a/package.json b/package.json
index 1e672f0..8d02d00 100644
--- a/package.json
+++ b/package.json
@@ -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",
diff --git a/tsconfig.json b/tsconfig.json
index ef63114..405df1a 100644
--- a/tsconfig.json
+++ b/tsconfig.json
@@ -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"],
diff --git a/yarn.lock b/yarn.lock
index 7c4e2ea..368c812 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -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==