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==