feat: implement core delivery partner features including authentication, KYC, navigation, and state management via Redux Toolkit
This commit is contained in:
parent
72f294d700
commit
a6c713ae8c
5
ReactotronConfig.js
Normal file
5
ReactotronConfig.js
Normal file
@ -0,0 +1,5 @@
|
||||
import Reactotron from 'reactotron-react-native';
|
||||
|
||||
Reactotron.configure() // controls connection & communication settings
|
||||
.useReactNative() // add all built-in react native plugins
|
||||
.connect(); // let's connect!
|
||||
@ -1,6 +1,8 @@
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
|
||||
<uses-permission android:name="android.permission.INTERNET" />
|
||||
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
|
||||
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" />
|
||||
|
||||
<application
|
||||
android:name=".MainApplication"
|
||||
@ -23,5 +25,8 @@
|
||||
<category android:name="android.intent.category.LAUNCHER" />
|
||||
</intent-filter>
|
||||
</activity>
|
||||
<meta-data
|
||||
android:name="com.google.android.geo.API_KEY"
|
||||
android:value="AIzaSyDKaKVyQCmSpHMXcMkNCccSXpILpuvwsVM" />
|
||||
</application>
|
||||
</manifest>
|
||||
|
||||
@ -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"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -5,18 +5,22 @@ import {
|
||||
} from 'react-native-safe-area-context';
|
||||
import RootNavigator from '@navigation/rootNavigator';
|
||||
import { Provider } from 'react-redux';
|
||||
import { store } from '@store';
|
||||
// import { store } from '@store';
|
||||
import { useAppTheme } from '@theme';
|
||||
import { persistor, store } from '@store';
|
||||
import { PersistGate } from 'redux-persist/integration/react';
|
||||
|
||||
function App() {
|
||||
const isDarkMode = useColorScheme() === 'dark';
|
||||
|
||||
return (
|
||||
<Provider store={store}>
|
||||
<PersistGate loading={null} persistor={persistor}>
|
||||
<SafeAreaProvider>
|
||||
<StatusBar barStyle={isDarkMode ? 'light-content' : 'dark-content'} />
|
||||
<AppContent />
|
||||
</SafeAreaProvider>
|
||||
</PersistGate>
|
||||
</Provider>
|
||||
);
|
||||
}
|
||||
|
||||
66
app/api/authApi.ts
Normal file
66
app/api/authApi.ts
Normal file
@ -0,0 +1,66 @@
|
||||
import { PartnerProfile } from '@app-types/index';
|
||||
import { LoginResponse, VerifyOtpResponse } from '@interfaces';
|
||||
import { apiClient } from '@services';
|
||||
// import apiClient from '../services/apiClient'; // Used when connecting to real backend
|
||||
|
||||
// Mock responses
|
||||
const mockSendOtpResponse = { requestId: 'req_mock_001' };
|
||||
const mockVerifyOtpResponse = {
|
||||
token: 'mock_token_xyz',
|
||||
partnerId: 'partner_001',
|
||||
};
|
||||
const mockProfile: PartnerProfile = {
|
||||
partnerId: 'partner_001',
|
||||
fullName: 'Rahul Kumar',
|
||||
email: 'rahul@example.com',
|
||||
phone: '9876543210',
|
||||
locationType: 'home',
|
||||
location: {
|
||||
coordinates: { latitude: 12.9716, longitude: 77.5946 },
|
||||
address: 'Koramangala 4th Block',
|
||||
city: 'Bengaluru',
|
||||
state: 'Karnataka',
|
||||
pincode: '560034',
|
||||
},
|
||||
avatarUri: null,
|
||||
rating: 4.8,
|
||||
totalDeliveries: 342,
|
||||
acceptanceRate: 95,
|
||||
completionRate: 98,
|
||||
isVerified: true,
|
||||
};
|
||||
|
||||
export const loginApi = async (phone: string) => {
|
||||
const result = await apiClient.post<LoginResponse>('/auth/otp/request', {
|
||||
phone,
|
||||
});
|
||||
return result;
|
||||
};
|
||||
|
||||
export const verifyOtpApi = async (
|
||||
phone: string,
|
||||
code: string,
|
||||
role: string,
|
||||
) => {
|
||||
return await apiClient.post<VerifyOtpResponse>('/auth/otp/verify', {
|
||||
phone,
|
||||
code,
|
||||
role,
|
||||
});
|
||||
};
|
||||
|
||||
export const getProfile = async (
|
||||
partnerId: string,
|
||||
): Promise<PartnerProfile> => {
|
||||
// return (await apiClient.get(`/profile/${partnerId}`)).data;
|
||||
await new Promise<void>(r => setTimeout(r, 500));
|
||||
return mockProfile;
|
||||
};
|
||||
|
||||
export const updateProfile = async (
|
||||
updates: Partial<PartnerProfile>,
|
||||
): Promise<PartnerProfile> => {
|
||||
// return (await apiClient.patch('/profile', updates)).data;
|
||||
await new Promise<void>(r => setTimeout(r, 800));
|
||||
return { ...mockProfile, ...updates };
|
||||
};
|
||||
12
app/api/dashboardApi.ts
Normal file
12
app/api/dashboardApi.ts
Normal file
@ -0,0 +1,12 @@
|
||||
import { ToggleStatusPayload, ToggleStatusResponse } from '@interfaces';
|
||||
import { apiClient } from '@services';
|
||||
|
||||
export const toggleStatus = async (
|
||||
payload: ToggleStatusPayload,
|
||||
): Promise<ToggleStatusResponse> => {
|
||||
const response = await apiClient.patch<ToggleStatusResponse>(
|
||||
'/delivery-partners/availability',
|
||||
payload,
|
||||
);
|
||||
return response;
|
||||
};
|
||||
15
app/api/earningsApi.ts
Normal file
15
app/api/earningsApi.ts
Normal file
@ -0,0 +1,15 @@
|
||||
import { DayEarning } from '@app-types/index';
|
||||
import { mockWeeklyEarnings, mockTodayEarnings } from '@store/mockData/mockEarnings';
|
||||
// import apiClient from '../services/apiClient'; // Used when connecting to real backend
|
||||
|
||||
export const getTodayEarnings = async (): Promise<typeof mockTodayEarnings> => {
|
||||
// return (await apiClient.get('/earnings/today')).data;
|
||||
await new Promise<void>((r) => setTimeout(r, 500));
|
||||
return mockTodayEarnings;
|
||||
};
|
||||
|
||||
export const getWeeklyEarnings = async (): Promise<DayEarning[]> => {
|
||||
// return (await apiClient.get('/earnings/weekly')).data;
|
||||
await new Promise<void>((r) => setTimeout(r, 500));
|
||||
return mockWeeklyEarnings;
|
||||
};
|
||||
4
app/api/index.ts
Normal file
4
app/api/index.ts
Normal file
@ -0,0 +1,4 @@
|
||||
export * from './authApi';
|
||||
export * from './onBoardApi';
|
||||
export * from './kycApi';
|
||||
export * from './dashboardApi';
|
||||
38
app/api/jobApi.ts
Normal file
38
app/api/jobApi.ts
Normal file
@ -0,0 +1,38 @@
|
||||
import { Job } from '@app-types/index';
|
||||
import { JobStatus } from '@utils/constants';
|
||||
import { mockJobOffer } from '@store/mockData/mockJobs';
|
||||
// import apiClient from '../services/apiClient'; // Used when connecting to real backend
|
||||
|
||||
export const acceptJob = async (jobId: string): Promise<Job> => {
|
||||
// return (await apiClient.post(`/jobs/${jobId}/accept`)).data;
|
||||
await new Promise<void>((r) => setTimeout(r, 500));
|
||||
return {
|
||||
...mockJobOffer,
|
||||
jobId,
|
||||
orderId: '#ORD125487',
|
||||
status: JobStatus.Accepted,
|
||||
acceptedAt: new Date().toISOString(),
|
||||
collectAmount: 0,
|
||||
};
|
||||
};
|
||||
|
||||
export const rejectJob = async (jobId: string): Promise<void> => {
|
||||
// await apiClient.post(`/jobs/${jobId}/reject`);
|
||||
await new Promise<void>((r) => setTimeout(r, 300));
|
||||
};
|
||||
|
||||
export const confirmPickup = async (jobId: string): Promise<void> => {
|
||||
// await apiClient.post(`/jobs/${jobId}/pickup`);
|
||||
await new Promise<void>((r) => setTimeout(r, 500));
|
||||
};
|
||||
|
||||
export const confirmDelivery = async (jobId: string, rating: number): Promise<{ earnings: number }> => {
|
||||
// return (await apiClient.post(`/jobs/${jobId}/deliver`, { rating })).data;
|
||||
await new Promise<void>((r) => setTimeout(r, 800));
|
||||
return { earnings: 78 };
|
||||
};
|
||||
|
||||
export const reportIssue = async (jobId: string, reason: string): Promise<void> => {
|
||||
// await apiClient.post(`/jobs/${jobId}/issue`, { reason });
|
||||
await new Promise<void>((r) => setTimeout(r, 500));
|
||||
};
|
||||
41
app/api/kycApi.ts
Normal file
41
app/api/kycApi.ts
Normal file
@ -0,0 +1,41 @@
|
||||
import { KycDocUploadResponse, MyKycResponse } from '@interfaces';
|
||||
import { apiClient } from '@services';
|
||||
|
||||
export interface KycUploadFile {
|
||||
uri: string;
|
||||
name: string;
|
||||
type: string;
|
||||
}
|
||||
|
||||
export interface KycUploadMetadata {
|
||||
docName: string;
|
||||
docNumber: string;
|
||||
}
|
||||
|
||||
export interface KycUploadPayload {
|
||||
files: KycUploadFile[];
|
||||
metadata: KycUploadMetadata[];
|
||||
}
|
||||
|
||||
export const uploadKycDocuments = async (
|
||||
payload: KycUploadPayload,
|
||||
): Promise<KycDocUploadResponse> => {
|
||||
const formData = new FormData();
|
||||
|
||||
payload.files.forEach(file => {
|
||||
// In React Native, FormData requires appending file objects with uri, name, and type properties
|
||||
formData.append('files', {
|
||||
uri: file.uri,
|
||||
name: file.name,
|
||||
type: file.type,
|
||||
} as any);
|
||||
});
|
||||
|
||||
formData.append('metadata', JSON.stringify(payload.metadata));
|
||||
|
||||
return await apiClient.post<KycDocUploadResponse>('/kyc/upload', formData);
|
||||
};
|
||||
|
||||
export const getMyKyc = async (): Promise<MyKycResponse> => {
|
||||
return await apiClient.get<MyKycResponse>('/kyc/my-details');
|
||||
};
|
||||
12
app/api/onBoardApi.ts
Normal file
12
app/api/onBoardApi.ts
Normal file
@ -0,0 +1,12 @@
|
||||
import { DashboardResponce, OnBoardPayload, UserProfile } from '@interfaces';
|
||||
import { apiClient } from '@services';
|
||||
|
||||
export const onBoardComplete = async (
|
||||
payload: OnBoardPayload,
|
||||
): Promise<UserProfile> => {
|
||||
return await apiClient.post<UserProfile>('/auth/onboard', payload);
|
||||
};
|
||||
|
||||
export const getDashboardApi = async (): Promise<DashboardResponce> => {
|
||||
return await apiClient.get<DashboardResponce>('/delivery-partners/dashboard');
|
||||
};
|
||||
@ -20,3 +20,4 @@ export * from './screenHeader/screenHeader';
|
||||
export * from './sectionHeader/sectionHeader';
|
||||
export * from './statusBadge/statusBadge';
|
||||
export * from './toast/toast';
|
||||
export * from './kycDocumentCard';
|
||||
|
||||
98
app/components/kycDocumentCard/KycDocumentCard.styles.ts
Normal file
98
app/components/kycDocumentCard/KycDocumentCard.styles.ts
Normal file
@ -0,0 +1,98 @@
|
||||
import { StyleSheet } from 'react-native';
|
||||
import { typography, spacing, borderRadius } from '@theme';
|
||||
|
||||
export const getStyles = (colors: any) =>
|
||||
StyleSheet.create({
|
||||
cardContainer: {
|
||||
backgroundColor: colors.cardBg,
|
||||
borderRadius: borderRadius.md,
|
||||
borderWidth: 1,
|
||||
borderColor: colors.border,
|
||||
padding: spacing.lg,
|
||||
marginBottom: spacing.xl,
|
||||
// Subtle elevation
|
||||
shadowColor: colors.cardShadow,
|
||||
shadowOffset: { width: 0, height: 2 },
|
||||
shadowOpacity: 0.1,
|
||||
shadowRadius: 4,
|
||||
elevation: 3,
|
||||
},
|
||||
headerRow: {
|
||||
flexDirection: 'row',
|
||||
justifyContent: 'space-between',
|
||||
alignItems: 'center',
|
||||
marginBottom: spacing.md,
|
||||
borderBottomWidth: 1,
|
||||
borderBottomColor: colors.border,
|
||||
paddingBottom: spacing.sm,
|
||||
},
|
||||
docNumberText: {
|
||||
fontSize: typography.fontSize.sm,
|
||||
fontWeight: typography.fontWeight.bold,
|
||||
color: colors.primary,
|
||||
textTransform: 'uppercase',
|
||||
letterSpacing: 0.5,
|
||||
},
|
||||
removeButton: {
|
||||
paddingVertical: spacing.xs,
|
||||
paddingHorizontal: spacing.sm,
|
||||
},
|
||||
removeButtonText: {
|
||||
fontSize: typography.fontSize.xs,
|
||||
color: colors.error,
|
||||
fontWeight: typography.fontWeight.semibold,
|
||||
},
|
||||
fieldContainer: {
|
||||
marginBottom: spacing.md,
|
||||
},
|
||||
label: {
|
||||
fontSize: 11,
|
||||
fontWeight: typography.fontWeight.bold,
|
||||
color: colors.textSecondary,
|
||||
textTransform: 'uppercase',
|
||||
marginBottom: spacing.xs,
|
||||
letterSpacing: 0.3,
|
||||
},
|
||||
input: {
|
||||
backgroundColor: colors.inputBg,
|
||||
borderWidth: 1,
|
||||
borderColor: colors.border,
|
||||
borderRadius: borderRadius.sm,
|
||||
paddingHorizontal: spacing.md,
|
||||
height: 48,
|
||||
color: colors.text,
|
||||
fontSize: typography.fontSize.sm,
|
||||
},
|
||||
uploadBox: {
|
||||
backgroundColor: colors.inputBg,
|
||||
borderWidth: 1,
|
||||
borderColor: colors.border,
|
||||
borderStyle: 'dashed',
|
||||
borderRadius: borderRadius.sm,
|
||||
paddingHorizontal: spacing.md,
|
||||
height: 48,
|
||||
flexDirection: 'row',
|
||||
justifyContent: 'space-between',
|
||||
alignItems: 'center',
|
||||
},
|
||||
uploadBoxActive: {
|
||||
borderColor: colors.primary,
|
||||
backgroundColor: colors.primaryLight,
|
||||
},
|
||||
uploadText: {
|
||||
fontSize: typography.fontSize.sm,
|
||||
color: colors.placeholder,
|
||||
fontWeight: typography.fontWeight.medium,
|
||||
},
|
||||
fileNameText: {
|
||||
fontSize: typography.fontSize.sm,
|
||||
color: colors.primary,
|
||||
fontWeight: typography.fontWeight.semibold,
|
||||
},
|
||||
helperText: {
|
||||
fontSize: 11,
|
||||
color: colors.textSecondary,
|
||||
marginTop: spacing.xs,
|
||||
fontStyle: 'italic',
|
||||
},
|
||||
});
|
||||
193
app/components/kycDocumentCard/KycDocumentCard.tsx
Normal file
193
app/components/kycDocumentCard/KycDocumentCard.tsx
Normal file
@ -0,0 +1,193 @@
|
||||
import React from 'react';
|
||||
import { View, Text, TextInput, TouchableOpacity, Alert } from 'react-native';
|
||||
import { useAppTheme } from '@theme';
|
||||
import Svg, { Path } from 'react-native-svg';
|
||||
import { pick, types, errorCodes } from '@react-native-documents/picker';
|
||||
import { getStyles } from './KycDocumentCard.styles';
|
||||
|
||||
export interface KycDocument {
|
||||
id: string;
|
||||
name: string;
|
||||
idNumber: string;
|
||||
fileName: string | null;
|
||||
fileUri?: string | null;
|
||||
fileType?: string | null;
|
||||
status?: string;
|
||||
}
|
||||
|
||||
interface KycDocumentCardProps {
|
||||
document: KycDocument;
|
||||
index: number;
|
||||
onUpdate?: (updatedFields: Partial<KycDocument>) => void;
|
||||
onRemove?: () => void;
|
||||
readOnly?: boolean;
|
||||
}
|
||||
|
||||
const UploadIcon = ({ size = 18, color = '#666' }) => (
|
||||
<Svg width={size} height={size} viewBox="0 0 24 24" fill="none">
|
||||
<Path
|
||||
d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4M17 8l-5-5-5 5M12 3v12"
|
||||
stroke={color}
|
||||
strokeWidth="2"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
/>
|
||||
</Svg>
|
||||
);
|
||||
|
||||
export const KycDocumentCard: React.FC<KycDocumentCardProps> = ({
|
||||
document,
|
||||
index,
|
||||
onUpdate,
|
||||
onRemove,
|
||||
readOnly = false,
|
||||
}) => {
|
||||
const { colors } = useAppTheme();
|
||||
const styles = getStyles(colors);
|
||||
|
||||
const handleSelectFile = async () => {
|
||||
if (readOnly) return;
|
||||
try {
|
||||
const [result] = await pick({
|
||||
type: [types.pdf, types.images],
|
||||
allowMultiSelection: false,
|
||||
});
|
||||
|
||||
onUpdate?.({
|
||||
fileName: result.name ?? `document_${Date.now()}`,
|
||||
fileUri: result.uri,
|
||||
fileType: result.type ?? 'application/octet-stream',
|
||||
});
|
||||
} catch (err: any) {
|
||||
if (err?.code === errorCodes.OPERATION_CANCELED) {
|
||||
// User dismissed the picker — no action needed
|
||||
return;
|
||||
}
|
||||
Alert.alert('File Picker Error', err?.message ?? 'Failed to open document picker.');
|
||||
}
|
||||
};
|
||||
|
||||
const renderStatusBadge = (status?: string) => {
|
||||
if (!status) return null;
|
||||
const normalized = status.toLowerCase();
|
||||
let bgColor = '#FFF9E6';
|
||||
let textColor = '#B27B00';
|
||||
let label = status;
|
||||
|
||||
if (normalized === 'verified' || normalized === 'approved') {
|
||||
bgColor = '#E6F4EA';
|
||||
textColor = '#137333';
|
||||
label = 'Verified';
|
||||
} else if (normalized === 'rejected' || normalized === 'failed') {
|
||||
bgColor = '#FCE8E6';
|
||||
textColor = '#C5221F';
|
||||
label = 'Rejected';
|
||||
} else if (normalized === 'pending' || normalized === 'submitted' || normalized === 'under_review') {
|
||||
bgColor = '#FFF9E6';
|
||||
textColor = '#B27B00';
|
||||
label = 'Pending';
|
||||
}
|
||||
|
||||
return (
|
||||
<View
|
||||
style={{
|
||||
backgroundColor: bgColor,
|
||||
paddingHorizontal: 8,
|
||||
paddingVertical: 4,
|
||||
borderRadius: 4,
|
||||
alignSelf: 'center',
|
||||
}}
|
||||
>
|
||||
<Text
|
||||
style={{
|
||||
color: textColor,
|
||||
fontSize: 10,
|
||||
fontWeight: 'bold',
|
||||
textTransform: 'uppercase',
|
||||
}}
|
||||
>
|
||||
{label}
|
||||
</Text>
|
||||
</View>
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<View style={styles.cardContainer}>
|
||||
<View style={styles.headerRow}>
|
||||
<Text style={styles.docNumberText}>Document {index + 1}</Text>
|
||||
{readOnly && renderStatusBadge(document.status)}
|
||||
{!readOnly && onRemove && (
|
||||
<TouchableOpacity
|
||||
style={styles.removeButton}
|
||||
onPress={onRemove}
|
||||
activeOpacity={0.7}
|
||||
>
|
||||
<Text style={styles.removeButtonText}>Remove</Text>
|
||||
</TouchableOpacity>
|
||||
)}
|
||||
</View>
|
||||
|
||||
{/* Document Name / Type */}
|
||||
<View style={styles.fieldContainer}>
|
||||
<Text style={styles.label}>Document Name / Type</Text>
|
||||
<TextInput
|
||||
style={styles.input}
|
||||
placeholder="e.g. National ID or Trade License"
|
||||
placeholderTextColor={colors.placeholder}
|
||||
value={document.name}
|
||||
onChangeText={(text) => onUpdate?.({ name: text })}
|
||||
autoCapitalize="sentences"
|
||||
editable={!readOnly}
|
||||
/>
|
||||
</View>
|
||||
|
||||
{/* Document Identification Number */}
|
||||
<View style={styles.fieldContainer}>
|
||||
<Text style={styles.label}>Document Identification Number</Text>
|
||||
<TextInput
|
||||
style={styles.input}
|
||||
placeholder="e.g. GSTIN/Aadhar/ID code"
|
||||
placeholderTextColor={colors.placeholder}
|
||||
value={document.idNumber}
|
||||
onChangeText={(text) => onUpdate?.({ idNumber: text })}
|
||||
autoCapitalize="characters"
|
||||
editable={!readOnly}
|
||||
/>
|
||||
</View>
|
||||
|
||||
{/* Document Attachment */}
|
||||
<View style={styles.fieldContainer}>
|
||||
<Text style={styles.label}>Document Attachment File (JPG, PNG, PDF)</Text>
|
||||
<TouchableOpacity
|
||||
style={[
|
||||
styles.uploadBox,
|
||||
document.fileName ? styles.uploadBoxActive : null,
|
||||
readOnly ? { borderStyle: 'solid', opacity: 0.8 } : null,
|
||||
]}
|
||||
onPress={handleSelectFile}
|
||||
activeOpacity={0.8}
|
||||
disabled={readOnly}
|
||||
>
|
||||
<Text
|
||||
style={[
|
||||
styles.uploadText,
|
||||
document.fileName ? styles.fileNameText : null,
|
||||
]}
|
||||
numberOfLines={1}
|
||||
>
|
||||
{document.fileName ? document.fileName : 'SELECT DOCUMENT FILE...'}
|
||||
</Text>
|
||||
{!readOnly && <UploadIcon color={document.fileName ? colors.primary : colors.textSecondary} />}
|
||||
</TouchableOpacity>
|
||||
{!readOnly && (
|
||||
<Text style={styles.helperText}>
|
||||
Supported: pdf, png, jpg, or jpeg under 200kb only.
|
||||
</Text>
|
||||
)}
|
||||
</View>
|
||||
</View>
|
||||
);
|
||||
};
|
||||
|
||||
export default KycDocumentCard;
|
||||
1
app/components/kycDocumentCard/index.ts
Normal file
1
app/components/kycDocumentCard/index.ts
Normal file
@ -0,0 +1 @@
|
||||
export * from './KycDocumentCard';
|
||||
@ -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);
|
||||
dispatch(loginWithPhone(mobileNumber))
|
||||
.unwrap()
|
||||
.then(() => {
|
||||
navigation.navigate(RouteNames.Otp, { phone: mobileNumber });
|
||||
}, 1500);
|
||||
})
|
||||
.catch((error: any) => {
|
||||
Alert.alert('Error', error.message);
|
||||
});
|
||||
};
|
||||
|
||||
// const handleForgotPassword = () => {
|
||||
@ -224,7 +235,7 @@ export const LoginScreen: React.FC = () => {
|
||||
<PrimaryButton
|
||||
title="Login"
|
||||
onPress={handleLogin}
|
||||
isLoading={isLoading}
|
||||
isLoading={loading}
|
||||
style={styles.loginButton}
|
||||
/>
|
||||
|
||||
|
||||
@ -2,27 +2,35 @@ import React, { useState, useEffect } from 'react';
|
||||
import { View, Text, ScrollView, TouchableOpacity, Alert } from 'react-native';
|
||||
import { OtpInput, NumericKeypad, PrimaryButton } from '@components';
|
||||
import { useAppTheme } from '@theme';
|
||||
import { useNavigation } from '@react-navigation/native';
|
||||
import { CompositeNavigationProp, useNavigation } from '@react-navigation/native';
|
||||
import { NativeStackNavigationProp } from '@react-navigation/native-stack';
|
||||
import { AuthStackParamList } from '@navigation/navigationTypes';
|
||||
import { AuthStackParamList, RootStackParamList } from '@navigation/navigationTypes';
|
||||
import { RouteNames } from '@utils/constants';
|
||||
import { getStyles } from './OtpScreen.styles';
|
||||
import { RootState, useAppDispatch, useAppSelector, verifyOtp } from '@store';
|
||||
|
||||
export const OtpScreen: React.FC<{ route: any }> = ({ route }) => {
|
||||
const { colors } = useAppTheme();
|
||||
const styles = getStyles(colors);
|
||||
const navigation = useNavigation<NativeStackNavigationProp<AuthStackParamList>>();
|
||||
const navigation = useNavigation<
|
||||
CompositeNavigationProp<
|
||||
NativeStackNavigationProp<AuthStackParamList, RouteNames.Otp>,
|
||||
NativeStackNavigationProp<RootStackParamList>
|
||||
>
|
||||
>();
|
||||
|
||||
const phone = route?.params?.phone || '+91 98765 43210';
|
||||
const [otp, setOtp] = useState('');
|
||||
const [timer, setTimer] = useState(30);
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
// const [isLoading, setIsLoading] = useState(false);
|
||||
const [error, setError] = useState(false);
|
||||
const dispatch = useAppDispatch();
|
||||
const { loading } = useAppSelector((state: RootState) => state.auth);
|
||||
|
||||
useEffect(() => {
|
||||
if (timer <= 0) return;
|
||||
const interval = setInterval(() => {
|
||||
setTimer((prev) => prev - 1);
|
||||
setTimer(prev => prev - 1);
|
||||
}, 1000);
|
||||
return () => clearInterval(interval);
|
||||
}, [timer]);
|
||||
@ -30,43 +38,46 @@ export const OtpScreen: React.FC<{ route: any }> = ({ route }) => {
|
||||
const handleKeyPress = (digit: string) => {
|
||||
if (otp.length < 6) {
|
||||
setError(false);
|
||||
setOtp((prev) => prev + digit);
|
||||
setOtp(prev => prev + digit);
|
||||
}
|
||||
};
|
||||
|
||||
const handleBackspace = () => {
|
||||
setOtp((prev) => prev.slice(0, -1));
|
||||
setOtp(prev => prev.slice(0, -1));
|
||||
};
|
||||
|
||||
const handleVerify = () => {
|
||||
const handleVerify = async () => {
|
||||
if (otp.length < 6) {
|
||||
setError(true);
|
||||
Alert.alert('Invalid OTP', 'Please enter a 6-digit verification code.');
|
||||
return;
|
||||
}
|
||||
|
||||
setIsLoading(true);
|
||||
setTimeout(() => {
|
||||
setIsLoading(false);
|
||||
if (otp === '123456' || otp.length === 6) {
|
||||
navigation.navigate(RouteNames.SetLocation);
|
||||
} else {
|
||||
setError(true);
|
||||
Alert.alert('Verification Failed', 'The code you entered is incorrect. Try 123456.');
|
||||
try {
|
||||
await dispatch(
|
||||
verifyOtp({ phone, code: otp, role: 'DELIVERY' }),
|
||||
).unwrap();
|
||||
navigation.navigate('OnBoarding', { screen: RouteNames.CompleteProfile });
|
||||
} catch (error) {
|
||||
Alert.alert('Error', error as string);
|
||||
}
|
||||
}, 1200);
|
||||
};
|
||||
|
||||
const handleResend = () => {
|
||||
if (timer > 0) return;
|
||||
setOtp('');
|
||||
setTimer(30);
|
||||
Alert.alert('OTP Resent', 'A new verification code has been sent to your mobile number.');
|
||||
Alert.alert(
|
||||
'OTP Resent',
|
||||
'A new verification code has been sent to your mobile number.',
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<View style={styles.container}>
|
||||
<ScrollView contentContainerStyle={styles.scrollContainer} bounces={false}>
|
||||
<ScrollView
|
||||
contentContainerStyle={styles.scrollContainer}
|
||||
bounces={false}
|
||||
>
|
||||
<View style={styles.headerContainer}>
|
||||
<Text style={styles.title}>Verify Phone</Text>
|
||||
<Text style={styles.subtitle}>
|
||||
@ -83,7 +94,10 @@ export const OtpScreen: React.FC<{ route: any }> = ({ route }) => {
|
||||
<View style={styles.resendContainer}>
|
||||
{timer > 0 ? (
|
||||
<Text style={styles.resendText}>
|
||||
Resend code in <Text style={{ color: colors.primary, fontWeight: 'bold' }}>00:{timer < 10 ? `0${timer}` : timer}</Text>
|
||||
Resend code in{' '}
|
||||
<Text style={{ color: colors.primary, fontWeight: 'bold' }}>
|
||||
00:{timer < 10 ? `0${timer}` : timer}
|
||||
</Text>
|
||||
</Text>
|
||||
) : (
|
||||
<TouchableOpacity onPress={handleResend} activeOpacity={0.7}>
|
||||
@ -95,7 +109,7 @@ export const OtpScreen: React.FC<{ route: any }> = ({ route }) => {
|
||||
<PrimaryButton
|
||||
title="Verify & Continue"
|
||||
onPress={handleVerify}
|
||||
isLoading={isLoading}
|
||||
isLoading={loading}
|
||||
style={styles.verifyButton}
|
||||
/>
|
||||
</View>
|
||||
@ -104,7 +118,7 @@ export const OtpScreen: React.FC<{ route: any }> = ({ route }) => {
|
||||
<NumericKeypad
|
||||
onPress={handleKeyPress}
|
||||
onBackspace={handleBackspace}
|
||||
disabled={isLoading}
|
||||
disabled={loading}
|
||||
/>
|
||||
</View>
|
||||
</ScrollView>
|
||||
|
||||
@ -1 +1 @@
|
||||
export { default } from "./arrivedAtCustomerScreen";
|
||||
export * from './arrivedAtCustomerScreen';
|
||||
|
||||
@ -1 +1 @@
|
||||
export { default } from "./arrivedAtStoreScreen";
|
||||
export * from './arrivedAtStoreScreen';
|
||||
|
||||
@ -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,
|
||||
},
|
||||
});
|
||||
|
||||
@ -1,63 +1,120 @@
|
||||
import React, { useState } from 'react';
|
||||
import { View, Text, ScrollView, TouchableOpacity, Alert } from 'react-native';
|
||||
import { useAppTheme } from '@theme';
|
||||
import { CustomInput, PrimaryButton } from '@components';
|
||||
import { ClipboardIcon, ChevronRightIcon, PersonIcon } from '@icons';
|
||||
import { CustomInput, PrimaryButton, ChipSelector } from '@components';
|
||||
import { PersonIcon } from '@icons';
|
||||
import { useNavigation } from '@react-navigation/native';
|
||||
import { NativeStackNavigationProp } from '@react-navigation/native-stack';
|
||||
import { AuthStackParamList } from '@navigation/navigationTypes';
|
||||
import { OnBoardingParamList } from '@navigation/navigationTypes';
|
||||
import { RouteNames } from '@utils/constants';
|
||||
import { Gender, VehicleType } from '@interfaces';
|
||||
import { getStyles } from './completeProfileScreen.styles';
|
||||
import { useAppDispatch, useAppSelector } from '@store';
|
||||
import { completeOnboard } from './thunk';
|
||||
|
||||
export const CompleteProfileScreen: React.FC = () => {
|
||||
const { colors } = useAppTheme();
|
||||
const styles = getStyles(colors);
|
||||
const navigation = useNavigation<NativeStackNavigationProp<AuthStackParamList>>();
|
||||
const navigation =
|
||||
useNavigation<NativeStackNavigationProp<OnBoardingParamList>>();
|
||||
|
||||
const [fullName, setFullName] = useState('Rahul Kumar');
|
||||
const [email, setEmail] = useState('rahul.kumar@gmail.com');
|
||||
const [vehicleNo, setVehicleNo] = useState('KA-01-MJ-8845');
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const [fullName, setFullName] = useState('');
|
||||
const [email, setEmail] = useState('');
|
||||
const [vehicleNo, setVehicleNo] = useState('');
|
||||
const [gender, setGender] = useState<Gender | ''>('');
|
||||
const [vehicleType, setVehicleType] = useState<VehicleType | ''>('');
|
||||
// const [isLoading, setIsLoading] = useState(false);
|
||||
const dispatch = useAppDispatch();
|
||||
const { isLoading } = useAppSelector(state => state.onboard);
|
||||
|
||||
const documents = [
|
||||
{ id: 'dl', title: 'Driving License', sub: 'Verified', status: 'verified' },
|
||||
{ id: 'aadhaar', title: 'Aadhaar Card', sub: 'Verified', status: 'verified' },
|
||||
{ id: 'rc', title: 'Vehicle RC', sub: 'Verified', status: 'verified' },
|
||||
{ id: 'insurance', title: 'Insurance Policy', sub: 'Expires on 12 May 2026', status: 'expiring' },
|
||||
];
|
||||
const genderOptions = Object.values(Gender).map(v => ({
|
||||
label: v,
|
||||
value: v,
|
||||
}));
|
||||
const vehicleTypeOptions = Object.values(VehicleType || '').map(v => ({
|
||||
label: v,
|
||||
value: v,
|
||||
}));
|
||||
|
||||
const handleDocumentPress = (docTitle: string) => {
|
||||
Alert.alert('Document Details', `Viewing or uploading ${docTitle}...`);
|
||||
};
|
||||
// const documents = [
|
||||
// { id: 'dl', title: 'Driving License', sub: 'Verified', status: 'verified' },
|
||||
// {
|
||||
// id: 'aadhaar',
|
||||
// title: 'Aadhaar Card',
|
||||
// sub: 'Verified',
|
||||
// status: 'verified',
|
||||
// },
|
||||
// { id: 'rc', title: 'Vehicle RC', sub: 'Verified', status: 'verified' },
|
||||
// {
|
||||
// id: 'insurance',
|
||||
// title: 'Insurance Policy',
|
||||
// sub: 'Expires on 12 May 2026',
|
||||
// status: 'expiring',
|
||||
// },
|
||||
// ];
|
||||
|
||||
// const handleDocumentPress = (docTitle: string) => {
|
||||
// Alert.alert('Document Details', `Viewing or uploading ${docTitle}...`);
|
||||
// };
|
||||
|
||||
const handleSubmit = () => {
|
||||
if (!fullName || !email || !vehicleNo) {
|
||||
Alert.alert('Required Fields', 'Please fill out all profile information.');
|
||||
if (!fullName || !email || !vehicleNo || !gender || !vehicleType) {
|
||||
Alert.alert(
|
||||
'Required Fields',
|
||||
'Please fill out all profile information including gender and vehicle type.',
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
setIsLoading(true);
|
||||
setTimeout(() => {
|
||||
setIsLoading(false);
|
||||
navigation.navigate(RouteNames.OnboardingComplete);
|
||||
}, 1500);
|
||||
dispatch(
|
||||
completeOnboard({
|
||||
name: fullName,
|
||||
email: email,
|
||||
vehicleNumber: vehicleNo,
|
||||
gender: gender,
|
||||
vehicleType: vehicleType,
|
||||
}),
|
||||
)
|
||||
.unwrap()
|
||||
.then(() => {
|
||||
navigation.navigate(RouteNames.Kyc);
|
||||
})
|
||||
.catch(error => {
|
||||
Alert.alert('Error', error.message);
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<View style={styles.container}>
|
||||
<ScrollView contentContainerStyle={styles.scrollContainer} showsVerticalScrollIndicator={false}>
|
||||
<ScrollView
|
||||
contentContainerStyle={styles.scrollContainer}
|
||||
showsVerticalScrollIndicator={false}
|
||||
>
|
||||
<View style={styles.headerContainer}>
|
||||
<Text style={styles.title}>Complete Profile</Text>
|
||||
<Text style={styles.subtitle}>Enter details & upload documents to get verified</Text>
|
||||
<Text style={styles.subtitle}>
|
||||
Enter details & upload documents to get verified
|
||||
</Text>
|
||||
</View>
|
||||
|
||||
{/* Profile Avatar Upload Mock */}
|
||||
<View style={styles.profileImageContainer}>
|
||||
<TouchableOpacity style={styles.avatarPlaceholder} activeOpacity={0.8}>
|
||||
<TouchableOpacity
|
||||
style={styles.avatarPlaceholder}
|
||||
activeOpacity={0.8}
|
||||
>
|
||||
<PersonIcon size={40} color={colors.textSecondary} />
|
||||
<Text style={styles.avatarText}>Add Photo</Text>
|
||||
<View style={styles.cameraBadge}>
|
||||
<Text style={{ color: colors.white, fontSize: 12, fontWeight: 'bold' }}>+</Text>
|
||||
<Text
|
||||
style={{
|
||||
color: colors.white,
|
||||
fontSize: 12,
|
||||
fontWeight: 'bold',
|
||||
}}
|
||||
>
|
||||
+
|
||||
</Text>
|
||||
</View>
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
@ -87,8 +144,28 @@ export const CompleteProfileScreen: React.FC = () => {
|
||||
/>
|
||||
</View>
|
||||
|
||||
{/* Required Documents Checklist */}
|
||||
{/* Gender Selection */}
|
||||
<View style={styles.section}>
|
||||
<Text style={styles.sectionTitle}>Gender</Text>
|
||||
<ChipSelector
|
||||
options={genderOptions}
|
||||
selected={gender}
|
||||
onChange={(val: Gender) => setGender(val)}
|
||||
/>
|
||||
</View>
|
||||
|
||||
{/* Vehicle Type Selection */}
|
||||
<View style={styles.section}>
|
||||
<Text style={styles.sectionTitle}>Vehicle Type</Text>
|
||||
<ChipSelector
|
||||
options={vehicleTypeOptions}
|
||||
selected={vehicleType}
|
||||
onChange={(val: VehicleType) => setVehicleType(val)}
|
||||
/>
|
||||
</View>
|
||||
|
||||
{/* Required Documents Checklist */}
|
||||
{/* <View style={styles.section}>
|
||||
<Text style={styles.sectionTitle}>Required Documents</Text>
|
||||
{documents.map((doc) => (
|
||||
<TouchableOpacity
|
||||
@ -135,7 +212,7 @@ export const CompleteProfileScreen: React.FC = () => {
|
||||
</View>
|
||||
</TouchableOpacity>
|
||||
))}
|
||||
</View>
|
||||
</View> */}
|
||||
|
||||
<PrimaryButton
|
||||
title="Submit & Verify"
|
||||
|
||||
@ -1 +1,3 @@
|
||||
export { default } from './completeProfileScreen';
|
||||
export * from './completeProfileScreen';
|
||||
export * from './reducer';
|
||||
export * from './thunk';
|
||||
|
||||
34
app/features/screens/completeProfileScreen/reducer.ts
Normal file
34
app/features/screens/completeProfileScreen/reducer.ts
Normal file
@ -0,0 +1,34 @@
|
||||
import { UserProfile } from '@interfaces';
|
||||
import { completeOnboard } from './thunk';
|
||||
import { createReducer } from '@reduxjs/toolkit';
|
||||
|
||||
export interface completeProfileState {
|
||||
isLoading: boolean;
|
||||
isSubmitting?: boolean;
|
||||
error: string | null;
|
||||
submitError: string | null;
|
||||
userProfile: UserProfile | null;
|
||||
}
|
||||
|
||||
const initialState: completeProfileState = {
|
||||
isLoading: false,
|
||||
error: null,
|
||||
submitError: null,
|
||||
userProfile: null,
|
||||
};
|
||||
|
||||
export const completeProfileReducer = createReducer(initialState, builder => {
|
||||
builder
|
||||
.addCase(completeOnboard.pending, state => {
|
||||
state.isLoading = true;
|
||||
state.submitError = null;
|
||||
})
|
||||
.addCase(completeOnboard.fulfilled, (state, action) => {
|
||||
state.isLoading = false;
|
||||
state.userProfile = action.payload;
|
||||
})
|
||||
.addCase(completeOnboard.rejected, (state, action) => {
|
||||
state.isLoading = false;
|
||||
state.submitError = action.payload as string;
|
||||
});
|
||||
});
|
||||
17
app/features/screens/completeProfileScreen/thunk.ts
Normal file
17
app/features/screens/completeProfileScreen/thunk.ts
Normal file
@ -0,0 +1,17 @@
|
||||
import { onBoardComplete } from '@api';
|
||||
import { OnBoardPayload } from '@interfaces';
|
||||
import { createAsyncThunk } from '@reduxjs/toolkit';
|
||||
|
||||
export const completeOnboard = createAsyncThunk(
|
||||
'preferences/completeOnboard',
|
||||
async (payload: OnBoardPayload, { rejectWithValue }) => {
|
||||
try {
|
||||
const response = await onBoardComplete(payload);
|
||||
return response;
|
||||
} catch (error: unknown) {
|
||||
const message =
|
||||
error instanceof Error ? error.message : 'Failed to complete onboard';
|
||||
return rejectWithValue(message);
|
||||
}
|
||||
},
|
||||
);
|
||||
@ -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';
|
||||
|
||||
@ -1 +1 @@
|
||||
export { default } from "./confirmPickupScreen";
|
||||
export * from './confirmPickupScreen';
|
||||
|
||||
@ -1,11 +1,18 @@
|
||||
import React, { useEffect, useRef } from 'react';
|
||||
import { View, Text, ScrollView, TouchableOpacity, Alert, Switch } from 'react-native';
|
||||
import {
|
||||
View,
|
||||
Text,
|
||||
ScrollView,
|
||||
TouchableOpacity,
|
||||
Alert,
|
||||
Switch,
|
||||
} from 'react-native';
|
||||
import { useAppTheme, borderRadius, spacing, shadow, typography } from '@theme';
|
||||
import { PrimaryButton } from '@components';
|
||||
import { BellIcon, PersonIcon, WalletIcon, ClipboardIcon } from '@icons';
|
||||
import { useAppDispatch, useAppSelector } from '@store';
|
||||
import { setOnlineStatus } from '@store/slices/authSlice';
|
||||
import { setNewJobOffer } from '@store/slices/jobSlice';
|
||||
import { setOnlineStatus } from '@store/commonReducers/auth';
|
||||
import { setNewJobOffer } from '@store/commonReducers/job';
|
||||
import { mockJobOffer } from '@store/mockData/mockJobs';
|
||||
import { useNavigation } from '@react-navigation/native';
|
||||
import { NativeStackNavigationProp } from '@react-navigation/native-stack';
|
||||
@ -13,18 +20,27 @@ import { AppStackParamList } from '@navigation/navigationTypes';
|
||||
import { RouteNames, JobStatus } from '@utils/constants';
|
||||
import { formatCurrency } from '@utils/formatters';
|
||||
import { getStyles } from './dashboardScreen.styles';
|
||||
import { toggleStatus } from './thunk';
|
||||
|
||||
const DashboardScreen: React.FC = () => {
|
||||
export const DashboardScreen: React.FC = () => {
|
||||
const { colors } = useAppTheme();
|
||||
const styles = getStyles(colors);
|
||||
const dispatch = useAppDispatch();
|
||||
const navigation = useNavigation<NativeStackNavigationProp<AppStackParamList>>();
|
||||
const navigation =
|
||||
useNavigation<NativeStackNavigationProp<AppStackParamList>>();
|
||||
|
||||
const isOnline = useAppSelector((state) => state.auth.isOnline);
|
||||
const { todayEarnings, completedCount, onlineMinutes, weeklyData } = useAppSelector(
|
||||
(state) => state.earnings
|
||||
// Derive online status from API response stored in dashboard slice.
|
||||
// Falls back to auth.isOnline during the initial load before any toggle API call.
|
||||
const { availabiltyStatus, loading: toggleLoading } = useAppSelector(
|
||||
state => state.dashboard,
|
||||
);
|
||||
const { jobStatus, activeJob } = useAppSelector((state) => state.job);
|
||||
const authIsOnline = useAppSelector(state => state.auth.isOnline);
|
||||
const isOnline = availabiltyStatus
|
||||
? availabiltyStatus.availabilityStatus === 'ONLINE'
|
||||
: authIsOnline;
|
||||
const { todayEarnings, completedCount, onlineMinutes, weeklyData } =
|
||||
useAppSelector(state => state.earnings);
|
||||
const { jobStatus, activeJob } = useAppSelector(state => state.job);
|
||||
|
||||
const timerRef = useRef<ReturnType<typeof setTimeout> | null>(null);
|
||||
|
||||
@ -50,7 +66,26 @@ const DashboardScreen: React.FC = () => {
|
||||
}, [isOnline, jobStatus, dispatch, navigation]);
|
||||
|
||||
const handleToggleOnline = () => {
|
||||
const newStatus = isOnline ? 'OFFLINE' : 'ONLINE';
|
||||
|
||||
// Optimistically update the auth flag so the timer and UI react immediately
|
||||
dispatch(setOnlineStatus(!isOnline));
|
||||
|
||||
dispatch(toggleStatus({ availabilityStatus: newStatus }))
|
||||
.unwrap()
|
||||
.then(response => {
|
||||
// Confirm state from server — setOnlineStatus stays in sync
|
||||
const isNowOnline = response.availabilityStatus === 'ONLINE';
|
||||
dispatch(setOnlineStatus(isNowOnline));
|
||||
})
|
||||
.catch(() => {
|
||||
// Revert the optimistic update on API failure
|
||||
dispatch(setOnlineStatus(isOnline));
|
||||
Alert.alert(
|
||||
'Status Update Failed',
|
||||
'Could not update your availability. Please try again.',
|
||||
);
|
||||
});
|
||||
};
|
||||
|
||||
const handleSimulateOffer = () => {
|
||||
@ -59,7 +94,10 @@ const DashboardScreen: React.FC = () => {
|
||||
return;
|
||||
}
|
||||
if (jobStatus !== JobStatus.Idle) {
|
||||
Alert.alert('Active Delivery', 'You already have an active job offer or order.');
|
||||
Alert.alert(
|
||||
'Active Delivery',
|
||||
'You already have an active job offer or order.',
|
||||
);
|
||||
return;
|
||||
}
|
||||
dispatch(setNewJobOffer(mockJobOffer));
|
||||
@ -95,15 +133,20 @@ const DashboardScreen: React.FC = () => {
|
||||
const formatOnlineTime = (mins: number) => {
|
||||
const hours = Math.floor(mins / 60);
|
||||
const remainingMins = mins % 60;
|
||||
return `${hours}h ${remainingMins < 10 ? `0${remainingMins}` : remainingMins}m`;
|
||||
return `${hours}h ${
|
||||
remainingMins < 10 ? `0${remainingMins}` : remainingMins
|
||||
}m`;
|
||||
};
|
||||
|
||||
// Helper to render chart bar heights dynamically based on amounts
|
||||
const maxWeeklyAmount = Math.max(...weeklyData.map((d) => d.amount), 1);
|
||||
const maxWeeklyAmount = Math.max(...weeklyData.map(d => d.amount), 1);
|
||||
|
||||
return (
|
||||
<View style={styles.container}>
|
||||
<ScrollView contentContainerStyle={styles.scrollContainer} showsVerticalScrollIndicator={false}>
|
||||
<ScrollView
|
||||
contentContainerStyle={styles.scrollContainer}
|
||||
showsVerticalScrollIndicator={false}
|
||||
>
|
||||
{/* Header Section */}
|
||||
<View style={styles.header}>
|
||||
<View>
|
||||
@ -115,13 +158,19 @@ const DashboardScreen: React.FC = () => {
|
||||
<View
|
||||
style={[
|
||||
styles.statusDot,
|
||||
{ backgroundColor: isOnline ? colors.statusOnline : colors.statusOffline },
|
||||
{
|
||||
backgroundColor: isOnline
|
||||
? colors.statusOnline
|
||||
: colors.statusOffline,
|
||||
},
|
||||
]}
|
||||
/>
|
||||
<Text
|
||||
style={[
|
||||
styles.statusText,
|
||||
{ color: isOnline ? colors.statusOnline : colors.statusOffline },
|
||||
{
|
||||
color: isOnline ? colors.statusOnline : colors.statusOffline,
|
||||
},
|
||||
]}
|
||||
>
|
||||
{isOnline ? 'Online' : 'Offline'}
|
||||
@ -131,7 +180,8 @@ const DashboardScreen: React.FC = () => {
|
||||
onValueChange={handleToggleOnline}
|
||||
trackColor={{ false: colors.border, true: colors.primaryLight }}
|
||||
thumbColor={isOnline ? colors.primary : colors.placeholder}
|
||||
style={{ marginLeft: 8 }}
|
||||
disabled={toggleLoading}
|
||||
style={{ marginLeft: 8, opacity: toggleLoading ? 0.5 : 1 }}
|
||||
/>
|
||||
</View>
|
||||
</View>
|
||||
@ -159,7 +209,9 @@ const DashboardScreen: React.FC = () => {
|
||||
<View style={styles.statDivider} />
|
||||
|
||||
<View style={styles.statBox}>
|
||||
<Text style={styles.statVal}>{isOnline ? completedCount : 0}</Text>
|
||||
<Text style={styles.statVal}>
|
||||
{isOnline ? completedCount : 0}
|
||||
</Text>
|
||||
<Text style={styles.statLabel}>Completed</Text>
|
||||
</View>
|
||||
|
||||
@ -182,7 +234,9 @@ const DashboardScreen: React.FC = () => {
|
||||
<Text style={styles.perfTitle}>Your Performance</Text>
|
||||
<View style={styles.perfRow}>
|
||||
<View style={styles.perfBox}>
|
||||
<Text style={[styles.perfVal, { color: colors.warning }]}>4.8 ★</Text>
|
||||
<Text style={[styles.perfVal, { color: colors.warning }]}>
|
||||
4.8 ★
|
||||
</Text>
|
||||
<Text style={styles.perfLabel}>Rating</Text>
|
||||
</View>
|
||||
<View style={styles.perfBox}>
|
||||
@ -202,7 +256,9 @@ const DashboardScreen: React.FC = () => {
|
||||
<View style={styles.chartRow}>
|
||||
{weeklyData.map((data, index) => {
|
||||
// Bar height percent
|
||||
const heightPercent = `${(data.amount / maxWeeklyAmount) * 85}%`;
|
||||
const heightPercent = `${
|
||||
(data.amount / maxWeeklyAmount) * 85
|
||||
}%`;
|
||||
return (
|
||||
<View key={index} style={styles.barContainer}>
|
||||
<Text style={styles.barVal}>₹{data.amount}</Text>
|
||||
@ -211,7 +267,10 @@ const DashboardScreen: React.FC = () => {
|
||||
styles.bar,
|
||||
{
|
||||
height: heightPercent as any,
|
||||
backgroundColor: index === weeklyData.length - 1 ? colors.primary : colors.placeholder,
|
||||
backgroundColor:
|
||||
index === weeklyData.length - 1
|
||||
? colors.primary
|
||||
: colors.placeholder,
|
||||
},
|
||||
]}
|
||||
/>
|
||||
@ -228,7 +287,9 @@ const DashboardScreen: React.FC = () => {
|
||||
activeOpacity={0.8}
|
||||
onPress={handleSimulateOffer}
|
||||
>
|
||||
<Text style={styles.simulateBtnText}>⚡ Simulate Immediate Job Offer</Text>
|
||||
<Text style={styles.simulateBtnText}>
|
||||
⚡ Simulate Immediate Job Offer
|
||||
</Text>
|
||||
</TouchableOpacity>
|
||||
|
||||
<PrimaryButton
|
||||
@ -239,9 +300,12 @@ const DashboardScreen: React.FC = () => {
|
||||
</>
|
||||
) : (
|
||||
<View style={styles.promptCard}>
|
||||
<Text style={styles.promptTitle}>Go Online to start receiving delivery requests</Text>
|
||||
<Text style={styles.promptTitle}>
|
||||
Go Online to start receiving delivery requests
|
||||
</Text>
|
||||
<Text style={styles.promptDesc}>
|
||||
Make sure you are in a high-demand delivery zone to maximize your earnings.
|
||||
Make sure you are in a high-demand delivery zone to maximize your
|
||||
earnings.
|
||||
</Text>
|
||||
<PrimaryButton
|
||||
title="Go Online"
|
||||
@ -257,7 +321,12 @@ const DashboardScreen: React.FC = () => {
|
||||
<View style={styles.helpRow}>
|
||||
<TouchableOpacity
|
||||
style={styles.helpItem}
|
||||
onPress={() => Alert.alert('Emergency SOS', 'Calling nearest security/emergency dispatch...')}
|
||||
onPress={() =>
|
||||
Alert.alert(
|
||||
'Emergency SOS',
|
||||
'Calling nearest security/emergency dispatch...',
|
||||
)
|
||||
}
|
||||
>
|
||||
<BellIcon size={24} color={colors.error} />
|
||||
<Text style={styles.helpLabel}>Emergency SOS</Text>
|
||||
@ -266,7 +335,11 @@ const DashboardScreen: React.FC = () => {
|
||||
|
||||
<TouchableOpacity
|
||||
style={styles.helpItem}
|
||||
onPress={() => navigation.navigate(RouteNames.BottomTabs, { screen: RouteNames.Profile })}
|
||||
onPress={() =>
|
||||
navigation.navigate(RouteNames.BottomTabs, {
|
||||
screen: RouteNames.Profile,
|
||||
})
|
||||
}
|
||||
>
|
||||
<ClipboardIcon size={24} color={colors.primary} />
|
||||
<Text style={styles.helpLabel}>FAQ Center</Text>
|
||||
@ -275,7 +348,12 @@ const DashboardScreen: React.FC = () => {
|
||||
|
||||
<TouchableOpacity
|
||||
style={styles.helpItem}
|
||||
onPress={() => Alert.alert('Support Chat', 'Initiating live helpdesk agent chat...')}
|
||||
onPress={() =>
|
||||
Alert.alert(
|
||||
'Support Chat',
|
||||
'Initiating live helpdesk agent chat...',
|
||||
)
|
||||
}
|
||||
>
|
||||
<PersonIcon size={24} color={colors.statusOnDelivery} />
|
||||
<Text style={styles.helpLabel}>Live Support</Text>
|
||||
@ -286,7 +364,9 @@ const DashboardScreen: React.FC = () => {
|
||||
</ScrollView>
|
||||
|
||||
{/* Active Job Floating Bottom Banner */}
|
||||
{jobStatus !== JobStatus.Idle && jobStatus !== JobStatus.NewRequest && activeJob && (
|
||||
{jobStatus !== JobStatus.Idle &&
|
||||
jobStatus !== JobStatus.NewRequest &&
|
||||
activeJob && (
|
||||
<TouchableOpacity
|
||||
style={{
|
||||
position: 'absolute',
|
||||
@ -305,23 +385,57 @@ const DashboardScreen: React.FC = () => {
|
||||
activeOpacity={0.9}
|
||||
onPress={handleActiveJobBannerPress}
|
||||
>
|
||||
<View style={{ flexDirection: 'row', alignItems: 'center', flex: 1 }}>
|
||||
<View
|
||||
style={{ flexDirection: 'row', alignItems: 'center', flex: 1 }}
|
||||
>
|
||||
<ClipboardIcon size={20} color={colors.white} />
|
||||
<View style={{ marginLeft: spacing.sm, flex: 1 }}>
|
||||
<Text style={{ color: colors.white, fontWeight: 'bold', fontSize: typography.fontSize.sm }}>
|
||||
<Text
|
||||
style={{
|
||||
color: colors.white,
|
||||
fontWeight: 'bold',
|
||||
fontSize: typography.fontSize.sm,
|
||||
}}
|
||||
>
|
||||
Order {activeJob.orderId}
|
||||
</Text>
|
||||
<Text style={{ color: colors.primaryLight, fontSize: typography.fontSize.xs }} numberOfLines={1}>
|
||||
{jobStatus === JobStatus.Accepted && 'Heading to pickup store...'}
|
||||
{jobStatus === JobStatus.ArrivedAtStore && 'At pickup store, check items...'}
|
||||
{jobStatus === JobStatus.PickedUp && 'Order picked up, routing to customer...'}
|
||||
{jobStatus === JobStatus.EnRoute && 'On the way to customer...'}
|
||||
{jobStatus === JobStatus.ArrivedAtCustomer && 'Arrived at customer location...'}
|
||||
<Text
|
||||
style={{
|
||||
color: colors.primaryLight,
|
||||
fontSize: typography.fontSize.xs,
|
||||
}}
|
||||
numberOfLines={1}
|
||||
>
|
||||
{jobStatus === JobStatus.Accepted &&
|
||||
'Heading to pickup store...'}
|
||||
{jobStatus === JobStatus.ArrivedAtStore &&
|
||||
'At pickup store, check items...'}
|
||||
{jobStatus === JobStatus.PickedUp &&
|
||||
'Order picked up, routing to customer...'}
|
||||
{jobStatus === JobStatus.EnRoute &&
|
||||
'On the way to customer...'}
|
||||
{jobStatus === JobStatus.ArrivedAtCustomer &&
|
||||
'Arrived at customer location...'}
|
||||
</Text>
|
||||
</View>
|
||||
</View>
|
||||
<View style={{ backgroundColor: colors.white, paddingHorizontal: spacing.md, paddingVertical: 6, borderRadius: borderRadius.sm }}>
|
||||
<Text style={{ color: colors.primary, fontWeight: 'bold', fontSize: typography.fontSize.xs }}>View</Text>
|
||||
<View
|
||||
style={{
|
||||
backgroundColor: colors.white,
|
||||
paddingHorizontal: spacing.md,
|
||||
paddingVertical: 6,
|
||||
borderRadius: borderRadius.sm,
|
||||
}}
|
||||
>
|
||||
<Text
|
||||
style={{
|
||||
color: colors.primary,
|
||||
fontWeight: 'bold',
|
||||
fontSize: typography.fontSize.xs,
|
||||
}}
|
||||
>
|
||||
View
|
||||
</Text>
|
||||
</View>
|
||||
</TouchableOpacity>
|
||||
)}
|
||||
@ -329,4 +443,4 @@ const DashboardScreen: React.FC = () => {
|
||||
);
|
||||
};
|
||||
|
||||
export default DashboardScreen;
|
||||
// export default DashboardScreen;
|
||||
|
||||
@ -1 +1,3 @@
|
||||
export { default } from './dashboardScreen';
|
||||
export * from './dashboardScreen';
|
||||
export * from './reducer';
|
||||
export * from './thunk';
|
||||
|
||||
31
app/features/screens/dashboardScreen/reducer.ts
Normal file
31
app/features/screens/dashboardScreen/reducer.ts
Normal file
@ -0,0 +1,31 @@
|
||||
import { ToggleStatusResponse } from '@interfaces';
|
||||
import { createReducer } from '@reduxjs/toolkit';
|
||||
import { toggleStatus } from './thunk';
|
||||
|
||||
export interface ToggleState {
|
||||
loading: boolean;
|
||||
error: string | null;
|
||||
availabiltyStatus: ToggleStatusResponse | null;
|
||||
}
|
||||
|
||||
export const initialState: ToggleState = {
|
||||
loading: false,
|
||||
error: null,
|
||||
availabiltyStatus: null,
|
||||
};
|
||||
|
||||
export const dashBoardReducer = createReducer(initialState, builder => {
|
||||
builder
|
||||
.addCase(toggleStatus.pending, state => {
|
||||
state.loading = true;
|
||||
state.error = null;
|
||||
})
|
||||
.addCase(toggleStatus.fulfilled, (state, action) => {
|
||||
state.loading = false;
|
||||
state.availabiltyStatus = action.payload;
|
||||
})
|
||||
.addCase(toggleStatus.rejected, (state, action) => {
|
||||
state.loading = false;
|
||||
state.error = action.payload as string;
|
||||
});
|
||||
});
|
||||
15
app/features/screens/dashboardScreen/thunk.ts
Normal file
15
app/features/screens/dashboardScreen/thunk.ts
Normal file
@ -0,0 +1,15 @@
|
||||
import { createAsyncThunk } from '@reduxjs/toolkit';
|
||||
import { toggleStatus as toggleStatusApi } from '@api';
|
||||
import { ToggleStatusPayload } from '@interfaces';
|
||||
|
||||
export const toggleStatus = createAsyncThunk(
|
||||
'dashboard/toggleStatus',
|
||||
async (payload: ToggleStatusPayload, { rejectWithValue }) => {
|
||||
try {
|
||||
const response = await toggleStatusApi(payload);
|
||||
return response;
|
||||
} catch (error: unknown) {
|
||||
return rejectWithValue(error);
|
||||
}
|
||||
},
|
||||
);
|
||||
@ -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';
|
||||
|
||||
@ -1 +1 @@
|
||||
export { default } from "./deliverOrderScreen";
|
||||
export * from './deliverOrderScreen';
|
||||
|
||||
@ -1 +1 @@
|
||||
export { default } from "./deliveryCompletedScreen";
|
||||
export * from './deliveryCompletedScreen';
|
||||
|
||||
1
app/features/screens/documentsScreen/index.ts
Normal file
1
app/features/screens/documentsScreen/index.ts
Normal file
@ -0,0 +1 @@
|
||||
export * from './documentsScreen';
|
||||
@ -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';
|
||||
|
||||
3
app/features/screens/kycScreen/index.ts
Normal file
3
app/features/screens/kycScreen/index.ts
Normal file
@ -0,0 +1,3 @@
|
||||
export * from './kycScreen';
|
||||
export * from './reducer';
|
||||
export * from './thunk';
|
||||
130
app/features/screens/kycScreen/kycScreen.styles.ts
Normal file
130
app/features/screens/kycScreen/kycScreen.styles.ts
Normal file
@ -0,0 +1,130 @@
|
||||
import { StyleSheet } from 'react-native';
|
||||
import { typography, spacing, borderRadius } from '@theme';
|
||||
|
||||
export const getStyles = (colors: any) =>
|
||||
StyleSheet.create({
|
||||
container: {
|
||||
flex: 1,
|
||||
backgroundColor: colors.background,
|
||||
},
|
||||
scrollContainer: {
|
||||
paddingHorizontal: spacing.lg,
|
||||
paddingTop: spacing.md,
|
||||
paddingBottom: spacing.xxxl,
|
||||
},
|
||||
// Top Compliance banner card
|
||||
bannerCard: {
|
||||
flexDirection: 'row',
|
||||
backgroundColor: colors.primaryLight,
|
||||
borderWidth: 1,
|
||||
borderColor: colors.border,
|
||||
borderRadius: borderRadius.md,
|
||||
padding: spacing.md,
|
||||
marginBottom: spacing.lg,
|
||||
alignItems: 'center',
|
||||
},
|
||||
bannerIconWrapper: {
|
||||
width: 44,
|
||||
height: 44,
|
||||
borderRadius: 22,
|
||||
backgroundColor: colors.white,
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
marginRight: spacing.md,
|
||||
shadowColor: '#000',
|
||||
shadowOffset: { width: 0, height: 1 },
|
||||
shadowOpacity: 0.05,
|
||||
shadowRadius: 2,
|
||||
elevation: 1,
|
||||
},
|
||||
bannerTextContainer: {
|
||||
flex: 1,
|
||||
},
|
||||
bannerTitle: {
|
||||
fontSize: 12,
|
||||
fontWeight: typography.fontWeight.bold,
|
||||
color: colors.text,
|
||||
letterSpacing: 0.5,
|
||||
textTransform: 'uppercase',
|
||||
},
|
||||
bannerSubtitle: {
|
||||
fontSize: typography.fontSize.xs,
|
||||
color: colors.textSecondary,
|
||||
marginTop: 2,
|
||||
lineHeight: 16,
|
||||
},
|
||||
// Main Card
|
||||
mainCard: {
|
||||
backgroundColor: colors.cardBg,
|
||||
borderRadius: borderRadius.md,
|
||||
borderWidth: 1,
|
||||
borderColor: colors.border,
|
||||
padding: spacing.md,
|
||||
marginBottom: spacing.lg,
|
||||
},
|
||||
mainCardHeader: {
|
||||
marginBottom: spacing.md,
|
||||
},
|
||||
mainCardTitle: {
|
||||
fontSize: typography.fontSize.md,
|
||||
fontWeight: typography.fontWeight.bold,
|
||||
color: colors.text,
|
||||
},
|
||||
mainCardSubtitle: {
|
||||
fontSize: typography.fontSize.xs,
|
||||
color: colors.textSecondary,
|
||||
marginTop: 2,
|
||||
},
|
||||
divider: {
|
||||
height: 1,
|
||||
backgroundColor: colors.border,
|
||||
marginVertical: spacing.md,
|
||||
},
|
||||
// Add Document Button
|
||||
addButtonContainer: {
|
||||
alignItems: 'flex-start',
|
||||
marginBottom: spacing.xl,
|
||||
},
|
||||
addBtn: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
borderWidth: 1,
|
||||
borderColor: colors.border,
|
||||
borderRadius: borderRadius.sm,
|
||||
paddingVertical: spacing.sm,
|
||||
paddingHorizontal: spacing.md,
|
||||
backgroundColor: colors.inputBg,
|
||||
},
|
||||
addBtnText: {
|
||||
fontSize: typography.fontSize.sm,
|
||||
fontWeight: typography.fontWeight.semibold,
|
||||
color: colors.text,
|
||||
marginLeft: spacing.xs,
|
||||
},
|
||||
// Submit Button
|
||||
submitBtn: {
|
||||
flexDirection: 'row',
|
||||
backgroundColor: colors.primary,
|
||||
borderRadius: borderRadius.sm,
|
||||
height: 48,
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
marginTop: spacing.sm,
|
||||
shadowColor: colors.primary,
|
||||
shadowOffset: { width: 0, height: 2 },
|
||||
shadowOpacity: 0.2,
|
||||
shadowRadius: 4,
|
||||
elevation: 4,
|
||||
},
|
||||
submitBtnDisabled: {
|
||||
backgroundColor: colors.border,
|
||||
shadowOpacity: 0,
|
||||
elevation: 0,
|
||||
},
|
||||
submitBtnText: {
|
||||
fontSize: typography.fontSize.md,
|
||||
fontWeight: typography.fontWeight.bold,
|
||||
color: colors.white,
|
||||
marginLeft: spacing.sm,
|
||||
},
|
||||
});
|
||||
355
app/features/screens/kycScreen/kycScreen.tsx
Normal file
355
app/features/screens/kycScreen/kycScreen.tsx
Normal file
@ -0,0 +1,355 @@
|
||||
import React, { useState } from 'react';
|
||||
import {
|
||||
View,
|
||||
Text,
|
||||
ScrollView,
|
||||
TouchableOpacity,
|
||||
Alert,
|
||||
ActivityIndicator,
|
||||
} from 'react-native';
|
||||
import { useAppTheme } from '@theme';
|
||||
import { ScreenHeader, KycDocumentCard, LoadingOverlay } from '@components';
|
||||
import { useNavigation, useFocusEffect } from '@react-navigation/native';
|
||||
import { NativeStackNavigationProp } from '@react-navigation/native-stack';
|
||||
import { OnBoardingParamList } from '@navigation/navigationTypes';
|
||||
import { RouteNames } from '@utils/constants';
|
||||
import Svg, { Path } from 'react-native-svg';
|
||||
import { useAppDispatch, useAppSelector } from '@store';
|
||||
import { uploadKyc, fetchMyKyc } from './thunk';
|
||||
import { getStyles } from './kycScreen.styles';
|
||||
|
||||
// Interface for Document state matching KycDocumentCard structure
|
||||
interface KycDocument {
|
||||
id: string;
|
||||
name: string;
|
||||
idNumber: string;
|
||||
fileName: string | null;
|
||||
fileUri: string | null;
|
||||
fileType: string | null;
|
||||
status?: string;
|
||||
}
|
||||
|
||||
// Inline Cloud Upload SVG representation for the top banner card
|
||||
const CloudUploadIcon = ({ size = 22, color = '#666' }) => (
|
||||
<Svg width={size} height={size} viewBox="0 0 24 24" fill="none">
|
||||
<Path
|
||||
d="M17.5 19A5.5 5.5 0 0 0 18 8h-1.26A8 8 0 1 0 3 16.28M12 12v9m0-9l-3 3m3-3l3 3"
|
||||
stroke={color}
|
||||
strokeWidth="2"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
/>
|
||||
</Svg>
|
||||
);
|
||||
|
||||
// Inline Plus icon for adding a new document
|
||||
const PlusIcon = ({ size = 16, color = '#333' }) => (
|
||||
<Svg width={size} height={size} viewBox="0 0 24 24" fill="none">
|
||||
<Path
|
||||
d="M12 5v14M5 12h14"
|
||||
stroke={color}
|
||||
strokeWidth="2.5"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
/>
|
||||
</Svg>
|
||||
);
|
||||
|
||||
// Inline Submit/Disk icon for the submit button
|
||||
const SubmitIcon = ({ size = 18, color = '#fff' }) => (
|
||||
<Svg width={size} height={size} viewBox="0 0 24 24" fill="none">
|
||||
<Path
|
||||
d="M19 21H5a2 2 0 0 1-2-2v-5a2 2 0 0 1 2-2h11l5 5v11a2 2 0 0 1-2 2z"
|
||||
stroke={color}
|
||||
strokeWidth="2"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
/>
|
||||
<Path
|
||||
d="M17 21v-8H7v8M7 3v5h8"
|
||||
stroke={color}
|
||||
strokeWidth="2"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
/>
|
||||
</Svg>
|
||||
);
|
||||
|
||||
export const KycScreen: React.FC = () => {
|
||||
const { colors } = useAppTheme();
|
||||
const styles = getStyles(colors);
|
||||
const navigation =
|
||||
useNavigation<NativeStackNavigationProp<OnBoardingParamList>>();
|
||||
const dispatch = useAppDispatch();
|
||||
|
||||
// Get upload loading status from KYC store slice
|
||||
const { isLoading, myKycResponse } = useAppSelector(state => state.kyc);
|
||||
|
||||
const [documents, setDocuments] = useState<KycDocument[]>([
|
||||
{
|
||||
id: '1',
|
||||
name: '',
|
||||
idNumber: '',
|
||||
fileName: null,
|
||||
fileUri: null,
|
||||
fileType: null,
|
||||
},
|
||||
]);
|
||||
|
||||
// Fetch current KYC verification status when screen is focused
|
||||
useFocusEffect(
|
||||
React.useCallback(() => {
|
||||
dispatch(fetchMyKyc());
|
||||
}, [dispatch]),
|
||||
);
|
||||
|
||||
const handleAddDocument = () => {
|
||||
setDocuments([
|
||||
...documents,
|
||||
{
|
||||
id: Date.now().toString(),
|
||||
name: '',
|
||||
idNumber: '',
|
||||
fileName: null,
|
||||
fileUri: null,
|
||||
fileType: null,
|
||||
},
|
||||
]);
|
||||
};
|
||||
|
||||
const handleRemoveDocument = (id: string) => {
|
||||
setDocuments(documents.filter(doc => doc.id !== id));
|
||||
};
|
||||
|
||||
const handleUpdateDocument = (
|
||||
id: string,
|
||||
updatedFields: Partial<KycDocument>,
|
||||
) => {
|
||||
setDocuments(
|
||||
documents.map(doc =>
|
||||
doc.id === id ? { ...doc, ...updatedFields } : doc,
|
||||
),
|
||||
);
|
||||
};
|
||||
|
||||
const handleSubmit = () => {
|
||||
// Validation
|
||||
for (let i = 0; i < documents.length; i++) {
|
||||
const doc = documents[i];
|
||||
if (!doc.name.trim()) {
|
||||
Alert.alert(
|
||||
'Required Field',
|
||||
`Please fill in the Document Name/Type for Document ${i + 1}.`,
|
||||
);
|
||||
return;
|
||||
}
|
||||
if (!doc.idNumber.trim()) {
|
||||
Alert.alert(
|
||||
'Required Field',
|
||||
`Please fill in the Document Identification Number for Document ${
|
||||
i + 1
|
||||
}.`,
|
||||
);
|
||||
return;
|
||||
}
|
||||
if (!doc.fileName || !doc.fileUri) {
|
||||
Alert.alert(
|
||||
'Required Field',
|
||||
`Please select and attach a document file for Document ${i + 1}.`,
|
||||
);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// Build files and metadata array payloads
|
||||
const payloadFiles = documents.map(doc => ({
|
||||
uri: doc.fileUri || '',
|
||||
name: doc.fileName || '',
|
||||
type: doc.fileName?.endsWith('.pdf') ? 'application/pdf' : 'image/png',
|
||||
}));
|
||||
|
||||
const payloadMetadata = documents.map(doc => ({
|
||||
docName: doc.name,
|
||||
docNumber: doc.idNumber,
|
||||
}));
|
||||
|
||||
dispatch(
|
||||
uploadKyc({
|
||||
files: payloadFiles,
|
||||
metadata: payloadMetadata,
|
||||
}),
|
||||
)
|
||||
.unwrap()
|
||||
.then(() => {
|
||||
Alert.alert('Success', 'KYC Documents submitted successfully!', [
|
||||
{
|
||||
text: 'OK',
|
||||
onPress: () => {
|
||||
dispatch(fetchMyKyc());
|
||||
navigation.navigate(RouteNames.OnboardingComplete);
|
||||
},
|
||||
},
|
||||
]);
|
||||
})
|
||||
.catch(err => {
|
||||
Alert.alert(
|
||||
'Upload Failed',
|
||||
err || 'Failed to submit KYC documents. Please try again.',
|
||||
);
|
||||
});
|
||||
};
|
||||
|
||||
// Determine if documents exist to render in read-only mode
|
||||
const hasSubmittedDocs = !!(
|
||||
myKycResponse &&
|
||||
myKycResponse.documents &&
|
||||
myKycResponse.documents.length > 0
|
||||
);
|
||||
|
||||
// Map backend documents if submitted, else use local state
|
||||
const displayedDocuments: KycDocument[] = hasSubmittedDocs
|
||||
? myKycResponse.documents.map(doc => ({
|
||||
id: doc.id,
|
||||
name: doc.documentType,
|
||||
idNumber: doc.documentNumber,
|
||||
fileName: doc.fileName,
|
||||
fileUri: doc.fileUrl,
|
||||
fileType: doc.fileName.endsWith('.pdf')
|
||||
? 'application/pdf'
|
||||
: 'image/png',
|
||||
status: doc.status,
|
||||
}))
|
||||
: documents;
|
||||
|
||||
return (
|
||||
<View style={styles.container}>
|
||||
<ScreenHeader
|
||||
title="KYC Verification"
|
||||
showBack={true}
|
||||
onBack={() => navigation.goBack()}
|
||||
/>
|
||||
|
||||
<ScrollView
|
||||
contentContainerStyle={styles.scrollContainer}
|
||||
showsVerticalScrollIndicator={false}
|
||||
>
|
||||
{/* Compliance Check Required Banner */}
|
||||
<View style={styles.bannerCard}>
|
||||
<View style={styles.bannerIconWrapper}>
|
||||
<CloudUploadIcon size={22} color={colors.primary} />
|
||||
</View>
|
||||
<View style={styles.bannerTextContainer}>
|
||||
<Text style={styles.bannerTitle}>
|
||||
KYC Compliance Check Required
|
||||
</Text>
|
||||
<Text style={styles.bannerSubtitle}>
|
||||
Please upload store identity documents to configure payouts,
|
||||
deliveries, and operational scopes.
|
||||
</Text>
|
||||
</View>
|
||||
</View>
|
||||
|
||||
{/* Upload KYC verification documents main card header info */}
|
||||
<View style={styles.mainCardHeader}>
|
||||
<Text style={styles.mainCardTitle}>
|
||||
{hasSubmittedDocs
|
||||
? 'Submitted KYC Documents'
|
||||
: 'Upload KYC Verification Documents'}
|
||||
</Text>
|
||||
<Text style={styles.mainCardSubtitle}>
|
||||
{hasSubmittedDocs
|
||||
? 'Below are the KYC documents submitted for verification.'
|
||||
: 'Provide details and attachments to initiate compliance verification.'}
|
||||
</Text>
|
||||
<View style={styles.divider} />
|
||||
</View>
|
||||
|
||||
{/* Render each document card */}
|
||||
{displayedDocuments.map((doc, idx) => (
|
||||
<KycDocumentCard
|
||||
key={doc.id}
|
||||
document={doc}
|
||||
index={idx}
|
||||
onUpdate={
|
||||
!hasSubmittedDocs
|
||||
? fields => handleUpdateDocument(doc.id, fields)
|
||||
: undefined
|
||||
}
|
||||
onRemove={
|
||||
!hasSubmittedDocs && idx > 0
|
||||
? () => handleRemoveDocument(doc.id)
|
||||
: undefined
|
||||
}
|
||||
readOnly={hasSubmittedDocs}
|
||||
/>
|
||||
))}
|
||||
|
||||
{!hasSubmittedDocs && (
|
||||
<>
|
||||
{/* Add another document button */}
|
||||
<View style={styles.addButtonContainer}>
|
||||
<TouchableOpacity
|
||||
style={styles.addBtn}
|
||||
onPress={handleAddDocument}
|
||||
activeOpacity={0.7}
|
||||
>
|
||||
<PlusIcon size={14} color={colors.text} />
|
||||
<Text style={styles.addBtnText}>Add Another Document</Text>
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
|
||||
{/* Upload & Submit KYC Action Button */}
|
||||
<TouchableOpacity
|
||||
style={[
|
||||
styles.submitBtn,
|
||||
isLoading ? styles.submitBtnDisabled : null,
|
||||
]}
|
||||
onPress={handleSubmit}
|
||||
disabled={isLoading}
|
||||
activeOpacity={0.8}
|
||||
>
|
||||
{isLoading ? (
|
||||
<ActivityIndicator color={colors.white} />
|
||||
) : (
|
||||
<>
|
||||
<SubmitIcon size={18} color={colors.white} />
|
||||
<Text style={styles.submitBtnText}>Upload & Submit KYC</Text>
|
||||
</>
|
||||
)}
|
||||
</TouchableOpacity>
|
||||
</>
|
||||
)}
|
||||
{hasSubmittedDocs && (
|
||||
<TouchableOpacity
|
||||
style={[
|
||||
styles.submitBtn,
|
||||
isLoading ? styles.submitBtnDisabled : null,
|
||||
]}
|
||||
onPress={() => {
|
||||
navigation.navigate(RouteNames.OnboardingComplete);
|
||||
}}
|
||||
disabled={isLoading}
|
||||
activeOpacity={0.8}
|
||||
>
|
||||
{isLoading ? (
|
||||
<ActivityIndicator color={colors.white} />
|
||||
) : (
|
||||
<>
|
||||
<SubmitIcon size={18} color={colors.white} />
|
||||
<Text style={styles.submitBtnText}>Next Page</Text>
|
||||
</>
|
||||
)}
|
||||
</TouchableOpacity>
|
||||
)}
|
||||
</ScrollView>
|
||||
|
||||
{/* Loading Overlay for the focus fetch check */}
|
||||
<LoadingOverlay
|
||||
visible={isLoading && !myKycResponse}
|
||||
message="Checking KYC status..."
|
||||
/>
|
||||
</View>
|
||||
);
|
||||
};
|
||||
|
||||
export default KycScreen;
|
||||
48
app/features/screens/kycScreen/reducer.ts
Normal file
48
app/features/screens/kycScreen/reducer.ts
Normal file
@ -0,0 +1,48 @@
|
||||
import { createReducer } from '@reduxjs/toolkit';
|
||||
import { KycDocUploadResponse } from '@interfaces';
|
||||
import { fetchMyKyc, uploadKyc } from './thunk';
|
||||
import { MyKycResponse } from '@interfaces';
|
||||
|
||||
export interface KycState {
|
||||
isLoading: boolean;
|
||||
error: string | null;
|
||||
uploadResponse: KycDocUploadResponse | null;
|
||||
myKycResponse: MyKycResponse | null;
|
||||
}
|
||||
|
||||
const initialState: KycState = {
|
||||
isLoading: false,
|
||||
error: null,
|
||||
uploadResponse: null,
|
||||
myKycResponse: null,
|
||||
};
|
||||
|
||||
export const kycReducer = createReducer(initialState, builder => {
|
||||
builder
|
||||
.addCase(uploadKyc.pending, state => {
|
||||
state.isLoading = true;
|
||||
state.error = null;
|
||||
})
|
||||
.addCase(uploadKyc.fulfilled, (state, action) => {
|
||||
state.isLoading = false;
|
||||
state.uploadResponse = action.payload;
|
||||
})
|
||||
.addCase(uploadKyc.rejected, (state, action) => {
|
||||
state.isLoading = false;
|
||||
state.error = action.payload as string;
|
||||
});
|
||||
|
||||
builder
|
||||
.addCase(fetchMyKyc.pending, state => {
|
||||
state.isLoading = true;
|
||||
state.error = null;
|
||||
})
|
||||
.addCase(fetchMyKyc.fulfilled, (state, action) => {
|
||||
state.isLoading = false;
|
||||
state.myKycResponse = action.payload;
|
||||
})
|
||||
.addCase(fetchMyKyc.rejected, (state, action) => {
|
||||
state.isLoading = false;
|
||||
state.error = action.payload as string;
|
||||
});
|
||||
});
|
||||
34
app/features/screens/kycScreen/thunk.ts
Normal file
34
app/features/screens/kycScreen/thunk.ts
Normal file
@ -0,0 +1,34 @@
|
||||
import { uploadKycDocuments, KycUploadPayload, getMyKyc } from '@api';
|
||||
import { createAsyncThunk } from '@reduxjs/toolkit';
|
||||
|
||||
export const uploadKyc = createAsyncThunk(
|
||||
'kyc/uploadKyc',
|
||||
async (payload: KycUploadPayload, { rejectWithValue }) => {
|
||||
try {
|
||||
const response = await uploadKycDocuments(payload);
|
||||
return response;
|
||||
} catch (error: unknown) {
|
||||
const message =
|
||||
error instanceof Error
|
||||
? error.message
|
||||
: 'Failed to upload KYC documents';
|
||||
return rejectWithValue(message);
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
export const fetchMyKyc = createAsyncThunk(
|
||||
'kyc/fetchMyKyc',
|
||||
async (_, { rejectWithValue }) => {
|
||||
try {
|
||||
const response = await getMyKyc();
|
||||
return response;
|
||||
} catch (error: unknown) {
|
||||
const message =
|
||||
error instanceof Error
|
||||
? error.message
|
||||
: 'Failed to fetch KYC documents';
|
||||
return rejectWithValue(message);
|
||||
}
|
||||
},
|
||||
);
|
||||
@ -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';
|
||||
|
||||
@ -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 });
|
||||
};
|
||||
|
||||
|
||||
@ -1 +1,3 @@
|
||||
export { default } from './onboardingCompleteScreen';
|
||||
export * from './onboardingCompleteScreen';
|
||||
export * from './thunk';
|
||||
export * from './reducer';
|
||||
|
||||
@ -1,18 +1,30 @@
|
||||
import React from 'react';
|
||||
import React, { useCallback } from 'react';
|
||||
import { View, Text } from 'react-native';
|
||||
import Svg, { Circle, Path } from 'react-native-svg';
|
||||
import { useAppTheme } from '@theme';
|
||||
import { PrimaryButton } from '@components';
|
||||
import { useNavigation } from '@react-navigation/native';
|
||||
import { useFocusEffect, useNavigation } from '@react-navigation/native';
|
||||
import { NativeStackNavigationProp } from '@react-navigation/native-stack';
|
||||
import { RootStackParamList } from '@navigation/navigationTypes';
|
||||
import { OnBoardingParamList } from '@navigation/navigationTypes';
|
||||
import { RouteNames } from '@utils/constants';
|
||||
import { getStyles } from './onboardingCompleteScreen.styles';
|
||||
import { getDashboard } from './thunk';
|
||||
import { useAppDispatch, useAppSelector } from '@store';
|
||||
|
||||
const OnboardingCompleteScreen: React.FC = () => {
|
||||
export const OnboardingCompleteScreen: React.FC = () => {
|
||||
const { colors } = useAppTheme();
|
||||
const navigation =
|
||||
useNavigation<NativeStackNavigationProp<OnBoardingParamList>>();
|
||||
const styles = getStyles(colors);
|
||||
const navigation = useNavigation<NativeStackNavigationProp<RootStackParamList>>();
|
||||
const dispatch = useAppDispatch();
|
||||
|
||||
useFocusEffect(
|
||||
useCallback(() => {
|
||||
dispatch(getDashboard());
|
||||
}, [dispatch]),
|
||||
);
|
||||
|
||||
const { isKycApproved } = useAppSelector(state => state.onboardingComplete);
|
||||
|
||||
return (
|
||||
<View style={styles.container}>
|
||||
@ -34,21 +46,23 @@ const OnboardingCompleteScreen: React.FC = () => {
|
||||
|
||||
<Text style={styles.successTitle}>Onboarding Completed!</Text>
|
||||
<Text style={styles.successDescription}>
|
||||
Welcome to the SG Delivery family! Your account has been successfully verified. You are now ready to go online and start earning.
|
||||
Welcome to the SG Delivery family! Your account has been successfully
|
||||
verified. You are now ready to go online and start earning.
|
||||
</Text>
|
||||
|
||||
<PrimaryButton
|
||||
title="Get Started"
|
||||
onPress={() =>
|
||||
navigation.navigate('App', {
|
||||
screen: RouteNames.BottomTabs,
|
||||
params: { screen: RouteNames.Dashboard },
|
||||
})
|
||||
onPress={() => {
|
||||
// If KYC is not yet approved, send user to KYC upload screen.
|
||||
// If KYC is approved, the auth reducer has already updated
|
||||
// user.status to ACTIVE via getDashboard.fulfilled, so the
|
||||
// rootNavigator will automatically swap to AppStack.
|
||||
if (!isKycApproved) {
|
||||
navigation.navigate(RouteNames.Kyc);
|
||||
}
|
||||
}}
|
||||
style={styles.startButton}
|
||||
/>
|
||||
</View>
|
||||
);
|
||||
};
|
||||
|
||||
export default OnboardingCompleteScreen;
|
||||
|
||||
38
app/features/screens/onboardingCompleteScreen/reducer.ts
Normal file
38
app/features/screens/onboardingCompleteScreen/reducer.ts
Normal file
@ -0,0 +1,38 @@
|
||||
import { DashboardResponce } from '@interfaces';
|
||||
import { createReducer, createSlice } from '@reduxjs/toolkit';
|
||||
import { getDashboard } from './thunk';
|
||||
|
||||
export interface OnBoardingCompleteState {
|
||||
dashboardResponce: DashboardResponce | null;
|
||||
isKycApproved: boolean | null;
|
||||
isLoading: boolean;
|
||||
error: string | null;
|
||||
}
|
||||
|
||||
const initialState: OnBoardingCompleteState = {
|
||||
dashboardResponce: null,
|
||||
isKycApproved: null,
|
||||
isLoading: false,
|
||||
error: null,
|
||||
};
|
||||
|
||||
export const onBoardingCompleteReducer = createReducer(
|
||||
initialState,
|
||||
builder => {
|
||||
builder
|
||||
.addCase(getDashboard.pending, state => {
|
||||
state.isLoading = true;
|
||||
state.error = null;
|
||||
})
|
||||
.addCase(getDashboard.fulfilled, (state, action) => {
|
||||
state.isLoading = false;
|
||||
state.isKycApproved = true;
|
||||
state.dashboardResponce = action.payload;
|
||||
})
|
||||
.addCase(getDashboard.rejected, (state, action) => {
|
||||
state.isLoading = false;
|
||||
state.isKycApproved = false;
|
||||
state.error = (action.payload as string) || 'Failed to get dashboard';
|
||||
});
|
||||
},
|
||||
);
|
||||
16
app/features/screens/onboardingCompleteScreen/thunk.ts
Normal file
16
app/features/screens/onboardingCompleteScreen/thunk.ts
Normal file
@ -0,0 +1,16 @@
|
||||
import { getDashboardApi } from '@api';
|
||||
import { createAsyncThunk } from '@reduxjs/toolkit';
|
||||
|
||||
export const getDashboard = createAsyncThunk(
|
||||
'onboardingComplete/getDashboard',
|
||||
async (_, { rejectWithValue }) => {
|
||||
try {
|
||||
const response = await getDashboardApi();
|
||||
return response;
|
||||
} catch (error: unknown) {
|
||||
const message =
|
||||
error instanceof Error ? error.message : 'Failed to get dashboard';
|
||||
return rejectWithValue(message);
|
||||
}
|
||||
},
|
||||
);
|
||||
@ -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';
|
||||
|
||||
@ -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';
|
||||
|
||||
@ -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';
|
||||
|
||||
@ -1 +1,2 @@
|
||||
export { default } from "./setLocationScreen";
|
||||
export * from './setLocationScreen';
|
||||
export * from './reducer';
|
||||
|
||||
43
app/features/screens/setLocationScreen/reducer.ts
Normal file
43
app/features/screens/setLocationScreen/reducer.ts
Normal file
@ -0,0 +1,43 @@
|
||||
import { createReducer, createAction } from '@reduxjs/toolkit';
|
||||
|
||||
// ─── Actions ──────────────────────────────────────────────────────────────────
|
||||
|
||||
export const setLocationData = createAction<{
|
||||
latitude: number;
|
||||
longitude: number;
|
||||
mapAddress: string;
|
||||
}>('setLocation/setLocationData');
|
||||
|
||||
export const clearLocationData = createAction('setLocation/clearLocationData');
|
||||
|
||||
// ─── State ────────────────────────────────────────────────────────────────────
|
||||
|
||||
export interface SetLocationState {
|
||||
latitude: number | null;
|
||||
longitude: number | null;
|
||||
mapAddress: string;
|
||||
}
|
||||
|
||||
const initialState: SetLocationState = {
|
||||
latitude: null,
|
||||
longitude: null,
|
||||
mapAddress: '',
|
||||
};
|
||||
|
||||
// ─── Reducer ──────────────────────────────────────────────────────────────────
|
||||
|
||||
const setLocationReducer = createReducer(initialState, builder => {
|
||||
builder
|
||||
.addCase(setLocationData, (state, action) => {
|
||||
state.latitude = action.payload.latitude;
|
||||
state.longitude = action.payload.longitude;
|
||||
state.mapAddress = action.payload.mapAddress;
|
||||
})
|
||||
.addCase(clearLocationData, state => {
|
||||
state.latitude = null;
|
||||
state.longitude = null;
|
||||
state.mapAddress = '';
|
||||
});
|
||||
});
|
||||
|
||||
export default setLocationReducer;
|
||||
@ -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,
|
||||
},
|
||||
mapText: {
|
||||
fontSize: typography.fontSize.lg,
|
||||
color: colors.textSecondary,
|
||||
fontWeight: typography.fontWeight.semibold,
|
||||
marginTop: spacing.md,
|
||||
android: { elevation: 5 },
|
||||
}),
|
||||
},
|
||||
mapSubText: {
|
||||
fontSize: typography.fontSize.sm,
|
||||
color: colors.placeholder,
|
||||
marginTop: spacing.xs,
|
||||
locateIcon: {
|
||||
fontSize: 22,
|
||||
},
|
||||
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,
|
||||
},
|
||||
pulseCircle: {
|
||||
position: 'absolute',
|
||||
width: 80,
|
||||
height: 80,
|
||||
borderRadius: 40,
|
||||
backgroundColor: colors.primary,
|
||||
opacity: 0.15,
|
||||
android: { elevation: 12 },
|
||||
}),
|
||||
},
|
||||
bottomCard: {
|
||||
position: 'absolute',
|
||||
bottom: spacing.xxl,
|
||||
left: spacing.lg,
|
||||
right: spacing.lg,
|
||||
sheetHandle: {
|
||||
alignSelf: 'center',
|
||||
width: 40,
|
||||
height: 4,
|
||||
borderRadius: 2,
|
||||
backgroundColor: colors.border,
|
||||
marginTop: 10,
|
||||
marginBottom: 16,
|
||||
},
|
||||
|
||||
// ---------- 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,
|
||||
},
|
||||
});
|
||||
|
||||
@ -1,57 +1,180 @@
|
||||
import React from 'react';
|
||||
import { View, Text } from 'react-native';
|
||||
import { useAppTheme } from '@theme';
|
||||
import { PrimaryButton } from '@components';
|
||||
import { LocationPinIcon } from '@icons';
|
||||
import React, { useState, useEffect, useRef, useCallback } from 'react';
|
||||
import {
|
||||
View,
|
||||
Text,
|
||||
TouchableOpacity,
|
||||
ActivityIndicator,
|
||||
InteractionManager,
|
||||
} from 'react-native';
|
||||
import MapView, { Marker, Region } from 'react-native-maps';
|
||||
import { useNavigation } from '@react-navigation/native';
|
||||
import { NativeStackNavigationProp } from '@react-navigation/native-stack';
|
||||
import { AuthStackParamList } from '@navigation/navigationTypes';
|
||||
import { RouteNames } from '@utils/constants';
|
||||
import { StackNavigationProp } from '@react-navigation/stack';
|
||||
import { getStyles } from './setLocationScreen.styles';
|
||||
import { PrimaryButton } from '@components';
|
||||
import { useAppTheme } from '@theme';
|
||||
|
||||
const SetLocationScreen: React.FC = () => {
|
||||
import { useAppDispatch } from '@store';
|
||||
import { setLocationData } from './reducer';
|
||||
import { OnBoardingParamList } from '@navigation/navigationTypes';
|
||||
import { RouteNames } from '@utils/constants';
|
||||
import {
|
||||
DEFAULT_LOCATION,
|
||||
getCurrentLocationWithAddress,
|
||||
reverseGeocode,
|
||||
showPermissionDeniedAlert,
|
||||
LatLng,
|
||||
} from '@services/locationService';
|
||||
|
||||
type NavProp = StackNavigationProp<OnBoardingParamList, RouteNames.SetLocation>;
|
||||
|
||||
const DELTA = { latitudeDelta: 0.006, longitudeDelta: 0.006 };
|
||||
|
||||
export const SetLocationScreen: React.FC = () => {
|
||||
const { colors } = useAppTheme();
|
||||
const styles = getStyles(colors);
|
||||
const navigation = useNavigation<NativeStackNavigationProp<AuthStackParamList>>();
|
||||
const navigation = useNavigation<NavProp>();
|
||||
const dispatch = useAppDispatch();
|
||||
const mapRef = useRef<MapView>(null);
|
||||
// Guard: never call setState after the component has unmounted
|
||||
const isMounted = useRef(true);
|
||||
|
||||
const [coords, setCoords] = useState<LatLng>(DEFAULT_LOCATION);
|
||||
const [address, setAddress] = useState('Fetching your location…');
|
||||
const [loading, setLoading] = useState(true);
|
||||
|
||||
// ------------------------------------------------------------------
|
||||
// Fetch current location on mount
|
||||
// ------------------------------------------------------------------
|
||||
const fetchCurrentLocation = useCallback(async () => {
|
||||
if (!isMounted.current) return;
|
||||
setLoading(true);
|
||||
try {
|
||||
console.log('--------------');
|
||||
const result = await getCurrentLocationWithAddress();
|
||||
console.log('result', result);
|
||||
if (!isMounted.current) return;
|
||||
setCoords(result.coords);
|
||||
setAddress(result.address);
|
||||
|
||||
// Animate the map to the user's location
|
||||
mapRef.current?.animateToRegion({ ...result.coords, ...DELTA }, 600);
|
||||
} catch (err: unknown) {
|
||||
const message = err instanceof Error ? err.message : '';
|
||||
if (message === 'Location permission denied') {
|
||||
// Show the alert here — safe, outside the Promise chain
|
||||
showPermissionDeniedAlert();
|
||||
}
|
||||
console.log('--------error------', err);
|
||||
if (!isMounted.current) return;
|
||||
setAddress('Could not determine location');
|
||||
} finally {
|
||||
if (isMounted.current) setLoading(false);
|
||||
}
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
// Wait for the screen-transition animation to fully finish before
|
||||
// calling PermissionsAndroid.request(). Calling it while the Activity
|
||||
// is still transitioning causes a native crash on Android.
|
||||
const task = InteractionManager.runAfterInteractions(() => {
|
||||
fetchCurrentLocation();
|
||||
});
|
||||
return () => {
|
||||
task.cancel();
|
||||
isMounted.current = false;
|
||||
};
|
||||
}, [fetchCurrentLocation]);
|
||||
|
||||
// ------------------------------------------------------------------
|
||||
// When user drags the map, update the marker + address
|
||||
// ------------------------------------------------------------------
|
||||
const handleRegionChangeComplete = useCallback(async (region: Region) => {
|
||||
const newCoords: LatLng = {
|
||||
latitude: region.latitude,
|
||||
longitude: region.longitude,
|
||||
};
|
||||
setCoords(newCoords);
|
||||
setAddress('Fetching address…');
|
||||
|
||||
const newAddress = await reverseGeocode(newCoords);
|
||||
setAddress(newAddress);
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<View style={styles.container}>
|
||||
{/* Blank Map Placeholder Area */}
|
||||
<View style={styles.mapContainer}>
|
||||
<View style={styles.placeholderMap}>
|
||||
{/* Pulsing marker simulation */}
|
||||
<View style={styles.pulseCircle} />
|
||||
<LocationPinIcon size={40} color={colors.primary} />
|
||||
{/* ---------- Map ---------- */}
|
||||
<MapView
|
||||
ref={mapRef}
|
||||
style={styles.map}
|
||||
initialRegion={{ ...DEFAULT_LOCATION, ...DELTA }}
|
||||
onRegionChangeComplete={handleRegionChangeComplete}
|
||||
>
|
||||
<Marker
|
||||
coordinate={coords}
|
||||
title="Your Location"
|
||||
description={address}
|
||||
/>
|
||||
</MapView>
|
||||
|
||||
<Text style={styles.mapText}>Select Location</Text>
|
||||
<Text style={styles.mapSubText}>Drag the map to pinpoint your location</Text>
|
||||
</View>
|
||||
</View>
|
||||
{/* ---------- "Locate me" floating button ---------- */}
|
||||
<TouchableOpacity
|
||||
style={styles.locateButton}
|
||||
activeOpacity={0.8}
|
||||
onPress={fetchCurrentLocation}
|
||||
>
|
||||
<Text style={styles.locateIcon}>📍</Text>
|
||||
</TouchableOpacity>
|
||||
|
||||
{/* Bottom Sheet Location Selector Card */}
|
||||
<View style={styles.bottomCard}>
|
||||
{/* ---------- Bottom sheet ---------- */}
|
||||
<View style={styles.bottomSheet}>
|
||||
<View style={styles.sheetHandle} />
|
||||
|
||||
<View style={styles.card}>
|
||||
<View style={styles.cardHeader}>
|
||||
<View style={styles.pinIconContainer}>
|
||||
<LocationPinIcon size={20} color={colors.primary} />
|
||||
</View>
|
||||
<Text style={styles.cardTitle}>Choose Delivery Area</Text>
|
||||
<Text style={styles.cardIcon}>📍</Text>
|
||||
<Text style={styles.cardTitle}>My Location</Text>
|
||||
</View>
|
||||
|
||||
<View style={styles.addressContainer}>
|
||||
<Text style={styles.addressLabel}>Current Address</Text>
|
||||
<Text style={styles.addressText}>
|
||||
MG Road, Ashok Nagar, Bengaluru, Karnataka 560001
|
||||
{loading ? (
|
||||
<ActivityIndicator
|
||||
size="small"
|
||||
color={colors.primary}
|
||||
style={{ marginVertical: 12 }}
|
||||
/>
|
||||
) : (
|
||||
<Text style={styles.cardAddress} numberOfLines={2}>
|
||||
{address}
|
||||
</Text>
|
||||
)}
|
||||
|
||||
<TouchableOpacity style={styles.changeLink} activeOpacity={0.7}>
|
||||
<Text style={styles.changeText}>Change</Text>
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
|
||||
<PrimaryButton
|
||||
title="Confirm Location"
|
||||
onPress={() => navigation.navigate(RouteNames.CompleteProfile)}
|
||||
onPress={() => {
|
||||
dispatch(
|
||||
setLocationData({
|
||||
latitude: coords.latitude,
|
||||
longitude: coords.longitude,
|
||||
mapAddress: address,
|
||||
}),
|
||||
);
|
||||
navigation.navigate(RouteNames.CompleteProfile);
|
||||
}}
|
||||
style={styles.confirmButton}
|
||||
/>
|
||||
|
||||
<TouchableOpacity
|
||||
style={styles.manualButton}
|
||||
onPress={() => navigation.navigate(RouteNames.CompleteProfile)}
|
||||
activeOpacity={0.7}
|
||||
>
|
||||
<Text style={styles.manualButtonText}>Enter address manually</Text>
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
</View>
|
||||
);
|
||||
};
|
||||
|
||||
export default SetLocationScreen;
|
||||
|
||||
0
app/features/screens/setLocationScreen/thunk.ts
Normal file
0
app/features/screens/setLocationScreen/thunk.ts
Normal file
71
app/interfaces/auth.ts
Normal file
71
app/interfaces/auth.ts
Normal file
@ -0,0 +1,71 @@
|
||||
export interface LoginResponse {
|
||||
message: string;
|
||||
phone: string;
|
||||
code: string;
|
||||
}
|
||||
|
||||
export interface VerifyOtpResponse {
|
||||
message: string;
|
||||
isNewUser: boolean;
|
||||
accessToken: string;
|
||||
refreshToken: string;
|
||||
user: User;
|
||||
}
|
||||
|
||||
export interface User {
|
||||
id: string;
|
||||
roleId: string;
|
||||
roleEnum: RoleEnum;
|
||||
name: string | null;
|
||||
email: string | null;
|
||||
phone: string;
|
||||
profileImage: string | null;
|
||||
status: UserStatus;
|
||||
mfaEnabled: boolean;
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
deletedAt: string | null;
|
||||
isDeleted: boolean;
|
||||
lastLogoutAt: string | null;
|
||||
loginAt: string;
|
||||
refreshTokenId: string;
|
||||
refreshTokenHash: string;
|
||||
refreshTokenExpiresAt: string;
|
||||
categoryPreferences: CategoryPreference[];
|
||||
gender: Gender | null;
|
||||
dateOfBirth: string | null;
|
||||
role: Role;
|
||||
// merchants: Merchant[];
|
||||
}
|
||||
|
||||
export interface Role {
|
||||
id: string;
|
||||
code: string;
|
||||
scope: string;
|
||||
isSystem: boolean;
|
||||
}
|
||||
|
||||
export interface CategoryPreference {
|
||||
// Extend when backend starts returning data
|
||||
}
|
||||
|
||||
export interface Merchant {
|
||||
name: string;
|
||||
imageUrl: string | null;
|
||||
}
|
||||
|
||||
export type RoleEnum = 'CUSTOMER' | 'DELIVERY';
|
||||
|
||||
export type UserStatus = 'ONBOARDING' | 'ACTIVE' | 'INACTIVE' | 'BLOCKED';
|
||||
|
||||
export enum Gender {
|
||||
MALE = 'MALE',
|
||||
FEMALE = 'FEMALE',
|
||||
OTHER = 'OTHER',
|
||||
}
|
||||
|
||||
export enum VehicleType {
|
||||
BIKE = 'Bike',
|
||||
CAR = 'Car',
|
||||
SCOOTER = 'Scooter',
|
||||
}
|
||||
9
app/interfaces/dashboard.ts
Normal file
9
app/interfaces/dashboard.ts
Normal file
@ -0,0 +1,9 @@
|
||||
export interface ToggleStatusPayload {
|
||||
availabilityStatus: string;
|
||||
}
|
||||
|
||||
export interface ToggleStatusResponse {
|
||||
id: string;
|
||||
availabilityStatus: string;
|
||||
message: string;
|
||||
}
|
||||
4
app/interfaces/index.ts
Normal file
4
app/interfaces/index.ts
Normal file
@ -0,0 +1,4 @@
|
||||
export * from './auth';
|
||||
export * from './onboard';
|
||||
export * from './kyc';
|
||||
export * from './dashboard';
|
||||
21
app/interfaces/kyc.ts
Normal file
21
app/interfaces/kyc.ts
Normal file
@ -0,0 +1,21 @@
|
||||
export interface KycDocUploadResponse {
|
||||
message: string;
|
||||
documents: KycDocUploadResponseDocument[];
|
||||
}
|
||||
|
||||
export interface KycDocUploadResponseDocument {
|
||||
id: string;
|
||||
userId: string;
|
||||
documentType: string;
|
||||
documentNumber: string;
|
||||
fileUrl: string;
|
||||
fileName: string;
|
||||
status: string;
|
||||
createdAt: string;
|
||||
verifiedAt: string | null;
|
||||
}
|
||||
|
||||
export interface MyKycResponse {
|
||||
userId: string;
|
||||
documents: KycDocUploadResponseDocument[];
|
||||
}
|
||||
62
app/interfaces/onboard.ts
Normal file
62
app/interfaces/onboard.ts
Normal file
@ -0,0 +1,62 @@
|
||||
import { Gender, Role, VehicleType } from './auth';
|
||||
|
||||
export interface OnBoardPayload {
|
||||
name: string;
|
||||
email: string;
|
||||
vehicleNumber: string;
|
||||
vehicleType: VehicleType;
|
||||
gender: Gender;
|
||||
}
|
||||
|
||||
export interface UserProfile {
|
||||
id: string;
|
||||
roleId: string;
|
||||
roleEnum:
|
||||
| 'CUSTOMER'
|
||||
| 'MERCHANT'
|
||||
| 'BUSINESS_ADMIN'
|
||||
| 'SUPER_ADMIN'
|
||||
| 'DELIVERY';
|
||||
name: string;
|
||||
email: string;
|
||||
phone: string;
|
||||
profileImage: string | null;
|
||||
status: 'ACTIVE' | 'INACTIVE' | 'SUSPENDED';
|
||||
mfaEnabled: boolean;
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
deletedAt: string | null;
|
||||
isDeleted: boolean;
|
||||
lastLogoutAt: string | null;
|
||||
loginAt: string;
|
||||
refreshTokenId: string;
|
||||
refreshTokenHash: string;
|
||||
refreshTokenExpiresAt: string;
|
||||
categoryPreferences: string[];
|
||||
gender: 'MALE' | 'FEMALE' | 'OTHER';
|
||||
dateOfBirth: string;
|
||||
role: Role;
|
||||
}
|
||||
|
||||
export interface DashboardResponce {
|
||||
message: string;
|
||||
isOnboarded?: boolean;
|
||||
isKycApproved?: boolean;
|
||||
deliveryPartner: {
|
||||
id: string;
|
||||
userId: string;
|
||||
partnerCode: string;
|
||||
kycStatus: string;
|
||||
status: string;
|
||||
availabilityStatus: string;
|
||||
rating: string;
|
||||
currentLatitude: string;
|
||||
currentLongitude: string;
|
||||
lastLocationAt: string;
|
||||
user: {
|
||||
id: string;
|
||||
name: string;
|
||||
status: string;
|
||||
};
|
||||
};
|
||||
}
|
||||
@ -17,18 +17,6 @@ const AuthStack: React.FC = () => {
|
||||
<Stack.Navigator screenOptions={{ headerShown: false }}>
|
||||
<Stack.Screen name={RouteNames.Login} component={LoginScreen} />
|
||||
<Stack.Screen name={RouteNames.Otp} component={OtpScreen} />
|
||||
<Stack.Screen
|
||||
name={RouteNames.SetLocation}
|
||||
component={SetLocationScreen}
|
||||
/>
|
||||
<Stack.Screen
|
||||
name={RouteNames.CompleteProfile}
|
||||
component={CompleteProfileScreen}
|
||||
/>
|
||||
<Stack.Screen
|
||||
name={RouteNames.OnboardingComplete}
|
||||
component={OnboardingCompleteScreen}
|
||||
/>
|
||||
</Stack.Navigator>
|
||||
);
|
||||
};
|
||||
|
||||
@ -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';
|
||||
|
||||
@ -4,8 +4,15 @@ import { RouteNames } from '@utils/constants';
|
||||
export type AuthStackParamList = {
|
||||
[RouteNames.Login]: undefined;
|
||||
[RouteNames.Otp]: { phone: string };
|
||||
// [RouteNames.SetLocation]: undefined;
|
||||
// [RouteNames.CompleteProfile]: undefined;
|
||||
// [RouteNames.OnboardingComplete]: undefined;
|
||||
};
|
||||
|
||||
export type OnBoardingParamList = {
|
||||
[RouteNames.SetLocation]: undefined;
|
||||
[RouteNames.CompleteProfile]: undefined;
|
||||
[RouteNames.Kyc]: undefined;
|
||||
[RouteNames.OnboardingComplete]: undefined;
|
||||
};
|
||||
|
||||
@ -34,4 +41,5 @@ export type AppStackParamList = {
|
||||
export type RootStackParamList = {
|
||||
Auth: NavigatorScreenParams<AuthStackParamList>;
|
||||
App: NavigatorScreenParams<AppStackParamList>;
|
||||
OnBoarding: NavigatorScreenParams<OnBoardingParamList>;
|
||||
};
|
||||
|
||||
34
app/navigation/onBoardingStack.tsx
Normal file
34
app/navigation/onBoardingStack.tsx
Normal file
@ -0,0 +1,34 @@
|
||||
import React from 'react';
|
||||
import { createNativeStackNavigator } from '@react-navigation/native-stack';
|
||||
import { OnBoardingParamList } from '@navigation/navigationTypes';
|
||||
import {
|
||||
SetLocationScreen,
|
||||
CompleteProfileScreen,
|
||||
OnboardingCompleteScreen,
|
||||
KycScreen,
|
||||
} from '@features/screens';
|
||||
import { RouteNames } from '@utils/constants';
|
||||
|
||||
const Stack = createNativeStackNavigator<OnBoardingParamList>();
|
||||
|
||||
const onBoardingStack: React.FC = () => {
|
||||
return (
|
||||
<Stack.Navigator screenOptions={{ headerShown: false }}>
|
||||
{/* <Stack.Screen
|
||||
name={RouteNames.SetLocation}
|
||||
component={SetLocationScreen}
|
||||
/> */}
|
||||
{/* <Stack.Screen
|
||||
name={RouteNames.CompleteProfile}
|
||||
component={CompleteProfileScreen}
|
||||
/> */}
|
||||
<Stack.Screen name={RouteNames.Kyc} component={KycScreen} />
|
||||
<Stack.Screen
|
||||
name={RouteNames.OnboardingComplete}
|
||||
component={OnboardingCompleteScreen}
|
||||
/>
|
||||
</Stack.Navigator>
|
||||
);
|
||||
};
|
||||
|
||||
export default onBoardingStack;
|
||||
@ -1,10 +1,16 @@
|
||||
import React from 'react';
|
||||
import { NavigationContainer, DefaultTheme, DarkTheme } from '@react-navigation/native';
|
||||
import {
|
||||
NavigationContainer,
|
||||
DefaultTheme,
|
||||
DarkTheme,
|
||||
} from '@react-navigation/native';
|
||||
import { createNativeStackNavigator } from '@react-navigation/native-stack';
|
||||
import { useColorScheme } from 'react-native';
|
||||
import { RootStackParamList } from '@navigation/navigationTypes';
|
||||
import AuthStack from './authStack';
|
||||
import AppStack from './appStack';
|
||||
import onBoardingStack from './onBoardingStack';
|
||||
import { useAppSelector } from '@store';
|
||||
|
||||
const Stack = createNativeStackNavigator<RootStackParamList>();
|
||||
|
||||
@ -12,11 +18,21 @@ const RootNavigator: React.FC = () => {
|
||||
const scheme = useColorScheme();
|
||||
const theme = scheme === 'dark' ? DarkTheme : DefaultTheme;
|
||||
|
||||
const accessToken = useAppSelector(state => state.auth.accessToken);
|
||||
const isOnboarding = useAppSelector(
|
||||
state => state.auth.user?.status === 'ONBOARDING',
|
||||
);
|
||||
|
||||
return (
|
||||
<NavigationContainer theme={theme}>
|
||||
<Stack.Navigator screenOptions={{ headerShown: false }}>
|
||||
{!accessToken ? (
|
||||
<Stack.Screen name="Auth" component={AuthStack} />
|
||||
) : isOnboarding ? (
|
||||
<Stack.Screen name="OnBoarding" component={onBoardingStack} />
|
||||
) : (
|
||||
<Stack.Screen name="App" component={AppStack} />
|
||||
)}
|
||||
</Stack.Navigator>
|
||||
</NavigationContainer>
|
||||
);
|
||||
|
||||
221
app/services/apiClient.ts
Normal file
221
app/services/apiClient.ts
Normal file
@ -0,0 +1,221 @@
|
||||
import axios, {
|
||||
AxiosInstance,
|
||||
AxiosRequestConfig,
|
||||
InternalAxiosRequestConfig,
|
||||
AxiosResponse,
|
||||
AxiosError,
|
||||
} from 'axios';
|
||||
import AsyncStorage from '@react-native-async-storage/async-storage';
|
||||
import { Store } from '@reduxjs/toolkit';
|
||||
|
||||
// ─── Storage Keys ────────────────────────────────────────────────────────────
|
||||
const STORAGE_KEYS = {
|
||||
ACCESS_TOKEN: '@auth_access_token',
|
||||
REFRESH_TOKEN: '@auth_refresh_token',
|
||||
} as const;
|
||||
|
||||
// ─── Config ──────────────────────────────────────────────────────────────────
|
||||
const BASE_URL = 'https://720b-202-8-116-13.ngrok-free.app'; // TODO: replace with your actual base URL
|
||||
|
||||
// ─── Token Helpers ───────────────────────────────────────────────────────────
|
||||
export const tokenManager = {
|
||||
async getAccessToken(): Promise<string | null> {
|
||||
return AsyncStorage.getItem(STORAGE_KEYS.ACCESS_TOKEN);
|
||||
},
|
||||
|
||||
async getRefreshToken(): Promise<string | null> {
|
||||
return AsyncStorage.getItem(STORAGE_KEYS.REFRESH_TOKEN);
|
||||
},
|
||||
|
||||
async setTokens(accessToken: string, refreshToken: string): Promise<void> {
|
||||
await AsyncStorage.setItem(STORAGE_KEYS.ACCESS_TOKEN, accessToken);
|
||||
await AsyncStorage.setItem(STORAGE_KEYS.REFRESH_TOKEN, refreshToken);
|
||||
},
|
||||
|
||||
async clearTokens(): Promise<void> {
|
||||
await AsyncStorage.removeItem(STORAGE_KEYS.ACCESS_TOKEN);
|
||||
await AsyncStorage.removeItem(STORAGE_KEYS.REFRESH_TOKEN);
|
||||
},
|
||||
};
|
||||
|
||||
// ─── Axios Instance ──────────────────────────────────────────────────────────
|
||||
const axiosInstance: AxiosInstance = axios.create({
|
||||
baseURL: BASE_URL,
|
||||
timeout: 15000,
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
Accept: 'application/json',
|
||||
},
|
||||
});
|
||||
|
||||
// ─── Request Interceptor — Attach Access Token ──────────────────────────────
|
||||
axiosInstance.interceptors.request.use(
|
||||
async (config: InternalAxiosRequestConfig) => {
|
||||
// Skip attaching token for auth endpoints (login, register, refresh)
|
||||
const publicPaths = [
|
||||
'/auth/otp/request',
|
||||
'/auth/otp/verify',
|
||||
'/auth/refresh-token',
|
||||
];
|
||||
const isPublic = publicPaths.some(path => config.url?.includes(path));
|
||||
|
||||
if (!isPublic) {
|
||||
const token = await tokenManager.getAccessToken();
|
||||
if (token) {
|
||||
config.headers.Authorization = `Bearer ${token}`;
|
||||
}
|
||||
}
|
||||
|
||||
// When the body is FormData, delete the Content-Type header entirely.
|
||||
// This forces the native XMLHttpRequest layer to auto-generate the correct
|
||||
// header: 'multipart/form-data; boundary=------xxxx'. The boundary is
|
||||
// required for the server to parse the multipart body — it cannot be set
|
||||
// manually because only XMLHttpRequest knows the generated boundary string.
|
||||
if (config.data instanceof FormData) {
|
||||
delete config.headers['Content-Type'];
|
||||
}
|
||||
|
||||
// Debug logging (remove in production)
|
||||
if (__DEV__) {
|
||||
// console.log(`🚀 [API] ${config.method?.toUpperCase()} ${config.url}`);
|
||||
}
|
||||
|
||||
return config;
|
||||
},
|
||||
(error: AxiosError) => {
|
||||
return Promise.reject(error);
|
||||
},
|
||||
);
|
||||
|
||||
// ─── Response Interceptor — Handle Token Refresh ────────────────────────────
|
||||
let isRefreshing = false;
|
||||
let failedQueue: Array<{
|
||||
resolve: (token: string) => void;
|
||||
reject: (error: unknown) => void;
|
||||
}> = [];
|
||||
|
||||
const processQueue = (error: unknown, token: string | null = null) => {
|
||||
failedQueue.forEach(({ resolve, reject }) => {
|
||||
if (error) {
|
||||
reject(error);
|
||||
} else if (token) {
|
||||
resolve(token);
|
||||
}
|
||||
});
|
||||
failedQueue = [];
|
||||
};
|
||||
|
||||
axiosInstance.interceptors.response.use(
|
||||
(response: AxiosResponse) => {
|
||||
return response;
|
||||
},
|
||||
async (error: AxiosError) => {
|
||||
const originalRequest = error.config as InternalAxiosRequestConfig & {
|
||||
_retry?: boolean;
|
||||
};
|
||||
|
||||
// If 401 and we haven't already retried this request
|
||||
if (error.response?.status === 401 && !originalRequest._retry) {
|
||||
// If already refreshing, queue this request
|
||||
if (isRefreshing) {
|
||||
return new Promise<AxiosResponse>((resolve, reject) => {
|
||||
failedQueue.push({
|
||||
resolve: (token: string) => {
|
||||
originalRequest.headers.Authorization = `Bearer ${token}`;
|
||||
resolve(axiosInstance(originalRequest));
|
||||
},
|
||||
reject,
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
originalRequest._retry = true;
|
||||
isRefreshing = true;
|
||||
|
||||
try {
|
||||
const refreshToken = await tokenManager.getRefreshToken();
|
||||
|
||||
if (!refreshToken) {
|
||||
throw new Error('No refresh token available');
|
||||
}
|
||||
|
||||
// Call refresh endpoint (uses a fresh axios call to avoid interceptor loop)
|
||||
const { data } = await axios.post(`${BASE_URL}/auth/refresh-token`, {
|
||||
refreshToken,
|
||||
});
|
||||
|
||||
const { accessToken: newAccessToken, refreshToken: newRefreshToken } =
|
||||
data;
|
||||
|
||||
await tokenManager.setTokens(newAccessToken, newRefreshToken);
|
||||
|
||||
// Retry all queued requests with the new token
|
||||
processQueue(null, newAccessToken);
|
||||
|
||||
// Retry the original request
|
||||
originalRequest.headers.Authorization = `Bearer ${newAccessToken}`;
|
||||
return axiosInstance(originalRequest);
|
||||
} catch (refreshError) {
|
||||
processQueue(refreshError, null);
|
||||
await tokenManager.clearTokens();
|
||||
|
||||
// TODO: Navigate to login screen or dispatch a logout action
|
||||
// e.g. store.dispatch(logout());
|
||||
|
||||
return Promise.reject(refreshError);
|
||||
} finally {
|
||||
isRefreshing = false;
|
||||
}
|
||||
}
|
||||
|
||||
// For all other errors, reject as-is
|
||||
return Promise.reject(error);
|
||||
},
|
||||
);
|
||||
let storeInstance: Store | null = null;
|
||||
export const injectStore = (_store: Store | null): void => {
|
||||
storeInstance = _store;
|
||||
};
|
||||
|
||||
// ─── API Methods ─────────────────────────────────────────────────────────────
|
||||
export const apiClient = {
|
||||
async get<T>(url: string, config?: AxiosRequestConfig): Promise<T> {
|
||||
const response = await axiosInstance.get<T>(url, config);
|
||||
return response.data;
|
||||
},
|
||||
|
||||
async post<T>(
|
||||
url: string,
|
||||
body?: unknown,
|
||||
config?: AxiosRequestConfig,
|
||||
): Promise<T> {
|
||||
const response = await axiosInstance.post<T>(url, body, config);
|
||||
return response.data;
|
||||
},
|
||||
|
||||
async put<T>(
|
||||
url: string,
|
||||
body?: unknown,
|
||||
config?: AxiosRequestConfig,
|
||||
): Promise<T> {
|
||||
const response = await axiosInstance.put<T>(url, body, config);
|
||||
return response.data;
|
||||
},
|
||||
|
||||
async patch<T>(
|
||||
url: string,
|
||||
body?: unknown,
|
||||
config?: AxiosRequestConfig,
|
||||
): Promise<T> {
|
||||
const response = await axiosInstance.patch<T>(url, body, config);
|
||||
return response.data;
|
||||
},
|
||||
|
||||
async delete<T>(url: string, config?: AxiosRequestConfig): Promise<T> {
|
||||
const response = await axiosInstance.delete<T>(url, config);
|
||||
return response.data;
|
||||
},
|
||||
};
|
||||
|
||||
// Export the raw instance if needed for advanced usage
|
||||
export { axiosInstance };
|
||||
2
app/services/index.ts
Normal file
2
app/services/index.ts
Normal file
@ -0,0 +1,2 @@
|
||||
export * from './apiClient';
|
||||
export * from './locationService';
|
||||
@ -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';
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Types
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
export interface LatLng {
|
||||
latitude: number;
|
||||
longitude: number;
|
||||
}
|
||||
|
||||
export interface LocationResult {
|
||||
coords: LatLng;
|
||||
address: string;
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Constants
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
const GOOGLE_MAPS_API_KEY = 'AIzaSyDKaKVyQCmSpHMXcMkNCccSXpILpuvwsVM';
|
||||
|
||||
/** Default region (Bengaluru) used as a fallback before the user's location loads. */
|
||||
export const DEFAULT_LOCATION: LatLng = {
|
||||
latitude: 12.9352,
|
||||
longitude: 77.6245,
|
||||
};
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Permission helpers
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Request location permission from the user.
|
||||
* Returns `true` if permission was granted, `false` otherwise.
|
||||
*/
|
||||
export const requestLocationPermission = async (): Promise<boolean> => {
|
||||
if (Platform.OS === 'ios') {
|
||||
try {
|
||||
const result = await Geolocation.requestAuthorization('whenInUse');
|
||||
return result === 'granted';
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// 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;
|
||||
}
|
||||
|
||||
class LocationService {
|
||||
async requestPermission(): Promise<boolean> {
|
||||
if (Platform.OS === 'android') {
|
||||
const granted = await PermissionsAndroid.request(
|
||||
PermissionsAndroid.PERMISSIONS.ACCESS_FINE_LOCATION,
|
||||
{
|
||||
title: 'Location Permission',
|
||||
message: 'This app needs access to your location to track deliveries.',
|
||||
message: 'This app needs access to your location to set it on the map.',
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
// Mocked location resolver
|
||||
async getCurrentLocation(): Promise<{ coordinates: Coordinates; address: string; city: string; state: string; pincode: string }> {
|
||||
// Return mock coordinates and address after a delay
|
||||
await new Promise<void>(resolve => setTimeout(resolve, 800));
|
||||
return {
|
||||
coordinates: {
|
||||
latitude: 12.9716,
|
||||
longitude: 77.5946,
|
||||
},
|
||||
address: 'Koramangala 4th Block, Bengaluru',
|
||||
city: 'Bengaluru',
|
||||
state: 'Karnataka',
|
||||
pincode: '560034',
|
||||
};
|
||||
);
|
||||
console.log('granted', granted);
|
||||
return granted === PermissionsAndroid.RESULTS.GRANTED;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
export const locationService = new LocationService();
|
||||
/**
|
||||
* Show an alert when the user denies location permission, with an option
|
||||
* to open app settings.
|
||||
*/
|
||||
export const showPermissionDeniedAlert = (): void => {
|
||||
Alert.alert(
|
||||
'Location Permission Required',
|
||||
'Please enable location permission in your device settings to use this feature.',
|
||||
[
|
||||
{ text: 'Cancel', style: 'cancel' },
|
||||
{ text: 'Open Settings', onPress: () => Linking.openSettings() },
|
||||
],
|
||||
);
|
||||
};
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Position helpers
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Get the device's current GPS position.
|
||||
* Wraps the callback-based Geolocation API in a Promise.
|
||||
*/
|
||||
/**
|
||||
* Wraps Geolocation.getCurrentPosition in a Promise.
|
||||
* Tries high-accuracy first; falls back to low-accuracy on failure.
|
||||
*/
|
||||
const getPositionOnce = (highAccuracy: boolean): Promise<LatLng> =>
|
||||
new Promise<LatLng>((resolve, reject) => {
|
||||
Geolocation.getCurrentPosition(
|
||||
(position: GeoPosition) => {
|
||||
resolve({
|
||||
latitude: position.coords.latitude,
|
||||
longitude: position.coords.longitude,
|
||||
});
|
||||
},
|
||||
(error: GeoError) => {
|
||||
console.log(
|
||||
'Geolocation error (highAccuracy=' + highAccuracy + '):',
|
||||
error,
|
||||
);
|
||||
reject(error);
|
||||
},
|
||||
{
|
||||
enableHighAccuracy: highAccuracy,
|
||||
timeout: highAccuracy ? 5000 : 10000,
|
||||
maximumAge: 10000,
|
||||
forceRequestLocation: false, // avoid native dialog that can crash
|
||||
showLocationDialog: true,
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
export const getCurrentPosition = async (): Promise<LatLng> => {
|
||||
console.log('getCurrentPosition called');
|
||||
try {
|
||||
// First attempt: high accuracy (GPS)
|
||||
return await getPositionOnce(true);
|
||||
} catch (highAccuracyError) {
|
||||
console.warn(
|
||||
'High-accuracy location failed, retrying with low accuracy…',
|
||||
highAccuracyError,
|
||||
);
|
||||
try {
|
||||
// Fallback: network / cell-tower accuracy
|
||||
return await getPositionOnce(false);
|
||||
} catch (lowAccuracyError) {
|
||||
console.error('Failed to get current position:', lowAccuracyError);
|
||||
throw lowAccuracyError;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Geocoding
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Reverse-geocode a lat/lng pair into a human-readable address string
|
||||
* using the Google Maps Geocoding API.
|
||||
*/
|
||||
export const reverseGeocode = async (coords: LatLng): Promise<string> => {
|
||||
try {
|
||||
const url =
|
||||
`https://maps.googleapis.com/maps/api/geocode/json` +
|
||||
`?latlng=${coords.latitude},${coords.longitude}` +
|
||||
`&key=${GOOGLE_MAPS_API_KEY}`;
|
||||
|
||||
const response = await fetch(url);
|
||||
const data = await response.json();
|
||||
|
||||
if (data.status === 'OK' && data.results.length > 0) {
|
||||
// Return the most specific formatted address.
|
||||
return data.results[0].formatted_address;
|
||||
}
|
||||
|
||||
console.warn(
|
||||
'Google Maps Geocoding failed:',
|
||||
data.status,
|
||||
data.error_message || '',
|
||||
);
|
||||
|
||||
// Fallback to OpenStreetMap Nominatim
|
||||
const osmUrl = `https://nominatim.openstreetmap.org/reverse?format=jsonv2&lat=${coords.latitude}&lon=${coords.longitude}`;
|
||||
const osmResponse = await fetch(osmUrl, {
|
||||
headers: {
|
||||
'User-Agent': 'SGDeliveryCustomerApp/1.0',
|
||||
},
|
||||
});
|
||||
const osmData = await osmResponse.json();
|
||||
if (osmData && osmData.display_name) {
|
||||
return osmData.display_name;
|
||||
}
|
||||
|
||||
return 'Address not found';
|
||||
} catch (error) {
|
||||
console.log('reverseGeocode error', error);
|
||||
try {
|
||||
// Fallback to OpenStreetMap Nominatim on catch
|
||||
const osmUrl = `https://nominatim.openstreetmap.org/reverse?format=jsonv2&lat=${coords.latitude}&lon=${coords.longitude}`;
|
||||
const osmResponse = await fetch(osmUrl, {
|
||||
headers: {
|
||||
'User-Agent': 'SGDeliveryCustomerApp/1.0',
|
||||
},
|
||||
});
|
||||
const osmData = await osmResponse.json();
|
||||
if (osmData && osmData.display_name) {
|
||||
return osmData.display_name;
|
||||
}
|
||||
} catch (osmError) {
|
||||
console.log('OSM fallback error', osmError);
|
||||
}
|
||||
return 'Unable to fetch address';
|
||||
}
|
||||
};
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Combined helper
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* All-in-one helper: request permissions → get position → reverse geocode.
|
||||
* Returns both the coordinates and the human-readable address.
|
||||
*
|
||||
* Throws if permission is denied or position cannot be obtained.
|
||||
*/
|
||||
export const getCurrentLocationWithAddress =
|
||||
async (): Promise<LocationResult> => {
|
||||
const hasPermission = await requestLocationPermission();
|
||||
console.log('hasPermission', hasPermission);
|
||||
if (!hasPermission) {
|
||||
// Alert shown by the caller so it doesn't fire inside a Promise chain
|
||||
// on mount (which can cause a crash on some Android versions).
|
||||
throw new Error('Location permission denied');
|
||||
}
|
||||
|
||||
const coords = await getCurrentPosition();
|
||||
const address = await reverseGeocode(coords);
|
||||
|
||||
console.log('coords', coords);
|
||||
console.log('address', address);
|
||||
|
||||
return { coords, address };
|
||||
};
|
||||
|
||||
@ -1,73 +0,0 @@
|
||||
import { createApi, fakeBaseQuery } from '@reduxjs/toolkit/query/react';
|
||||
import { PartnerProfile } from '@app-types/index';
|
||||
|
||||
// Mock responses
|
||||
const mockSendOtpResponse = { requestId: 'req_mock_001' };
|
||||
const mockVerifyOtpResponse = { token: 'mock_token_xyz', partnerId: 'partner_001' };
|
||||
const mockProfile: PartnerProfile = {
|
||||
partnerId: 'partner_001',
|
||||
fullName: 'Rahul Kumar',
|
||||
email: 'rahul@example.com',
|
||||
phone: '9876543210',
|
||||
locationType: 'home',
|
||||
location: {
|
||||
coordinates: { latitude: 12.9716, longitude: 77.5946 },
|
||||
address: 'Koramangala 4th Block',
|
||||
city: 'Bengaluru',
|
||||
state: 'Karnataka',
|
||||
pincode: '560034',
|
||||
},
|
||||
avatarUri: null,
|
||||
rating: 4.8,
|
||||
totalDeliveries: 342,
|
||||
acceptanceRate: 95,
|
||||
completionRate: 98,
|
||||
isVerified: true,
|
||||
};
|
||||
|
||||
export const authApi = createApi({
|
||||
reducerPath: 'authApi',
|
||||
baseQuery: fakeBaseQuery(),
|
||||
endpoints: (builder) => ({
|
||||
sendOtp: builder.mutation<{ requestId: string }, { phone: string }>({
|
||||
queryFn: async ({ phone: _phone }) => {
|
||||
await new Promise<void>((r) => setTimeout(r, 1000));
|
||||
return { data: mockSendOtpResponse };
|
||||
},
|
||||
}),
|
||||
verifyOtp: builder.mutation<
|
||||
{ token: string; partnerId: string },
|
||||
{ requestId: string; otp: string }
|
||||
>({
|
||||
queryFn: async ({ otp }) => {
|
||||
await new Promise<void>((r) => setTimeout(r, 1000));
|
||||
if (otp === '000000') {
|
||||
return { error: { status: 'CUSTOM_ERROR', error: 'Invalid OTP' } };
|
||||
}
|
||||
return { data: mockVerifyOtpResponse };
|
||||
},
|
||||
}),
|
||||
getProfile: builder.query<PartnerProfile, string>({
|
||||
queryFn: async (_partnerId) => {
|
||||
await new Promise<void>((r) => setTimeout(r, 500));
|
||||
return { data: mockProfile };
|
||||
},
|
||||
}),
|
||||
updateProfile: builder.mutation<
|
||||
PartnerProfile,
|
||||
Partial<PartnerProfile>
|
||||
>({
|
||||
queryFn: async (updates) => {
|
||||
await new Promise<void>((r) => setTimeout(r, 800));
|
||||
return { data: { ...mockProfile, ...updates } };
|
||||
},
|
||||
}),
|
||||
}),
|
||||
});
|
||||
|
||||
export const {
|
||||
useSendOtpMutation,
|
||||
useVerifyOtpMutation,
|
||||
useGetProfileQuery,
|
||||
useUpdateProfileMutation,
|
||||
} = authApi;
|
||||
@ -1,24 +0,0 @@
|
||||
import { createApi, fakeBaseQuery } from '@reduxjs/toolkit/query/react';
|
||||
import { DayEarning } from '@app-types/index';
|
||||
import { mockWeeklyEarnings, mockTodayEarnings } from '@store/mockData/mockEarnings';
|
||||
|
||||
export const earningsApi = createApi({
|
||||
reducerPath: 'earningsApi',
|
||||
baseQuery: fakeBaseQuery(),
|
||||
endpoints: (builder) => ({
|
||||
getTodayEarnings: builder.query<typeof mockTodayEarnings, void>({
|
||||
queryFn: async () => {
|
||||
await new Promise<void>((r) => setTimeout(r, 500));
|
||||
return { data: mockTodayEarnings };
|
||||
},
|
||||
}),
|
||||
getWeeklyEarnings: builder.query<DayEarning[], void>({
|
||||
queryFn: async () => {
|
||||
await new Promise<void>((r) => setTimeout(r, 500));
|
||||
return { data: mockWeeklyEarnings };
|
||||
},
|
||||
}),
|
||||
}),
|
||||
});
|
||||
|
||||
export const { useGetTodayEarningsQuery, useGetWeeklyEarningsQuery } = earningsApi;
|
||||
@ -1,61 +0,0 @@
|
||||
import { createApi, fakeBaseQuery } from '@reduxjs/toolkit/query/react';
|
||||
import { Job } from '@app-types/index';
|
||||
import { JobStatus } from '@utils/constants';
|
||||
import { mockJobOffer } from '@store/mockData/mockJobs';
|
||||
|
||||
export const jobApi = createApi({
|
||||
reducerPath: 'jobApi',
|
||||
baseQuery: fakeBaseQuery(),
|
||||
endpoints: (builder) => ({
|
||||
acceptJob: builder.mutation<Job, string>({
|
||||
queryFn: async (jobId) => {
|
||||
await new Promise<void>((r) => setTimeout(r, 500));
|
||||
return {
|
||||
data: {
|
||||
...mockJobOffer,
|
||||
jobId,
|
||||
orderId: '#ORD125487',
|
||||
status: JobStatus.Accepted,
|
||||
acceptedAt: new Date().toISOString(),
|
||||
collectAmount: 0,
|
||||
},
|
||||
};
|
||||
},
|
||||
}),
|
||||
rejectJob: builder.mutation<void, string>({
|
||||
queryFn: async (_jobId) => {
|
||||
await new Promise<void>((r) => setTimeout(r, 300));
|
||||
return { data: undefined };
|
||||
},
|
||||
}),
|
||||
confirmPickup: builder.mutation<void, string>({
|
||||
queryFn: async (_jobId) => {
|
||||
await new Promise<void>((r) => setTimeout(r, 500));
|
||||
return { data: undefined };
|
||||
},
|
||||
}),
|
||||
confirmDelivery: builder.mutation<
|
||||
{ earnings: number },
|
||||
{ jobId: string; rating: number }
|
||||
>({
|
||||
queryFn: async ({ jobId: _jobId }) => {
|
||||
await new Promise<void>((r) => setTimeout(r, 800));
|
||||
return { data: { earnings: 78 } };
|
||||
},
|
||||
}),
|
||||
reportIssue: builder.mutation<void, { jobId: string; reason: string }>({
|
||||
queryFn: async (_args) => {
|
||||
await new Promise<void>((r) => setTimeout(r, 500));
|
||||
return { data: undefined };
|
||||
},
|
||||
}),
|
||||
}),
|
||||
});
|
||||
|
||||
export const {
|
||||
useAcceptJobMutation,
|
||||
useRejectJobMutation,
|
||||
useConfirmPickupMutation,
|
||||
useConfirmDeliveryMutation,
|
||||
useReportIssueMutation,
|
||||
} = jobApi;
|
||||
2
app/store/commonReducers/auth/index.ts
Normal file
2
app/store/commonReducers/auth/index.ts
Normal file
@ -0,0 +1,2 @@
|
||||
export * from './thunk';
|
||||
export * from './reducer';
|
||||
148
app/store/commonReducers/auth/reducer.ts
Normal file
148
app/store/commonReducers/auth/reducer.ts
Normal file
@ -0,0 +1,148 @@
|
||||
import { createReducer, createAction, PayloadAction } from '@reduxjs/toolkit';
|
||||
import { PartnerProfile, PartnerLocation } from '@app-types/index';
|
||||
import {
|
||||
getProfileThunk,
|
||||
updateProfileThunk,
|
||||
loginWithPhone,
|
||||
verifyOtp,
|
||||
} from './thunk';
|
||||
import { LoginResponse, User } from '@interfaces';
|
||||
import { getDashboard } from '@features/screens/onboardingCompleteScreen/thunk';
|
||||
|
||||
export interface AuthState {
|
||||
isAuthenticated: boolean;
|
||||
loginData: LoginResponse | null;
|
||||
user: User | null;
|
||||
accessToken: string | null;
|
||||
refreshToken: string | null;
|
||||
isNewUser: boolean | null;
|
||||
hasCompletedOnboarding: boolean;
|
||||
isOnline: boolean;
|
||||
token: string | null;
|
||||
partnerId: string | null;
|
||||
partnerProfile: PartnerProfile | null;
|
||||
requestId: string | null;
|
||||
loading: boolean;
|
||||
error: string | null;
|
||||
}
|
||||
|
||||
const initialState: AuthState = {
|
||||
isAuthenticated: false,
|
||||
user: null,
|
||||
accessToken: null,
|
||||
refreshToken: null,
|
||||
isNewUser: null,
|
||||
hasCompletedOnboarding: false,
|
||||
isOnline: false,
|
||||
token: null,
|
||||
partnerId: null,
|
||||
partnerProfile: null,
|
||||
requestId: null,
|
||||
loading: false,
|
||||
error: null,
|
||||
loginData: null,
|
||||
};
|
||||
|
||||
export const setAuthenticated = createAction<{
|
||||
token: string;
|
||||
partnerId: string;
|
||||
}>('auth/setAuthenticated');
|
||||
export const setRequestId = createAction<string>('auth/setRequestId');
|
||||
export const setOnboardingComplete = createAction('auth/setOnboardingComplete');
|
||||
export const setOnlineStatus = createAction<boolean>('auth/setOnlineStatus');
|
||||
export const setProfile = createAction<PartnerProfile>('auth/setProfile');
|
||||
export const setLocation = createAction<PartnerLocation>('auth/setLocation');
|
||||
export const logout = createAction('auth/logout');
|
||||
|
||||
export const authReducer = createReducer(initialState, builder => {
|
||||
builder
|
||||
// Sync Actions
|
||||
.addCase(setAuthenticated, (state, action) => {
|
||||
state.isAuthenticated = true;
|
||||
state.token = action.payload.token;
|
||||
state.partnerId = action.payload.partnerId;
|
||||
})
|
||||
.addCase(setRequestId, (state, action) => {
|
||||
state.requestId = action.payload;
|
||||
})
|
||||
.addCase(setOnboardingComplete, state => {
|
||||
state.hasCompletedOnboarding = true;
|
||||
})
|
||||
.addCase(setOnlineStatus, (state, action) => {
|
||||
state.isOnline = action.payload;
|
||||
})
|
||||
.addCase(setProfile, (state, action) => {
|
||||
state.partnerProfile = action.payload;
|
||||
})
|
||||
.addCase(setLocation, (state, action) => {
|
||||
if (state.partnerProfile) {
|
||||
state.partnerProfile.location = action.payload;
|
||||
}
|
||||
})
|
||||
.addCase(logout, () => initialState)
|
||||
// Async Thunks
|
||||
// Login
|
||||
.addCase(loginWithPhone.pending, state => {
|
||||
state.loading = true;
|
||||
state.error = null;
|
||||
})
|
||||
.addCase(loginWithPhone.fulfilled, (state, action) => {
|
||||
state.loginData = action.payload;
|
||||
state.loading = false;
|
||||
state.error = null;
|
||||
})
|
||||
.addCase(loginWithPhone.rejected, (state, action) => {
|
||||
state.loading = false;
|
||||
state.error = action.payload as string;
|
||||
})
|
||||
// Verify OTP
|
||||
.addCase(verifyOtp.pending, state => {
|
||||
state.loading = true;
|
||||
state.error = null;
|
||||
})
|
||||
.addCase(verifyOtp.fulfilled, (state, action) => {
|
||||
const { user, accessToken, refreshToken, isNewUser } = action.payload;
|
||||
state.user = user;
|
||||
state.accessToken = accessToken;
|
||||
state.refreshToken = refreshToken;
|
||||
state.isNewUser = isNewUser;
|
||||
state.loading = false;
|
||||
state.error = null;
|
||||
})
|
||||
.addCase(verifyOtp.rejected, (state, action) => {
|
||||
state.loading = false;
|
||||
state.error = action.payload as string;
|
||||
})
|
||||
.addCase(getProfileThunk.pending, state => {
|
||||
state.loading = true;
|
||||
state.error = null;
|
||||
})
|
||||
.addCase(getProfileThunk.fulfilled, (state, action) => {
|
||||
state.loading = false;
|
||||
state.partnerProfile = action.payload;
|
||||
})
|
||||
.addCase(getProfileThunk.rejected, (state, action) => {
|
||||
state.loading = false;
|
||||
state.error = action.payload as string;
|
||||
})
|
||||
.addCase(updateProfileThunk.pending, state => {
|
||||
state.loading = true;
|
||||
state.error = null;
|
||||
})
|
||||
.addCase(updateProfileThunk.fulfilled, (state, action) => {
|
||||
state.loading = false;
|
||||
state.partnerProfile = action.payload;
|
||||
})
|
||||
.addCase(updateProfileThunk.rejected, (state, action) => {
|
||||
state.loading = false;
|
||||
state.error = action.payload as string;
|
||||
})
|
||||
// When dashboard is fetched, sync user.status so rootNavigator
|
||||
// automatically swaps from OnBoardingStack → AppStack
|
||||
.addCase(getDashboard.fulfilled, (state, action) => {
|
||||
const serverStatus = action.payload?.deliveryPartner?.user?.status;
|
||||
if (state.user && serverStatus) {
|
||||
state.user.status = serverStatus as User['status'];
|
||||
}
|
||||
});
|
||||
});
|
||||
66
app/store/commonReducers/auth/thunk.ts
Normal file
66
app/store/commonReducers/auth/thunk.ts
Normal file
@ -0,0 +1,66 @@
|
||||
import { createAsyncThunk } from '@reduxjs/toolkit';
|
||||
import { PartnerProfile } from '@app-types/index';
|
||||
import { getProfile, loginApi, updateProfile, verifyOtpApi } from '@api';
|
||||
import { tokenManager } from '@services';
|
||||
|
||||
export const loginWithPhone = createAsyncThunk(
|
||||
'auth/loginWithPhone',
|
||||
async (phone: string, { rejectWithValue }) => {
|
||||
try {
|
||||
const response = await loginApi(phone);
|
||||
console.log(response);
|
||||
return response;
|
||||
} catch (error: unknown) {
|
||||
const message = error instanceof Error ? error.message : 'Login failed';
|
||||
return rejectWithValue(message);
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
export const verifyOtp = createAsyncThunk(
|
||||
'auth/verifyOtp',
|
||||
async (
|
||||
{ phone, code, role }: { phone: string; code: string; role: string },
|
||||
{ rejectWithValue },
|
||||
) => {
|
||||
try {
|
||||
const response = await verifyOtpApi(phone, code, role);
|
||||
|
||||
// Persist tokens on successful verification
|
||||
if (response.accessToken && response.refreshToken) {
|
||||
await tokenManager.setTokens(
|
||||
response.accessToken,
|
||||
response.refreshToken,
|
||||
);
|
||||
}
|
||||
|
||||
return response;
|
||||
} catch (error: unknown) {
|
||||
const message =
|
||||
error instanceof Error ? error.message : 'OTP verification failed';
|
||||
return rejectWithValue(message);
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
export const getProfileThunk = createAsyncThunk<PartnerProfile, string>(
|
||||
'auth/getProfile',
|
||||
async (partnerId, { rejectWithValue }) => {
|
||||
try {
|
||||
return await getProfile(partnerId);
|
||||
} catch (error: any) {
|
||||
return rejectWithValue(error.message || 'Failed to get profile');
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
export const updateProfileThunk = createAsyncThunk<
|
||||
PartnerProfile,
|
||||
Partial<PartnerProfile>
|
||||
>('auth/updateProfile', async (updates, { rejectWithValue }) => {
|
||||
try {
|
||||
return await updateProfile(updates);
|
||||
} catch (error: any) {
|
||||
return rejectWithValue(error.message || 'Failed to update profile');
|
||||
}
|
||||
});
|
||||
2
app/store/commonReducers/earnings/index.ts
Normal file
2
app/store/commonReducers/earnings/index.ts
Normal file
@ -0,0 +1,2 @@
|
||||
export * from './thunk';
|
||||
export * from './reducer';
|
||||
74
app/store/commonReducers/earnings/reducer.ts
Normal file
74
app/store/commonReducers/earnings/reducer.ts
Normal file
@ -0,0 +1,74 @@
|
||||
import { createReducer, createAction } from '@reduxjs/toolkit';
|
||||
import { DayEarning } from '@app-types/index';
|
||||
import { mockWeeklyEarnings, mockTodayEarnings } from '../../mockData/mockEarnings';
|
||||
import { getTodayEarningsThunk, getWeeklyEarningsThunk } from './thunk';
|
||||
|
||||
export interface EarningsState {
|
||||
todayEarnings: number;
|
||||
completedCount: number;
|
||||
onlineMinutes: number;
|
||||
weeklyData: DayEarning[];
|
||||
loading: boolean;
|
||||
error: string | null;
|
||||
}
|
||||
|
||||
const initialState: EarningsState = {
|
||||
todayEarnings: mockTodayEarnings.total,
|
||||
completedCount: mockTodayEarnings.completedCount,
|
||||
onlineMinutes: mockTodayEarnings.onlineMinutes,
|
||||
weeklyData: mockWeeklyEarnings,
|
||||
loading: false,
|
||||
error: null,
|
||||
};
|
||||
|
||||
export const addEarning = createAction<number>('earnings/addEarning');
|
||||
export const setEarnings = createAction<Partial<EarningsState>>('earnings/setEarnings');
|
||||
export const incrementOnlineMinutes = createAction('earnings/incrementOnlineMinutes');
|
||||
export const resetTodayEarnings = createAction('earnings/resetTodayEarnings');
|
||||
|
||||
export const earningsReducer = createReducer(initialState, (builder) => {
|
||||
builder
|
||||
// Sync Actions
|
||||
.addCase(addEarning, (state, action) => {
|
||||
state.todayEarnings += action.payload;
|
||||
state.completedCount += 1;
|
||||
})
|
||||
.addCase(setEarnings, (state, action) => {
|
||||
return { ...state, ...action.payload };
|
||||
})
|
||||
.addCase(incrementOnlineMinutes, (state) => {
|
||||
state.onlineMinutes += 1;
|
||||
})
|
||||
.addCase(resetTodayEarnings, (state) => {
|
||||
state.todayEarnings = 0;
|
||||
state.completedCount = 0;
|
||||
state.onlineMinutes = 0;
|
||||
})
|
||||
// Async Thunks
|
||||
.addCase(getTodayEarningsThunk.pending, (state) => {
|
||||
state.loading = true;
|
||||
state.error = null;
|
||||
})
|
||||
.addCase(getTodayEarningsThunk.fulfilled, (state, action) => {
|
||||
state.loading = false;
|
||||
state.todayEarnings = action.payload.total;
|
||||
state.completedCount = action.payload.completedCount;
|
||||
state.onlineMinutes = action.payload.onlineMinutes;
|
||||
})
|
||||
.addCase(getTodayEarningsThunk.rejected, (state, action) => {
|
||||
state.loading = false;
|
||||
state.error = action.payload as string;
|
||||
})
|
||||
.addCase(getWeeklyEarningsThunk.pending, (state) => {
|
||||
state.loading = true;
|
||||
state.error = null;
|
||||
})
|
||||
.addCase(getWeeklyEarningsThunk.fulfilled, (state, action) => {
|
||||
state.loading = false;
|
||||
state.weeklyData = action.payload;
|
||||
})
|
||||
.addCase(getWeeklyEarningsThunk.rejected, (state, action) => {
|
||||
state.loading = false;
|
||||
state.error = action.payload as string;
|
||||
});
|
||||
});
|
||||
26
app/store/commonReducers/earnings/thunk.ts
Normal file
26
app/store/commonReducers/earnings/thunk.ts
Normal file
@ -0,0 +1,26 @@
|
||||
import { createAsyncThunk } from '@reduxjs/toolkit';
|
||||
import { DayEarning } from '@app-types/index';
|
||||
import * as earningsApi from '../../../api/earningsApi';
|
||||
import { mockTodayEarnings } from '../../mockData/mockEarnings';
|
||||
|
||||
export const getTodayEarningsThunk = createAsyncThunk<typeof mockTodayEarnings, void>(
|
||||
'earnings/getTodayEarnings',
|
||||
async (_, { rejectWithValue }) => {
|
||||
try {
|
||||
return await earningsApi.getTodayEarnings();
|
||||
} catch (error: any) {
|
||||
return rejectWithValue(error.message || 'Failed to get today earnings');
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
export const getWeeklyEarningsThunk = createAsyncThunk<DayEarning[], void>(
|
||||
'earnings/getWeeklyEarnings',
|
||||
async (_, { rejectWithValue }) => {
|
||||
try {
|
||||
return await earningsApi.getWeeklyEarnings();
|
||||
} catch (error: any) {
|
||||
return rejectWithValue(error.message || 'Failed to get weekly earnings');
|
||||
}
|
||||
}
|
||||
);
|
||||
3
app/store/commonReducers/index.ts
Normal file
3
app/store/commonReducers/index.ts
Normal file
@ -0,0 +1,3 @@
|
||||
export * from './auth';
|
||||
export * from './earnings';
|
||||
export * from './job';
|
||||
2
app/store/commonReducers/job/index.ts
Normal file
2
app/store/commonReducers/job/index.ts
Normal file
@ -0,0 +1,2 @@
|
||||
export * from './thunk';
|
||||
export * from './reducer';
|
||||
151
app/store/commonReducers/job/reducer.ts
Normal file
151
app/store/commonReducers/job/reducer.ts
Normal file
@ -0,0 +1,151 @@
|
||||
import { createReducer, createAction } from '@reduxjs/toolkit';
|
||||
import { Job, JobOffer } from '@app-types/index';
|
||||
import { JobStatus } from '@utils/constants';
|
||||
import { acceptJobThunk, rejectJobThunk, confirmPickupThunk, confirmDeliveryThunk } from './thunk';
|
||||
|
||||
export interface JobState {
|
||||
activeJob: Job | null;
|
||||
jobStatus: JobStatus;
|
||||
newJobOffer: JobOffer | null;
|
||||
jobHistory: Job[];
|
||||
loading: boolean;
|
||||
error: string | null;
|
||||
}
|
||||
|
||||
const initialState: JobState = {
|
||||
activeJob: null,
|
||||
jobStatus: JobStatus.Idle,
|
||||
newJobOffer: null,
|
||||
jobHistory: [],
|
||||
loading: false,
|
||||
error: null,
|
||||
};
|
||||
|
||||
export const setNewJobOffer = createAction<JobOffer>('job/setNewJobOffer');
|
||||
export const acceptJobSync = createAction('job/acceptJobSync');
|
||||
export const rejectJobSync = createAction('job/rejectJobSync');
|
||||
export const advanceJobStatus = createAction<JobStatus>('job/advanceJobStatus');
|
||||
export const completeJob = createAction('job/completeJob');
|
||||
export const clearJob = createAction('job/clearJob');
|
||||
export const setJobHistory = createAction<Job[]>('job/setJobHistory');
|
||||
|
||||
export const jobReducer = createReducer(initialState, (builder) => {
|
||||
builder
|
||||
// Sync Actions
|
||||
.addCase(setNewJobOffer, (state, action) => {
|
||||
state.newJobOffer = action.payload;
|
||||
state.jobStatus = JobStatus.NewRequest;
|
||||
})
|
||||
.addCase(acceptJobSync, (state) => {
|
||||
if (state.newJobOffer) {
|
||||
state.activeJob = {
|
||||
...state.newJobOffer,
|
||||
orderId: `#ORD${Date.now().toString().slice(-6)}`,
|
||||
status: JobStatus.Accepted,
|
||||
acceptedAt: new Date().toISOString(),
|
||||
collectAmount: state.newJobOffer.paymentType === 'cod' ? state.newJobOffer.amount : 0,
|
||||
};
|
||||
state.jobStatus = JobStatus.Accepted;
|
||||
state.newJobOffer = null;
|
||||
}
|
||||
})
|
||||
.addCase(rejectJobSync, (state) => {
|
||||
state.newJobOffer = null;
|
||||
state.jobStatus = JobStatus.Idle;
|
||||
})
|
||||
.addCase(advanceJobStatus, (state, action) => {
|
||||
state.jobStatus = action.payload;
|
||||
if (state.activeJob) {
|
||||
state.activeJob.status = action.payload;
|
||||
if (action.payload === JobStatus.PickedUp) {
|
||||
state.activeJob.pickedUpAt = new Date().toISOString();
|
||||
}
|
||||
}
|
||||
})
|
||||
.addCase(completeJob, (state) => {
|
||||
if (state.activeJob) {
|
||||
const completedJob: Job = {
|
||||
...state.activeJob,
|
||||
status: JobStatus.Delivered,
|
||||
deliveredAt: new Date().toISOString(),
|
||||
};
|
||||
state.jobHistory = [completedJob, ...state.jobHistory];
|
||||
}
|
||||
state.activeJob = null;
|
||||
state.jobStatus = JobStatus.Idle;
|
||||
})
|
||||
.addCase(clearJob, (state) => {
|
||||
state.activeJob = null;
|
||||
state.newJobOffer = null;
|
||||
state.jobStatus = JobStatus.Idle;
|
||||
})
|
||||
.addCase(setJobHistory, (state, action) => {
|
||||
state.jobHistory = action.payload;
|
||||
})
|
||||
// Async Thunks
|
||||
.addCase(acceptJobThunk.pending, (state) => {
|
||||
state.loading = true;
|
||||
state.error = null;
|
||||
})
|
||||
.addCase(acceptJobThunk.fulfilled, (state, action) => {
|
||||
state.loading = false;
|
||||
state.activeJob = action.payload;
|
||||
state.jobStatus = JobStatus.Accepted;
|
||||
state.newJobOffer = null;
|
||||
})
|
||||
.addCase(acceptJobThunk.rejected, (state, action) => {
|
||||
state.loading = false;
|
||||
state.error = action.payload as string;
|
||||
})
|
||||
.addCase(rejectJobThunk.pending, (state) => {
|
||||
state.loading = true;
|
||||
state.error = null;
|
||||
})
|
||||
.addCase(rejectJobThunk.fulfilled, (state) => {
|
||||
state.loading = false;
|
||||
state.newJobOffer = null;
|
||||
state.jobStatus = JobStatus.Idle;
|
||||
})
|
||||
.addCase(rejectJobThunk.rejected, (state, action) => {
|
||||
state.loading = false;
|
||||
state.error = action.payload as string;
|
||||
})
|
||||
.addCase(confirmPickupThunk.pending, (state) => {
|
||||
state.loading = true;
|
||||
state.error = null;
|
||||
})
|
||||
.addCase(confirmPickupThunk.fulfilled, (state) => {
|
||||
state.loading = false;
|
||||
state.jobStatus = JobStatus.PickedUp;
|
||||
if (state.activeJob) {
|
||||
state.activeJob.status = JobStatus.PickedUp;
|
||||
state.activeJob.pickedUpAt = new Date().toISOString();
|
||||
}
|
||||
})
|
||||
.addCase(confirmPickupThunk.rejected, (state, action) => {
|
||||
state.loading = false;
|
||||
state.error = action.payload as string;
|
||||
})
|
||||
.addCase(confirmDeliveryThunk.pending, (state) => {
|
||||
state.loading = true;
|
||||
state.error = null;
|
||||
})
|
||||
.addCase(confirmDeliveryThunk.fulfilled, (state) => {
|
||||
state.loading = false;
|
||||
// Complete Job logic can be triggered here or separately
|
||||
if (state.activeJob) {
|
||||
const completedJob: Job = {
|
||||
...state.activeJob,
|
||||
status: JobStatus.Delivered,
|
||||
deliveredAt: new Date().toISOString(),
|
||||
};
|
||||
state.jobHistory = [completedJob, ...state.jobHistory];
|
||||
}
|
||||
state.activeJob = null;
|
||||
state.jobStatus = JobStatus.Idle;
|
||||
})
|
||||
.addCase(confirmDeliveryThunk.rejected, (state, action) => {
|
||||
state.loading = false;
|
||||
state.error = action.payload as string;
|
||||
});
|
||||
});
|
||||
58
app/store/commonReducers/job/thunk.ts
Normal file
58
app/store/commonReducers/job/thunk.ts
Normal file
@ -0,0 +1,58 @@
|
||||
import { createAsyncThunk } from '@reduxjs/toolkit';
|
||||
import { Job } from '@app-types/index';
|
||||
import * as jobApi from '../../../api/jobApi';
|
||||
|
||||
export const acceptJobThunk = createAsyncThunk<Job, string>(
|
||||
'job/acceptJob',
|
||||
async (jobId, { rejectWithValue }) => {
|
||||
try {
|
||||
return await jobApi.acceptJob(jobId);
|
||||
} catch (error: any) {
|
||||
return rejectWithValue(error.message || 'Failed to accept job');
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
export const rejectJobThunk = createAsyncThunk<void, string>(
|
||||
'job/rejectJob',
|
||||
async (jobId, { rejectWithValue }) => {
|
||||
try {
|
||||
return await jobApi.rejectJob(jobId);
|
||||
} catch (error: any) {
|
||||
return rejectWithValue(error.message || 'Failed to reject job');
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
export const confirmPickupThunk = createAsyncThunk<void, string>(
|
||||
'job/confirmPickup',
|
||||
async (jobId, { rejectWithValue }) => {
|
||||
try {
|
||||
return await jobApi.confirmPickup(jobId);
|
||||
} catch (error: any) {
|
||||
return rejectWithValue(error.message || 'Failed to confirm pickup');
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
export const confirmDeliveryThunk = createAsyncThunk<{ earnings: number }, { jobId: string; rating: number }>(
|
||||
'job/confirmDelivery',
|
||||
async ({ jobId, rating }, { rejectWithValue }) => {
|
||||
try {
|
||||
return await jobApi.confirmDelivery(jobId, rating);
|
||||
} catch (error: any) {
|
||||
return rejectWithValue(error.message || 'Failed to confirm delivery');
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
export const reportIssueThunk = createAsyncThunk<void, { jobId: string; reason: string }>(
|
||||
'job/reportIssue',
|
||||
async ({ jobId, reason }, { rejectWithValue }) => {
|
||||
try {
|
||||
return await jobApi.reportIssue(jobId, reason);
|
||||
} catch (error: any) {
|
||||
return rejectWithValue(error.message || 'Failed to report issue');
|
||||
}
|
||||
}
|
||||
);
|
||||
@ -1,36 +1,4 @@
|
||||
import { configureStore } from '@reduxjs/toolkit';
|
||||
import { setupListeners } from '@reduxjs/toolkit/query';
|
||||
import { TypedUseSelectorHook, useDispatch, useSelector } from 'react-redux';
|
||||
import authReducer from './slices/authSlice';
|
||||
import jobReducer from './slices/jobSlice';
|
||||
import earningsReducer from './slices/earningsSlice';
|
||||
import { authApi } from './api/authApi';
|
||||
import { jobApi } from './api/jobApi';
|
||||
import { earningsApi } from './api/earningsApi';
|
||||
|
||||
export const store = configureStore({
|
||||
reducer: {
|
||||
auth: authReducer,
|
||||
job: jobReducer,
|
||||
earnings: earningsReducer,
|
||||
[authApi.reducerPath]: authApi.reducer,
|
||||
[jobApi.reducerPath]: jobApi.reducer,
|
||||
[earningsApi.reducerPath]: earningsApi.reducer,
|
||||
},
|
||||
middleware: (getDefaultMiddleware) =>
|
||||
getDefaultMiddleware({
|
||||
serializableCheck: false,
|
||||
}).concat(
|
||||
authApi.middleware,
|
||||
jobApi.middleware,
|
||||
earningsApi.middleware
|
||||
),
|
||||
});
|
||||
|
||||
setupListeners(store.dispatch);
|
||||
|
||||
export type RootState = ReturnType<typeof store.getState>;
|
||||
export type AppDispatch = typeof store.dispatch;
|
||||
|
||||
export const useAppDispatch = () => useDispatch<AppDispatch>();
|
||||
export const useAppSelector: TypedUseSelectorHook<RootState> = useSelector;
|
||||
export * from './commonReducers';
|
||||
export * from './migration';
|
||||
export * from './rootReducer';
|
||||
export * from './store';
|
||||
|
||||
10
app/store/migration.ts
Normal file
10
app/store/migration.ts
Normal file
@ -0,0 +1,10 @@
|
||||
/* eslint-disable @typescript-eslint/explicit-module-boundary-types */
|
||||
// Here we avoid typing things as the more migrations we have the more complex types we will have to create
|
||||
|
||||
export const migrations = {
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
1: (state: any) => state,
|
||||
};
|
||||
|
||||
const migrationVersions: number[] = Object.keys(migrations).map(k => Number(k));
|
||||
export const persistVersion = migrationVersions[migrationVersions.length - 1];
|
||||
22
app/store/rootReducer.ts
Normal file
22
app/store/rootReducer.ts
Normal file
@ -0,0 +1,22 @@
|
||||
import { combineReducers } from '@reduxjs/toolkit';
|
||||
import { authReducer } from './commonReducers/auth';
|
||||
import { earningsReducer } from './commonReducers/earnings';
|
||||
import { jobReducer } from './commonReducers/job';
|
||||
import setLocationReducer from '@features/screens/setLocationScreen/reducer';
|
||||
import { completeProfileReducer, kycReducer } from '@features/screens';
|
||||
import { onBoardingCompleteReducer } from '@features/screens/onboardingCompleteScreen';
|
||||
import { dashBoardReducer } from '@features/screens/dashboardScreen';
|
||||
|
||||
const rootReducer = combineReducers({
|
||||
auth: authReducer,
|
||||
earnings: earningsReducer,
|
||||
job: jobReducer,
|
||||
setLocation: setLocationReducer,
|
||||
onboard: completeProfileReducer,
|
||||
kyc: kycReducer,
|
||||
onboardingComplete: onBoardingCompleteReducer,
|
||||
dashboard: dashBoardReducer,
|
||||
});
|
||||
|
||||
export type RootState = ReturnType<typeof rootReducer>;
|
||||
export default rootReducer;
|
||||
@ -1,67 +0,0 @@
|
||||
import { createSlice, PayloadAction } from '@reduxjs/toolkit';
|
||||
import { PartnerProfile, PartnerLocation } from '@app-types/index';
|
||||
|
||||
interface AuthState {
|
||||
isAuthenticated: boolean;
|
||||
hasCompletedOnboarding: boolean;
|
||||
isOnline: boolean;
|
||||
token: string | null;
|
||||
partnerId: string | null;
|
||||
partnerProfile: PartnerProfile | null;
|
||||
requestId: string | null; // OTP request ID
|
||||
}
|
||||
|
||||
const initialState: AuthState = {
|
||||
isAuthenticated: false,
|
||||
hasCompletedOnboarding: false,
|
||||
isOnline: false,
|
||||
token: null,
|
||||
partnerId: null,
|
||||
partnerProfile: null,
|
||||
requestId: null,
|
||||
};
|
||||
|
||||
const authSlice = createSlice({
|
||||
name: 'auth',
|
||||
initialState,
|
||||
reducers: {
|
||||
setAuthenticated: (
|
||||
state,
|
||||
action: PayloadAction<{ token: string; partnerId: string }>,
|
||||
) => {
|
||||
state.isAuthenticated = true;
|
||||
state.token = action.payload.token;
|
||||
state.partnerId = action.payload.partnerId;
|
||||
},
|
||||
setRequestId: (state, action: PayloadAction<string>) => {
|
||||
state.requestId = action.payload;
|
||||
},
|
||||
setOnboardingComplete: (state) => {
|
||||
state.hasCompletedOnboarding = true;
|
||||
},
|
||||
setOnlineStatus: (state, action: PayloadAction<boolean>) => {
|
||||
state.isOnline = action.payload;
|
||||
},
|
||||
setProfile: (state, action: PayloadAction<PartnerProfile>) => {
|
||||
state.partnerProfile = action.payload;
|
||||
},
|
||||
setLocation: (state, action: PayloadAction<PartnerLocation>) => {
|
||||
if (state.partnerProfile) {
|
||||
state.partnerProfile.location = action.payload;
|
||||
}
|
||||
},
|
||||
logout: () => initialState,
|
||||
},
|
||||
});
|
||||
|
||||
export const {
|
||||
setAuthenticated,
|
||||
setRequestId,
|
||||
setOnboardingComplete,
|
||||
setOnlineStatus,
|
||||
setProfile,
|
||||
setLocation,
|
||||
logout,
|
||||
} = authSlice.actions;
|
||||
|
||||
export default authSlice.reducer;
|
||||
@ -1,48 +0,0 @@
|
||||
import { createSlice, PayloadAction } from '@reduxjs/toolkit';
|
||||
import { DayEarning } from '@app-types/index';
|
||||
import { mockWeeklyEarnings, mockTodayEarnings } from '@store/mockData/mockEarnings';
|
||||
|
||||
interface EarningsState {
|
||||
todayEarnings: number;
|
||||
completedCount: number;
|
||||
onlineMinutes: number;
|
||||
weeklyData: DayEarning[];
|
||||
}
|
||||
|
||||
const initialState: EarningsState = {
|
||||
todayEarnings: mockTodayEarnings.total,
|
||||
completedCount: mockTodayEarnings.completedCount,
|
||||
onlineMinutes: mockTodayEarnings.onlineMinutes,
|
||||
weeklyData: mockWeeklyEarnings,
|
||||
};
|
||||
|
||||
const earningsSlice = createSlice({
|
||||
name: 'earnings',
|
||||
initialState,
|
||||
reducers: {
|
||||
addEarning: (state, action: PayloadAction<number>) => {
|
||||
state.todayEarnings += action.payload;
|
||||
state.completedCount += 1;
|
||||
},
|
||||
setEarnings: (state, action: PayloadAction<Partial<EarningsState>>) => {
|
||||
return { ...state, ...action.payload };
|
||||
},
|
||||
incrementOnlineMinutes: (state) => {
|
||||
state.onlineMinutes += 1;
|
||||
},
|
||||
resetTodayEarnings: (state) => {
|
||||
state.todayEarnings = 0;
|
||||
state.completedCount = 0;
|
||||
state.onlineMinutes = 0;
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
export const {
|
||||
addEarning,
|
||||
setEarnings,
|
||||
incrementOnlineMinutes,
|
||||
resetTodayEarnings,
|
||||
} = earningsSlice.actions;
|
||||
|
||||
export default earningsSlice.reducer;
|
||||
@ -1,86 +0,0 @@
|
||||
import { createSlice, PayloadAction } from '@reduxjs/toolkit';
|
||||
import { Job, JobOffer } from '@app-types/index';
|
||||
import { JobStatus } from '@utils/constants';
|
||||
|
||||
interface JobState {
|
||||
activeJob: Job | null;
|
||||
jobStatus: JobStatus;
|
||||
newJobOffer: JobOffer | null;
|
||||
jobHistory: Job[];
|
||||
}
|
||||
|
||||
const initialState: JobState = {
|
||||
activeJob: null,
|
||||
jobStatus: JobStatus.Idle,
|
||||
newJobOffer: null,
|
||||
jobHistory: [],
|
||||
};
|
||||
|
||||
const jobSlice = createSlice({
|
||||
name: 'job',
|
||||
initialState,
|
||||
reducers: {
|
||||
setNewJobOffer: (state, action: PayloadAction<JobOffer>) => {
|
||||
state.newJobOffer = action.payload;
|
||||
state.jobStatus = JobStatus.NewRequest;
|
||||
},
|
||||
acceptJob: (state) => {
|
||||
if (state.newJobOffer) {
|
||||
state.activeJob = {
|
||||
...state.newJobOffer,
|
||||
orderId: `#ORD${Date.now().toString().slice(-6)}`,
|
||||
status: JobStatus.Accepted,
|
||||
acceptedAt: new Date().toISOString(),
|
||||
collectAmount: state.newJobOffer.paymentType === 'cod' ? state.newJobOffer.amount : 0,
|
||||
};
|
||||
state.jobStatus = JobStatus.Accepted;
|
||||
state.newJobOffer = null;
|
||||
}
|
||||
},
|
||||
rejectJob: (state) => {
|
||||
state.newJobOffer = null;
|
||||
state.jobStatus = JobStatus.Idle;
|
||||
},
|
||||
advanceJobStatus: (state, action: PayloadAction<JobStatus>) => {
|
||||
state.jobStatus = action.payload;
|
||||
if (state.activeJob) {
|
||||
state.activeJob.status = action.payload;
|
||||
if (action.payload === JobStatus.PickedUp) {
|
||||
state.activeJob.pickedUpAt = new Date().toISOString();
|
||||
}
|
||||
}
|
||||
},
|
||||
completeJob: (state) => {
|
||||
if (state.activeJob) {
|
||||
const completedJob: Job = {
|
||||
...state.activeJob,
|
||||
status: JobStatus.Delivered,
|
||||
deliveredAt: new Date().toISOString(),
|
||||
};
|
||||
state.jobHistory = [completedJob, ...state.jobHistory];
|
||||
}
|
||||
state.activeJob = null;
|
||||
state.jobStatus = JobStatus.Idle;
|
||||
},
|
||||
clearJob: (state) => {
|
||||
state.activeJob = null;
|
||||
state.newJobOffer = null;
|
||||
state.jobStatus = JobStatus.Idle;
|
||||
},
|
||||
setJobHistory: (state, action: PayloadAction<Job[]>) => {
|
||||
state.jobHistory = action.payload;
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
export const {
|
||||
setNewJobOffer,
|
||||
acceptJob,
|
||||
rejectJob,
|
||||
advanceJobStatus,
|
||||
completeJob,
|
||||
clearJob,
|
||||
setJobHistory,
|
||||
} = jobSlice.actions;
|
||||
|
||||
export default jobSlice.reducer;
|
||||
53
app/store/store.ts
Normal file
53
app/store/store.ts
Normal file
@ -0,0 +1,53 @@
|
||||
import { useDispatch, useSelector, TypedUseSelectorHook } from 'react-redux';
|
||||
import AsyncStorage from '@react-native-async-storage/async-storage';
|
||||
import {
|
||||
createMigrate,
|
||||
FLUSH,
|
||||
PAUSE,
|
||||
PERSIST,
|
||||
persistReducer,
|
||||
persistStore,
|
||||
PURGE,
|
||||
REGISTER,
|
||||
REHYDRATE,
|
||||
} from 'redux-persist';
|
||||
import { ThunkAction } from 'redux-thunk';
|
||||
import { Action, configureStore } from '@reduxjs/toolkit';
|
||||
import { migrations, persistVersion } from './migration';
|
||||
import rootReducer, { RootState } from './rootReducer';
|
||||
import { injectStore } from '../services';
|
||||
|
||||
const persistConfig = {
|
||||
key: 'root',
|
||||
version: persistVersion,
|
||||
storage: AsyncStorage,
|
||||
blacklist: [],
|
||||
migrate: createMigrate(migrations, { debug: __DEV__ }),
|
||||
};
|
||||
|
||||
const persistedReducer = persistReducer(persistConfig, rootReducer);
|
||||
|
||||
export const store = configureStore({
|
||||
reducer: persistedReducer,
|
||||
devTools: __DEV__,
|
||||
middleware: getDefaultMiddleware =>
|
||||
getDefaultMiddleware({
|
||||
immutableCheck: false,
|
||||
serializableCheck: {
|
||||
ignoredActions: [FLUSH, REHYDRATE, PAUSE, PERSIST, PURGE, REGISTER],
|
||||
},
|
||||
}),
|
||||
});
|
||||
|
||||
injectStore(store);
|
||||
|
||||
export const persistor = persistStore(store);
|
||||
|
||||
export type AppDispatch = typeof store.dispatch;
|
||||
|
||||
export const useAppDispatch = (): AppDispatch => useDispatch<AppDispatch>();
|
||||
export const useAppSelector: TypedUseSelectorHook<RootState> = useSelector;
|
||||
|
||||
export type AppThunk = ThunkAction<void, RootState, unknown, Action<string>>;
|
||||
|
||||
export default store;
|
||||
@ -29,6 +29,7 @@ export enum RouteNames {
|
||||
DeliverOrder = 'DeliverOrder',
|
||||
DeliveryCompleted = 'DeliveryCompleted',
|
||||
Documents = 'Documents',
|
||||
Kyc = 'Kyc',
|
||||
}
|
||||
|
||||
export enum JobStatus {
|
||||
|
||||
@ -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',
|
||||
},
|
||||
},
|
||||
],
|
||||
|
||||
4
index.js
4
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);
|
||||
|
||||
20
package.json
20
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",
|
||||
|
||||
@ -13,12 +13,17 @@
|
||||
"@navigation/*": ["./app/navigation/*"],
|
||||
"@store": ["./app/store"],
|
||||
"@store/*": ["./app/store/*"],
|
||||
"@services": ["./app/services"],
|
||||
"@services/*": ["./app/services/*"],
|
||||
"@hooks/*": ["./app/hooks/*"],
|
||||
"@utils/*": ["./app/utils/*"],
|
||||
"@icons": ["./app/components/icons"],
|
||||
"@icons/*": ["./app/components/icons/*"],
|
||||
"@app-types/*": ["./app/types/*"]
|
||||
"@app-types/*": ["./app/types/*"],
|
||||
"@interfaces": ["./app/interfaces"],
|
||||
"@interfaces/*": ["./app/interfaces/*"],
|
||||
"@api": ["./app/api"],
|
||||
"@api/*": ["./app/api/*"]
|
||||
}
|
||||
},
|
||||
"include": ["**/*.ts", "**/*.tsx"],
|
||||
|
||||
268
yarn.lock
268
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==
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user