feat(app): implement core application structure with authentication, delivery flows, map services, and state management

This commit is contained in:
Tamojit Biswas 2026-07-03 13:53:18 +05:30
parent 8f6e162e5e
commit 44f6050e60
72 changed files with 4390 additions and 1435 deletions

View File

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

View File

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

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

@ -0,0 +1,42 @@
import { apiClient } from '../services/apiClient';
import { User } from '../interfaces';
// ─── Types ───────────────────────────────────────────────────────────────────
export interface LoginResponse {
success: boolean;
message: string;
}
export interface VerifyOtpResponse {
success: boolean;
user: User;
accessToken: string;
refreshToken: string;
}
export interface UpdateProfileResponse {
success: boolean;
user: User;
}
export interface LogoutResponse {
success: boolean;
}
// ─── Endpoints ───────────────────────────────────────────────────────────────
export const loginApi = (mobileNumber: string) =>
apiClient.post<LoginResponse>('/auth/login', { mobileNumber });
export const verifyOtpApi = (mobileNumber: string, otp: string) =>
apiClient.post<VerifyOtpResponse>('/auth/verify-otp', { mobileNumber, otp });
export const updateProfileApi = (data: {
name: string;
email: string;
locationLabels: string[];
}) => apiClient.put<UpdateProfileResponse>('/auth/profile', data);
export const logoutApi = () =>
apiClient.post<LogoutResponse>('/auth/logout', {});

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

@ -0,0 +1,29 @@
import { apiClient } from '../services/apiClient';
import { Provider, CatalogItem, Order, DeliveryAgent, Location } from '../interfaces';
// ─── Types ───────────────────────────────────────────────────────────────────
export interface PlaceOrderResponse {
success: boolean;
order: Order;
}
// ─── Endpoints ───────────────────────────────────────────────────────────────
export const getProvidersApi = () =>
apiClient.get<Provider[]>('/providers');
export const getProviderCatalogApi = (providerId: string) =>
apiClient.get<CatalogItem[]>(`/providers/${providerId}/catalog`);
export const searchProvidersApi = (query: string) =>
apiClient.get<Provider[]>(`/search?q=${query}`);
export const placeOrderApi = (orderData: Partial<Order>) =>
apiClient.post<PlaceOrderResponse>('/orders', orderData);
export const getDeliveryAgentApi = () =>
apiClient.get<DeliveryAgent>('/delivery/agent');
export const getLiveLocationApi = () =>
apiClient.get<Location>('/delivery/live-location');

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

@ -0,0 +1,2 @@
export * from './authApi';
export * from './deliveryApi';

View File

@ -1,9 +1,15 @@
import { TextInputProps } from 'react-native'; import { TextInputProps, ViewStyle, StyleProp } from 'react-native';
export interface CustomInputProps extends TextInputProps { export interface CustomInputProps extends TextInputProps {
label: string; label?: string;
isPassword?: boolean; isPassword?: boolean;
rightActionText?: string; rightActionText?: string;
onRightActionPress?: () => void; onRightActionPress?: () => void;
error?: string; error?: string;
style?: StyleProp<ViewStyle>;
/**
* Optional element rendered at the start of the input, before the
* TextInput itself e.g. a country-code chip ("+91") or an icon.
*/
leftElement?: React.ReactNode;
} }

View File

@ -1,49 +1,56 @@
import { StyleSheet } from 'react-native'; import { StyleSheet } from 'react-native';
import { typography } from '@theme'; import { typography } from '@theme';
export const getStyles = (colors: any) => StyleSheet.create({ export const getStyles = (colors: any) =>
container: { StyleSheet.create({
marginBottom: 20, container: {
width: '100%', marginBottom: 20,
}, width: '100%',
labelRow: { },
flexDirection: 'row', labelRow: {
justifyContent: 'space-between', flexDirection: 'row',
alignItems: 'center', justifyContent: 'space-between',
marginBottom: 8, alignItems: 'center',
}, marginBottom: 8,
label: { },
fontSize: typography.fontSize.sm, label: {
fontWeight: typography.fontWeight.medium, fontSize: typography.fontSize.sm,
color: colors.textSecondary, fontWeight: typography.fontWeight.medium,
}, color: colors.textSecondary,
rightAction: { },
fontSize: typography.fontSize.sm, rightAction: {
fontWeight: typography.fontWeight.semibold, fontSize: typography.fontSize.sm,
color: colors.primary, fontWeight: typography.fontWeight.semibold,
}, color: colors.primary,
inputContainer: { },
flexDirection: 'row', inputContainer: {
alignItems: 'center', flexDirection: 'row',
borderWidth: 1, alignItems: 'center',
borderColor: colors.border, borderWidth: 1,
borderRadius: 12, borderColor: colors.border,
backgroundColor: colors.inputBg, borderRadius: 12,
paddingHorizontal: 16, backgroundColor: colors.inputBg,
height: 56, paddingHorizontal: 16,
}, height: 56,
input: { },
flex: 1, leftElementWrap: {
height: '100%', marginRight: 10,
color: colors.text, paddingRight: 10,
fontSize: typography.fontSize.md, borderRightWidth: 1,
fontWeight: typography.fontWeight.regular, borderRightColor: colors.border,
paddingVertical: 0, },
}, input: {
errorText: { flex: 1,
color: colors.error, height: '100%',
fontSize: typography.fontSize.xs, color: colors.text,
marginTop: 4, fontSize: typography.fontSize.md,
marginLeft: 4, fontWeight: typography.fontWeight.regular,
}, paddingVertical: 0,
}); },
errorText: {
color: colors.error,
fontSize: typography.fontSize.xs,
marginTop: 4,
marginLeft: 4,
},
});

View File

@ -1,7 +1,7 @@
import React, { useState } from 'react'; import React, { useState } from 'react';
import { View, Text, TextInput, TouchableOpacity } from 'react-native'; import { View, Text, TextInput, TouchableOpacity } from 'react-native';
import { CustomInputProps } from './customInput.props'; import { CustomInputProps } from './CustomInput.props';
import { getStyles } from './customInput.styles'; import { getStyles } from './CustomInput.styles';
import { useAppTheme } from '@theme'; import { useAppTheme } from '@theme';
export const CustomInput: React.FC<CustomInputProps> = ({ export const CustomInput: React.FC<CustomInputProps> = ({
@ -12,6 +12,7 @@ export const CustomInput: React.FC<CustomInputProps> = ({
error, error,
secureTextEntry, secureTextEntry,
style, style,
leftElement,
...rest ...rest
}) => { }) => {
const { colors } = useAppTheme(); const { colors } = useAppTheme();
@ -25,7 +26,16 @@ export const CustomInput: React.FC<CustomInputProps> = ({
<Text style={styles.label}>{label}</Text> <Text style={styles.label}>{label}</Text>
</View> </View>
)} )}
<View style={[styles.inputContainer, error ? { borderColor: colors.error } : null, style]}> <View
style={[
styles.inputContainer,
error ? { borderColor: colors.error } : null,
style,
]}
>
{leftElement && (
<View style={styles.leftElementWrap}>{leftElement}</View>
)}
<TextInput <TextInput
style={styles.input} style={styles.input}
placeholderTextColor={colors.placeholder} placeholderTextColor={colors.placeholder}
@ -36,8 +46,13 @@ export const CustomInput: React.FC<CustomInputProps> = ({
/> />
<View style={{ flexDirection: 'row', alignItems: 'center', gap: 12 }}> <View style={{ flexDirection: 'row', alignItems: 'center', gap: 12 }}>
{isPassword && ( {isPassword && (
<TouchableOpacity onPress={() => setIsSecure(!isSecure)} activeOpacity={0.7}> <TouchableOpacity
<Text style={styles.rightAction}>{isSecure ? 'Show' : 'Hide'}</Text> onPress={() => setIsSecure(!isSecure)}
activeOpacity={0.7}
>
<Text style={styles.rightAction}>
{isSecure ? 'Show' : 'Hide'}
</Text>
</TouchableOpacity> </TouchableOpacity>
)} )}
{rightActionText && onRightActionPress && ( {rightActionText && onRightActionPress && (

View File

@ -1,2 +1,2 @@
export * from './customInput'; export * from './CustomInput';
export * from './customInput.props'; export * from './CustomInput.props';

View File

@ -1,7 +1,7 @@
import React from 'react'; import React from 'react';
import { TouchableOpacity, Text, ActivityIndicator } from 'react-native'; import { TouchableOpacity, Text, ActivityIndicator } from 'react-native';
import { PrimaryButtonProps } from './primaryButton.props'; import { PrimaryButtonProps } from './PrimaryButton.props';
import { styles } from './primaryButton.styles'; import { styles } from './PrimaryButton.styles';
import { colors } from '@theme'; import { colors } from '@theme';
export const PrimaryButton: React.FC<PrimaryButtonProps> = ({ export const PrimaryButton: React.FC<PrimaryButtonProps> = ({

View File

@ -1,2 +1,2 @@
export * from './primaryButton'; export * from './PrimaryButton';
export * from './primaryButton.props'; export * from './PrimaryButton.props';

View File

@ -1,7 +1,7 @@
import React from 'react'; import React from 'react';
import { TouchableOpacity, Text } from 'react-native'; import { TouchableOpacity, Text } from 'react-native';
import { SocialButtonProps } from './socialButton.props'; import { SocialButtonProps } from './socialButton.props';
import { getStyles } from './socialButton.styles'; import { getStyles } from './SocialButton.styles';
import { useAppTheme } from '@theme'; import { useAppTheme } from '@theme';
import Svg, { Path } from 'react-native-svg'; import Svg, { Path } from 'react-native-svg';
@ -26,7 +26,7 @@ const GoogleIcon = ({ style }: { style: any }) => (
</Svg> </Svg>
); );
const AppleIcon = ({ style, color }: { style: any, color: string }) => ( const AppleIcon = ({ style, color }: { style: any; color: string }) => (
<Svg width={24} height={24} viewBox="0 0 24 24" style={style}> <Svg width={24} height={24} viewBox="0 0 24 24" style={style}>
<Path <Path
fill={color} fill={color}

View File

@ -1,2 +1,2 @@
export * from './socialButton'; export * from './SocialButton';
export * from './socialButton.props'; export * from './SocialButton.props';

View File

@ -1,6 +1,6 @@
export * from './customInput'; export * from './CustomInput';
export * from './primaryButton'; export * from './PrimaryButton';
export * from './socialButton'; export * from './SocialButton';
export * from './header'; export * from './header';
export * from './searchBar'; export * from './searchBar';
export * from './providerCard'; export * from './providerCard';

View File

@ -1,9 +1,9 @@
export interface ProviderCardProps { export interface ProviderCardProps {
imageUrl: string; imageUrl?: string;
name: string; name: string;
rating: number; rating: number;
deliveryTime: string; deliveryTime: string;
tag: string; tag: string;
discountText?: string; discountText?: string;
onPress?: () => void; onPress: () => void;
} }

View File

@ -1,69 +1,143 @@
import { StyleSheet } from 'react-native'; import { StyleSheet, Platform } from 'react-native';
import { typography } from '@theme'; import { typography } from '@theme';
export const getStyles = (colors: any) => StyleSheet.create({ export const getStyles = (colors: any) =>
container: { StyleSheet.create({
flexDirection: 'row', container: {
backgroundColor: colors.cardBg, backgroundColor: colors.cardBg,
borderRadius: 12, borderRadius: 18,
padding: 12, marginHorizontal: 16,
marginHorizontal: 16, marginVertical: 8,
marginVertical: 6, overflow: 'hidden',
borderWidth: 1, ...Platform.select({
borderColor: colors.border, ios: {
}, shadowColor: '#000',
image: { shadowOffset: { width: 0, height: 3 },
width: 80, shadowOpacity: 0.06,
height: 80, shadowRadius: 10,
borderRadius: 8, },
backgroundColor: colors.border, android: { elevation: 2 },
justifyContent: 'center', }),
alignItems: 'center', },
},
imagePlaceholder: { // ---------- Image ----------
fontSize: 32, imageWrap: {
}, width: '100%',
info: { height: 150,
flex: 1, position: 'relative',
marginLeft: 12, backgroundColor: colors.border,
justifyContent: 'center', },
}, image: {
name: { width: '100%',
fontSize: typography.fontSize.md, height: '100%',
fontWeight: typography.fontWeight.semibold, },
color: colors.text, imagePlaceholder: {
marginBottom: 4, width: '100%',
}, height: '100%',
row: { alignItems: 'center',
flexDirection: 'row', justifyContent: 'center',
alignItems: 'center', backgroundColor: colors.border,
marginBottom: 2, },
}, imagePlaceholderEmoji: {
rating: { fontSize: 34,
fontSize: typography.fontSize.sm, },
color: colors.textSecondary,
marginRight: 8, discountRibbon: {
}, position: 'absolute',
deliveryTime: { top: 12,
fontSize: typography.fontSize.sm, left: 0,
color: colors.textSecondary, backgroundColor: colors.primary,
}, paddingVertical: 4,
tag: { paddingHorizontal: 10,
fontSize: typography.fontSize.xs, borderTopRightRadius: 8,
color: colors.primary, borderBottomRightRadius: 8,
fontWeight: typography.fontWeight.medium, },
}, discountRibbonText: {
discountBadge: { color: '#FFFFFF',
alignSelf: 'flex-start', fontSize: typography.fontSize.xs,
backgroundColor: '#FFE7E7', fontWeight: typography.fontWeight.bold,
paddingHorizontal: 8, },
paddingVertical: 2,
borderRadius: 4, favoriteButton: {
marginTop: 4, position: 'absolute',
}, top: 10,
discountText: { right: 10,
fontSize: typography.fontSize.xs, width: 32,
color: '#D32F2F', height: 32,
fontWeight: typography.fontWeight.semibold, borderRadius: 16,
}, backgroundColor: 'rgba(255,255,255,0.92)',
}); alignItems: 'center',
justifyContent: 'center',
},
favoriteIcon: {
fontSize: 15,
},
// ---------- Body ----------
body: {
padding: 14,
},
topRow: {
flexDirection: 'row',
justifyContent: 'space-between',
alignItems: 'flex-start',
},
name: {
fontSize: typography.fontSize.md,
fontWeight: typography.fontWeight.bold,
color: colors.text,
flex: 1,
marginRight: 8,
},
ratingPill: {
flexDirection: 'row',
alignItems: 'center',
backgroundColor: colors.primaryMuted ?? '#E9F7EF',
borderRadius: 8,
paddingHorizontal: 6,
paddingVertical: 3,
},
ratingStar: {
fontSize: 11,
marginRight: 3,
},
ratingText: {
fontSize: typography.fontSize.xs,
fontWeight: typography.fontWeight.bold,
color: colors.primary,
},
metaRow: {
flexDirection: 'row',
alignItems: 'center',
marginTop: 8,
},
metaText: {
fontSize: typography.fontSize.sm,
color: colors.textSecondary,
},
metaDivider: {
width: 3,
height: 3,
borderRadius: 1.5,
backgroundColor: colors.textSecondary,
marginHorizontal: 8,
opacity: 0.5,
},
tagPill: {
alignSelf: 'flex-start',
backgroundColor: colors.background,
borderRadius: 6,
paddingHorizontal: 8,
paddingVertical: 3,
marginTop: 10,
borderWidth: 1,
borderColor: colors.border,
},
tagPillText: {
fontSize: typography.fontSize.xs,
color: colors.textSecondary,
fontWeight: typography.fontWeight.medium,
},
});

View File

@ -1,5 +1,5 @@
import React from 'react'; import React, { useState } from 'react';
import { View, Text, TouchableOpacity } from 'react-native'; import { View, Text, Image, TouchableOpacity } from 'react-native';
import { ProviderCardProps } from './providerCard.props'; import { ProviderCardProps } from './providerCard.props';
import { getStyles } from './providerCard.styles'; import { getStyles } from './providerCard.styles';
import { useAppTheme } from '@theme'; import { useAppTheme } from '@theme';
@ -15,28 +15,62 @@ export const ProviderCard: React.FC<ProviderCardProps> = ({
}) => { }) => {
const { colors } = useAppTheme(); const { colors } = useAppTheme();
const styles = getStyles(colors); const styles = getStyles(colors);
const [isFavorite, setIsFavorite] = useState(false);
return ( return (
<TouchableOpacity <TouchableOpacity
style={styles.container} style={styles.container}
onPress={onPress} onPress={onPress}
activeOpacity={0.8} activeOpacity={0.85}
> >
<View style={styles.image}> <View style={styles.imageWrap}>
<Text style={styles.imagePlaceholder}>{imageUrl || '🍽️'}</Text> {imageUrl ? (
</View> <Image
<View style={styles.info}> source={{ uri: imageUrl }}
<Text style={styles.name} numberOfLines={1}>{name}</Text> style={styles.image}
<View style={styles.row}> resizeMode="cover"
<Text style={styles.rating}> {rating.toFixed(1)}</Text> />
<Text style={styles.deliveryTime}>🕐 {deliveryTime}</Text> ) : (
</View> <View style={styles.imagePlaceholder}>
<Text style={styles.tag}>{tag}</Text> <Text style={styles.imagePlaceholderEmoji}>🍽</Text>
{discountText && (
<View style={styles.discountBadge}>
<Text style={styles.discountText}>{discountText}</Text>
</View> </View>
)} )}
{!!discountText && (
<View style={styles.discountRibbon}>
<Text style={styles.discountRibbonText}>{discountText}</Text>
</View>
)}
<TouchableOpacity
style={styles.favoriteButton}
activeOpacity={0.7}
onPress={() => setIsFavorite(prev => !prev)}
>
<Text style={styles.favoriteIcon}>{isFavorite ? '❤️' : '🤍'}</Text>
</TouchableOpacity>
</View>
<View style={styles.body}>
<View style={styles.topRow}>
<Text style={styles.name} numberOfLines={1}>
{name}
</Text>
<View style={styles.ratingPill}>
<Text style={styles.ratingStar}></Text>
<Text style={styles.ratingText}>{rating.toFixed(1)}</Text>
</View>
</View>
<View style={styles.metaRow}>
<Text style={styles.metaText}>🕐 {deliveryTime}</Text>
<View style={styles.metaDivider} />
<Text style={styles.metaText}>{tag}</Text>
</View>
<View style={styles.tagPill}>
<Text style={styles.tagPillText}>Free delivery over 199</Text>
</View>
</View> </View>
</TouchableOpacity> </TouchableOpacity>
); );

View File

@ -1,32 +1,142 @@
import { StyleSheet } from 'react-native'; import { StyleSheet, Dimensions, Platform } from 'react-native';
import { typography } from '@theme'; import { typography } from '@theme';
export const getStyles = (colors: any) => StyleSheet.create({ const { width } = Dimensions.get('window');
container: {
flex: 1, export const getStyles = (colors: any) =>
backgroundColor: colors.background, StyleSheet.create({
paddingHorizontal: 24, container: {
}, flex: 1,
scrollContainer: { backgroundColor: colors.primary,
flexGrow: 1, },
justifyContent: 'center', scrollContainer: {
paddingBottom: 24, flexGrow: 1,
}, },
headerContainer: {
marginTop: 40, // ---------- Hero ----------
marginBottom: 32, hero: {
}, backgroundColor: colors.primary,
title: { paddingTop: Platform.OS === 'ios' ? 70 : 48,
fontSize: typography.fontSize.xxl, paddingBottom: 56,
fontWeight: typography.fontWeight.bold, alignItems: 'center',
color: colors.text, overflow: 'hidden',
marginBottom: 8, },
}, heroDecoCircleLg: {
subtitle: { position: 'absolute',
fontSize: typography.fontSize.md, width: 220,
color: colors.textSecondary, height: 220,
}, borderRadius: 110,
formContainer: { backgroundColor: 'rgba(255,255,255,0.06)',
width: '100%', top: -100,
}, right: -60,
}); },
heroDecoCircleSm: {
position: 'absolute',
width: 120,
height: 120,
borderRadius: 60,
backgroundColor: 'rgba(255,255,255,0.08)',
top: 30,
left: -50,
},
logoBadge: {
width: 68,
height: 68,
borderRadius: 20,
backgroundColor: 'rgba(255,255,255,0.95)',
alignItems: 'center',
justifyContent: 'center',
marginBottom: 16,
...Platform.select({
ios: {
shadowColor: '#000',
shadowOffset: { width: 0, height: 4 },
shadowOpacity: 0.15,
shadowRadius: 10,
},
android: { elevation: 4 },
}),
},
logoEmoji: {
fontSize: 32,
},
brandName: {
fontSize: typography.fontSize.xl,
fontWeight: typography.fontWeight.bold,
color: '#FFFFFF',
marginBottom: 4,
},
brandTagline: {
fontSize: typography.fontSize.sm,
color: 'rgba(255,255,255,0.85)',
},
// ---------- Form card ----------
formCard: {
flex: 1,
backgroundColor: colors.background,
borderTopLeftRadius: 32,
borderTopRightRadius: 32,
marginTop: -28,
paddingTop: 32,
paddingHorizontal: 24,
paddingBottom: 24,
},
headerContainer: {
marginBottom: 28,
},
title: {
fontSize: typography.fontSize.xxl,
fontWeight: typography.fontWeight.bold,
color: colors.text,
marginBottom: 8,
},
subtitle: {
fontSize: typography.fontSize.md,
color: colors.textSecondary,
},
formContainer: {
width: '100%',
},
// Phone prefix chip rendered via CustomInput's leftElement
prefixChip: {
flexDirection: 'row',
alignItems: 'center',
},
prefixFlag: {
fontSize: 16,
marginRight: 6,
},
prefixText: {
fontSize: typography.fontSize.md,
fontWeight: typography.fontWeight.semibold,
color: colors.text,
},
helperText: {
fontSize: typography.fontSize.xs,
color: colors.textSecondary,
marginTop: -12,
marginBottom: 20,
marginLeft: 4,
},
// ---------- Footer ----------
footerSpacer: {
flex: 1,
minHeight: 24,
},
termsText: {
fontSize: typography.fontSize.xs,
color: colors.textSecondary,
textAlign: 'center',
lineHeight: 18,
marginTop: 20,
paddingHorizontal: 8,
},
termsLink: {
color: colors.primary,
fontWeight: typography.fontWeight.semibold,
},
});

View File

@ -1,4 +1,4 @@
import React, { useState } from 'react'; import React from 'react';
import { import {
View, View,
Text, Text,
@ -6,39 +6,17 @@ import {
Platform, Platform,
ScrollView, ScrollView,
} from 'react-native'; } from 'react-native';
import { useNavigation } from '@react-navigation/native';
import { StackNavigationProp } from '@react-navigation/stack';
import { getStyles } from './loginScreen.styles'; import { getStyles } from './loginScreen.styles';
import { CustomInput, PrimaryButton } from '@components'; import { CustomInput, PrimaryButton } from '@components';
import { useAppTheme } from '@theme'; import { useAppTheme } from '@theme';
import { useAppDispatch } from '../../../hooks/useAppDispatch'; import { useLoginScreen } from '../../../hooks';
import { loginWithPhone } from '../../../store/authSlice';
import { AuthStackParamList } from '../../../navigation/authStack';
type LoginNavProp = StackNavigationProp<AuthStackParamList, 'LoginScreen'>;
export const LoginScreen: React.FC = () => { export const LoginScreen: React.FC = () => {
const { colors } = useAppTheme(); const { colors } = useAppTheme();
const styles = getStyles(colors); const styles = getStyles(colors);
const navigation = useNavigation<LoginNavProp>();
const dispatch = useAppDispatch();
const [mobileNumber, setMobileNumber] = useState(''); const { mobileNumber, error, onChangeMobileNumber, handleLogin } =
const [error, setError] = useState<string | undefined>(undefined); useLoginScreen();
const handleLogin = () => {
if (!mobileNumber) {
setError('Mobile number is required');
return;
}
if (mobileNumber.length < 10) {
setError('Please enter a valid mobile number');
return;
}
setError(undefined);
dispatch(loginWithPhone(mobileNumber));
navigation.navigate('OtpScreen', { mobileNumber });
};
return ( return (
<KeyboardAvoidingView <KeyboardAvoidingView
@ -48,30 +26,62 @@ export const LoginScreen: React.FC = () => {
<ScrollView <ScrollView
contentContainerStyle={styles.scrollContainer} contentContainerStyle={styles.scrollContainer}
showsVerticalScrollIndicator={false} showsVerticalScrollIndicator={false}
bounces={false}
> >
<View style={styles.headerContainer}> {/* Hero / branding */}
<Text style={styles.title}>Welcome back</Text> <View style={styles.hero}>
<Text style={styles.subtitle}>Login to continue</Text> <View style={styles.heroDecoCircleLg} />
<View style={styles.heroDecoCircleSm} />
<View style={styles.logoBadge}>
<Text style={styles.logoEmoji}>🛵</Text>
</View>
<Text style={styles.brandName}>QuickCart</Text>
<Text style={styles.brandTagline}>Delivered fast, every time</Text>
</View> </View>
<View style={styles.formContainer}> {/* Form card */}
<CustomInput <View style={styles.formCard}>
label="Mobile number" <View style={styles.headerContainer}>
placeholder="+91 98765 43210" <Text style={styles.title}>Welcome back</Text>
keyboardType="phone-pad" <Text style={styles.subtitle}>Login to continue</Text>
value={mobileNumber} </View>
onChangeText={(text) => {
setMobileNumber(text);
if (error) setError(undefined);
}}
error={error}
/>
<PrimaryButton <View style={styles.formContainer}>
title="Login" <CustomInput
onPress={handleLogin} label="Mobile number"
style={{ marginTop: 12 }} placeholder="98765 43210"
/> keyboardType="phone-pad"
maxLength={10}
value={mobileNumber}
onChangeText={onChangeMobileNumber}
error={error}
leftElement={
<View style={styles.prefixChip}>
<Text style={styles.prefixFlag}>🇮🇳</Text>
<Text style={styles.prefixText}>+91</Text>
</View>
}
/>
{!error && (
<Text style={styles.helperText}>
We'll send a one-time password to verify this number
</Text>
)}
<PrimaryButton
title="Get OTP"
onPress={handleLogin}
style={{ marginTop: 12 }}
/>
</View>
<View style={styles.footerSpacer} />
<Text style={styles.termsText}>
By continuing, you agree to our{' '}
<Text style={styles.termsLink}>Terms of Service</Text> and{' '}
<Text style={styles.termsLink}>Privacy Policy</Text>
</Text>
</View> </View>
</ScrollView> </ScrollView>
</KeyboardAvoidingView> </KeyboardAvoidingView>

View File

@ -1,68 +1,224 @@
import { StyleSheet } from 'react-native'; import { StyleSheet, Platform } from 'react-native';
import { typography } from '@theme'; import { typography } from '@theme';
export const getStyles = (colors: any) => StyleSheet.create({ export const getStyles = (colors: any) =>
container: { StyleSheet.create({
flex: 1, container: {
backgroundColor: colors.background, flex: 1,
}, backgroundColor: colors.background,
content: { },
paddingBottom: 40, content: {
}, paddingBottom: 40,
profileCard: { },
alignItems: 'center',
paddingVertical: 32, // ---------- Profile card ----------
borderBottomWidth: 1, profileCard: {
borderBottomColor: colors.border, alignItems: 'center',
marginHorizontal: 16, backgroundColor: colors.cardBg,
}, marginHorizontal: 16,
avatar: { marginTop: 16,
width: 72, borderRadius: 20,
height: 72, paddingVertical: 28,
borderRadius: 36, paddingHorizontal: 20,
backgroundColor: colors.primary, ...Platform.select({
justifyContent: 'center', ios: {
alignItems: 'center', shadowColor: '#000',
marginBottom: 12, shadowOffset: { width: 0, height: 3 },
}, shadowOpacity: 0.06,
avatarText: { shadowRadius: 10,
fontSize: 28, },
fontWeight: typography.fontWeight.bold, android: { elevation: 2 },
color: '#FFFFFF', }),
}, },
profileName: { avatarWrap: {
fontSize: typography.fontSize.xl, position: 'relative',
fontWeight: typography.fontWeight.bold, marginBottom: 14,
color: colors.text, },
marginBottom: 4, avatar: {
}, width: 84,
profilePhone: { height: 84,
fontSize: typography.fontSize.md, borderRadius: 42,
color: colors.textSecondary, backgroundColor: colors.primary,
}, justifyContent: 'center',
menuSection: { alignItems: 'center',
marginTop: 16, borderWidth: 3,
paddingHorizontal: 16, borderColor: colors.primaryMuted ?? '#E9F7EF',
}, },
menuItem: { avatarText: {
flexDirection: 'row', fontSize: 32,
alignItems: 'center', fontWeight: typography.fontWeight.bold,
paddingVertical: 16, color: '#FFFFFF',
borderBottomWidth: 1, },
borderBottomColor: colors.border, editAvatarBadge: {
}, position: 'absolute',
menuIcon: { bottom: -2,
fontSize: 20, right: -2,
marginRight: 14, width: 28,
}, height: 28,
menuLabel: { borderRadius: 14,
flex: 1, backgroundColor: colors.background,
fontSize: typography.fontSize.md, alignItems: 'center',
fontWeight: typography.fontWeight.medium, justifyContent: 'center',
color: colors.text, borderWidth: 2,
}, borderColor: colors.cardBg,
menuArrow: { },
fontSize: 18, editAvatarIcon: {
color: colors.textSecondary, fontSize: 12,
}, },
}); profileName: {
fontSize: typography.fontSize.xl,
fontWeight: typography.fontWeight.bold,
color: colors.text,
marginBottom: 4,
},
profilePhoneRow: {
flexDirection: 'row',
alignItems: 'center',
marginBottom: 16,
},
profilePhoneIcon: {
fontSize: 13,
marginRight: 5,
},
profilePhone: {
fontSize: typography.fontSize.md,
color: colors.textSecondary,
},
editProfileButton: {
borderWidth: 1.5,
borderColor: colors.primary,
borderRadius: 20,
paddingHorizontal: 20,
paddingVertical: 8,
},
editProfileButtonText: {
fontSize: typography.fontSize.sm,
fontWeight: typography.fontWeight.semibold,
color: colors.primary,
},
// ---------- Stats row ----------
statsRow: {
flexDirection: 'row',
marginHorizontal: 16,
marginTop: 14,
backgroundColor: colors.cardBg,
borderRadius: 16,
paddingVertical: 16,
...Platform.select({
ios: {
shadowColor: '#000',
shadowOffset: { width: 0, height: 2 },
shadowOpacity: 0.05,
shadowRadius: 8,
},
android: { elevation: 1 },
}),
},
statItem: {
flex: 1,
alignItems: 'center',
},
statDivider: {
width: 1,
backgroundColor: colors.border,
},
statValue: {
fontSize: typography.fontSize.lg,
fontWeight: typography.fontWeight.bold,
color: colors.text,
},
statLabel: {
fontSize: typography.fontSize.xs,
color: colors.textSecondary,
marginTop: 2,
},
// ---------- Menu sections ----------
menuSection: {
marginTop: 24,
paddingHorizontal: 16,
},
sectionLabel: {
fontSize: typography.fontSize.xs,
fontWeight: typography.fontWeight.semibold,
color: colors.textSecondary,
letterSpacing: 0.8,
textTransform: 'uppercase',
marginBottom: 10,
marginLeft: 4,
},
sectionCard: {
backgroundColor: colors.cardBg,
borderRadius: 16,
overflow: 'hidden',
...Platform.select({
ios: {
shadowColor: '#000',
shadowOffset: { width: 0, height: 2 },
shadowOpacity: 0.04,
shadowRadius: 8,
},
android: { elevation: 1 },
}),
},
menuItem: {
flexDirection: 'row',
alignItems: 'center',
paddingVertical: 14,
paddingHorizontal: 14,
},
menuItemDivider: {
borderBottomWidth: 1,
borderBottomColor: colors.border,
},
menuIconBadge: {
width: 38,
height: 38,
borderRadius: 12,
alignItems: 'center',
justifyContent: 'center',
marginRight: 14,
},
menuIcon: {
fontSize: 17,
},
menuTextWrap: {
flex: 1,
},
menuLabel: {
fontSize: typography.fontSize.md,
fontWeight: typography.fontWeight.medium,
color: colors.text,
},
menuSubLabel: {
fontSize: typography.fontSize.xs,
color: colors.textSecondary,
marginTop: 1,
},
menuArrowBadge: {
width: 24,
height: 24,
borderRadius: 12,
backgroundColor: colors.background,
alignItems: 'center',
justifyContent: 'center',
},
menuArrow: {
fontSize: 13,
color: colors.textSecondary,
},
// ---------- Logout ----------
logoutSection: {
paddingHorizontal: 16,
marginTop: 28,
},
versionText: {
textAlign: 'center',
fontSize: typography.fontSize.xs,
color: colors.textSecondary,
marginTop: 18,
opacity: 0.7,
},
});

View File

@ -1,59 +1,197 @@
import React from 'react'; import React from 'react';
import { View, Text, TouchableOpacity, ScrollView } from 'react-native'; import { View, Text, TouchableOpacity, ScrollView } from 'react-native';
import {
useNavigation,
CompositeNavigationProp,
} from '@react-navigation/native';
import { BottomTabNavigationProp } from '@react-navigation/bottom-tabs';
import { StackNavigationProp } from '@react-navigation/stack';
import { getStyles } from './accountScreen.styles'; import { getStyles } from './accountScreen.styles';
import { Header, PrimaryButton } from '@components'; import { Header, PrimaryButton } from '@components';
import { useAppTheme } from '@theme'; import { useAppTheme } from '@theme';
import { useAppDispatch, useAppSelector } from '../../../hooks/useAppDispatch'; import { useAppDispatch, useAppSelector } from '../../../hooks/useAppDispatch';
import { logout } from '../../../store/authSlice'; import { logout } from '../../../store/commonreducers/auth';
import { AppStackParamList } from '../../../navigation/appStack';
import { MainTabParamList } from '../../../navigation/mainTabNavigator';
const MENU_ITEMS = [ type AccountNavProp = CompositeNavigationProp<
{ icon: '📍', label: 'My Addresses' }, BottomTabNavigationProp<MainTabParamList, 'AccountScreen'>,
{ icon: '💳', label: 'Payment Methods' }, StackNavigationProp<AppStackParamList>
{ icon: '🔔', label: 'Notifications' }, >;
{ icon: '❓', label: 'Help & Support' },
{ icon: '📋', label: 'Terms & Conditions' }, interface MenuItemData {
{ icon: '🔒', label: 'Privacy Policy' }, icon: string;
label: string;
subLabel: string;
tint: string;
onPress?: (navigation: AccountNavProp) => void;
}
interface MenuSectionData {
title: string;
items: MenuItemData[];
}
const MENU_SECTIONS: MenuSectionData[] = [
{
title: 'Account',
items: [
{
icon: '📍',
label: 'My Addresses',
subLabel: 'Manage delivery addresses',
tint: '#FDECEA',
},
{
icon: '💳',
label: 'Payment Methods',
subLabel: 'Cards, UPI & wallets',
tint: '#FFF4E5',
},
{
icon: '🔔',
label: 'Notifications',
subLabel: 'Alerts, offers & updates',
tint: '#FFF9DB',
},
],
},
{
title: 'Support & Legal',
items: [
{
icon: '❓',
label: 'Help & Support',
subLabel: 'FAQs and contact us',
tint: '#EAF4FF',
onPress: navigation => navigation.navigate('HelpSupportScreen'),
},
{
icon: '📋',
label: 'Terms & Conditions',
subLabel: 'Our terms of service',
tint: '#F1F0FF',
},
{
icon: '🔒',
label: 'Privacy Policy',
subLabel: 'How we handle your data',
tint: '#E9F7EF',
},
],
},
]; ];
export const AccountScreen: React.FC = () => { export const AccountScreen: React.FC = () => {
const { colors } = useAppTheme(); const { colors } = useAppTheme();
const styles = getStyles(colors); const styles = getStyles(colors);
const dispatch = useAppDispatch(); const dispatch = useAppDispatch();
const user = useAppSelector((state) => state.auth.user); const navigation = useNavigation<AccountNavProp>();
const user = useAppSelector(state => state.auth.user);
// TODO: wire these to real selectors once order/wallet state is available.
// const ordersCount = user?.ordersCount ?? 0;
// const savedAddressesCount = user?.addressesCount ?? 0;
// const walletBalance = user?.walletBalance ?? 0;
return ( return (
<View style={styles.container}> <View style={styles.container}>
<Header title="Account" /> <Header title="Account" />
<ScrollView contentContainerStyle={styles.content}> <ScrollView
contentContainerStyle={styles.content}
showsVerticalScrollIndicator={false}
>
{/* Profile card */}
<View style={styles.profileCard}> <View style={styles.profileCard}>
<View style={styles.avatar}> <View style={styles.avatarWrap}>
<Text style={styles.avatarText}> <View style={styles.avatar}>
{user?.name?.charAt(0)?.toUpperCase() || 'U'} <Text style={styles.avatarText}>
</Text> {user?.name?.charAt(0)?.toUpperCase() || 'U'}
</View> </Text>
<Text style={styles.profileName}>{user?.name || 'User'}</Text> </View>
<Text style={styles.profilePhone}>{user?.mobileNumber || '+91 98765 43210'}</Text>
</View>
<View style={styles.menuSection}>
{MENU_ITEMS.map((item, index) => (
<TouchableOpacity <TouchableOpacity
key={index} style={styles.editAvatarBadge}
style={styles.menuItem}
activeOpacity={0.7} activeOpacity={0.7}
> >
<Text style={styles.menuIcon}>{item.icon}</Text> <Text style={styles.editAvatarIcon}></Text>
<Text style={styles.menuLabel}>{item.label}</Text>
<Text style={styles.menuArrow}>{'>'}</Text>
</TouchableOpacity> </TouchableOpacity>
))} </View>
<Text style={styles.profileName}>{user?.name || 'User'}</Text>
<View style={styles.profilePhoneRow}>
<Text style={styles.profilePhoneIcon}>📞</Text>
<Text style={styles.profilePhone}>
{user?.mobileNumber || '+91 98765 43210'}
</Text>
</View>
<TouchableOpacity
style={styles.editProfileButton}
activeOpacity={0.75}
>
<Text style={styles.editProfileButtonText}>Edit Profile</Text>
</TouchableOpacity>
</View> </View>
<PrimaryButton {/* Quick stats */}
title="Logout" {/* <View style={styles.statsRow}>
onPress={() => dispatch(logout())} <View style={styles.statItem}>
style={{ marginTop: 24 }} <Text style={styles.statValue}>{ordersCount}</Text>
/> <Text style={styles.statLabel}>Orders</Text>
</View>
<View style={styles.statDivider} />
<View style={styles.statItem}>
<Text style={styles.statValue}>{savedAddressesCount}</Text>
<Text style={styles.statLabel}>Addresses</Text>
</View>
<View style={styles.statDivider} />
<View style={styles.statItem}>
<Text style={styles.statValue}>{walletBalance}</Text>
<Text style={styles.statLabel}>Wallet</Text>
</View>
</View> */}
{/* Menu sections */}
{MENU_SECTIONS.map(section => (
<View key={section.title} style={styles.menuSection}>
<Text style={styles.sectionLabel}>{section.title}</Text>
<View style={styles.sectionCard}>
{section.items.map((item, index) => {
const isLast = index === section.items.length - 1;
return (
<TouchableOpacity
key={item.label}
style={[styles.menuItem, !isLast && styles.menuItemDivider]}
activeOpacity={0.7}
onPress={() => item.onPress?.(navigation)}
>
<View
style={[
styles.menuIconBadge,
{ backgroundColor: item.tint },
]}
>
<Text style={styles.menuIcon}>{item.icon}</Text>
</View>
<View style={styles.menuTextWrap}>
<Text style={styles.menuLabel}>{item.label}</Text>
<Text style={styles.menuSubLabel}>{item.subLabel}</Text>
</View>
<View style={styles.menuArrowBadge}>
<Text style={styles.menuArrow}>{'>'}</Text>
</View>
</TouchableOpacity>
);
})}
</View>
</View>
))}
{/* Logout */}
<View style={styles.logoutSection}>
<PrimaryButton title="Logout" onPress={() => dispatch(logout())} />
<Text style={styles.versionText}>App version 1.0.0</Text>
</View>
</ScrollView> </ScrollView>
</View> </View>
); );

View File

@ -1,109 +1,350 @@
import { StyleSheet } from 'react-native'; import { StyleSheet, Platform } from 'react-native';
import { typography } from '@theme'; import { typography } from '@theme';
export const getStyles = (colors: any) => StyleSheet.create({ export const getStyles = (colors: any) =>
container: { StyleSheet.create({
flex: 1, container: {
backgroundColor: colors.background, flex: 1,
}, backgroundColor: colors.background,
list: { },
paddingBottom: 100, list: {
}, paddingBottom: 140,
cartItem: { },
flexDirection: 'row',
alignItems: 'center', // ---------- ETA banner ----------
justifyContent: 'space-between', etaBanner: {
paddingVertical: 14, flexDirection: 'row',
paddingHorizontal: 16, alignItems: 'center',
borderBottomWidth: 1, backgroundColor: colors.primaryMuted ?? '#E9F7EF',
borderBottomColor: colors.border, marginHorizontal: 16,
}, marginTop: 12,
itemInfo: { marginBottom: 4,
flex: 1, borderRadius: 12,
}, paddingVertical: 10,
itemTitle: { paddingHorizontal: 14,
fontSize: typography.fontSize.md, },
fontWeight: typography.fontWeight.medium, etaIcon: {
color: colors.text, fontSize: 16,
marginBottom: 4, marginRight: 8,
}, },
itemPrice: { etaText: {
fontSize: typography.fontSize.md, fontSize: typography.fontSize.sm,
fontWeight: typography.fontWeight.bold, fontWeight: typography.fontWeight.semibold,
color: colors.text, color: colors.primary,
}, },
footer: {
padding: 16, // ---------- Item cards ----------
}, itemsCard: {
couponRow: { backgroundColor: colors.cardBg,
flexDirection: 'row', marginHorizontal: 16,
alignItems: 'center', marginTop: 14,
marginBottom: 20, borderRadius: 16,
}, overflow: 'hidden',
couponInput: { ...Platform.select({
flex: 1, ios: {
height: 48, shadowColor: '#000',
borderWidth: 1, shadowOffset: { width: 0, height: 2 },
borderColor: colors.border, shadowOpacity: 0.05,
borderRadius: 12, shadowRadius: 8,
paddingHorizontal: 16, },
fontSize: typography.fontSize.md, android: { elevation: 1 },
color: colors.text, }),
backgroundColor: colors.inputBg, },
marginRight: 8, cartItem: {
}, flexDirection: 'row',
applyButton: { alignItems: 'center',
paddingHorizontal: 20, paddingVertical: 14,
paddingVertical: 14, paddingHorizontal: 14,
borderRadius: 12, },
backgroundColor: colors.primary, cartItemDivider: {
}, borderBottomWidth: 1,
applyButtonText: { borderBottomColor: colors.border,
color: '#FFFFFF', },
fontWeight: typography.fontWeight.semibold, itemThumb: {
fontSize: typography.fontSize.sm, width: 52,
}, height: 52,
feeRow: { borderRadius: 10,
flexDirection: 'row', backgroundColor: colors.inputBg,
justifyContent: 'space-between', alignItems: 'center',
marginBottom: 8, justifyContent: 'center',
}, marginRight: 12,
feeLabel: { },
fontSize: typography.fontSize.sm, itemThumbEmoji: {
color: colors.textSecondary, fontSize: 22,
}, },
feeValue: { itemInfo: {
fontSize: typography.fontSize.sm, flex: 1,
fontWeight: typography.fontWeight.medium, marginRight: 10,
color: colors.text, },
}, itemTitle: {
totalRow: { fontSize: typography.fontSize.md,
borderTopWidth: 1, fontWeight: typography.fontWeight.semibold,
borderTopColor: colors.border, color: colors.text,
paddingTop: 12, marginBottom: 4,
marginTop: 4, },
}, itemPrice: {
totalLabel: { fontSize: typography.fontSize.sm,
fontSize: typography.fontSize.md, fontWeight: typography.fontWeight.bold,
fontWeight: typography.fontWeight.bold, color: colors.textSecondary,
color: colors.text, },
},
totalValue: { // ---------- Add more items ----------
fontSize: typography.fontSize.md, addMoreRow: {
fontWeight: typography.fontWeight.bold, flexDirection: 'row',
color: colors.text, alignItems: 'center',
}, justifyContent: 'center',
bottomPanel: { paddingVertical: 12,
flexDirection: 'row', marginHorizontal: 16,
alignItems: 'center', marginTop: 4,
padding: 16, borderWidth: 1.5,
borderTopWidth: 1, borderStyle: 'dashed',
borderTopColor: colors.border, borderColor: colors.border,
backgroundColor: colors.background, borderRadius: 12,
}, },
bottomTotal: { addMoreIcon: {
fontSize: typography.fontSize.lg, fontSize: 14,
fontWeight: typography.fontWeight.bold, marginRight: 6,
color: colors.text, color: colors.primary,
}, },
}); addMoreText: {
fontSize: typography.fontSize.sm,
fontWeight: typography.fontWeight.semibold,
color: colors.primary,
},
// ---------- Footer ----------
footer: {
paddingHorizontal: 16,
paddingTop: 20,
},
// Coupon
sectionTitle: {
fontSize: typography.fontSize.sm,
fontWeight: typography.fontWeight.bold,
color: colors.text,
marginBottom: 10,
},
couponCard: {
backgroundColor: colors.cardBg,
borderRadius: 14,
padding: 6,
marginBottom: 20,
...Platform.select({
ios: {
shadowColor: '#000',
shadowOffset: { width: 0, height: 2 },
shadowOpacity: 0.05,
shadowRadius: 8,
},
android: { elevation: 1 },
}),
},
couponRow: {
flexDirection: 'row',
alignItems: 'center',
},
couponIcon: {
fontSize: 18,
marginLeft: 10,
marginRight: 4,
},
couponInput: {
flex: 1,
height: 44,
paddingHorizontal: 8,
fontSize: typography.fontSize.md,
color: colors.text,
},
applyButton: {
paddingHorizontal: 18,
paddingVertical: 11,
borderRadius: 10,
backgroundColor: colors.primary,
marginRight: 4,
},
applyButtonText: {
color: '#FFFFFF',
fontWeight: typography.fontWeight.semibold,
fontSize: typography.fontSize.sm,
},
couponAppliedRow: {
flexDirection: 'row',
alignItems: 'center',
justifyContent: 'space-between',
paddingVertical: 10,
paddingHorizontal: 12,
},
couponAppliedLeft: {
flexDirection: 'row',
alignItems: 'center',
flex: 1,
},
couponCheckBadge: {
width: 28,
height: 28,
borderRadius: 14,
backgroundColor: colors.primaryMuted ?? '#E9F7EF',
alignItems: 'center',
justifyContent: 'center',
marginRight: 10,
},
couponCheckIcon: {
fontSize: 13,
},
couponAppliedCode: {
fontSize: typography.fontSize.sm,
fontWeight: typography.fontWeight.bold,
color: colors.text,
},
couponAppliedSub: {
fontSize: typography.fontSize.xs,
color: colors.primary,
marginTop: 1,
},
couponRemoveText: {
fontSize: typography.fontSize.sm,
fontWeight: typography.fontWeight.semibold,
color: '#D32F2F',
},
// Bill details
billCard: {
backgroundColor: colors.cardBg,
borderRadius: 14,
padding: 16,
...Platform.select({
ios: {
shadowColor: '#000',
shadowOffset: { width: 0, height: 2 },
shadowOpacity: 0.05,
shadowRadius: 8,
},
android: { elevation: 1 },
}),
},
feeRow: {
flexDirection: 'row',
justifyContent: 'space-between',
marginBottom: 10,
},
feeLabel: {
fontSize: typography.fontSize.sm,
color: colors.textSecondary,
},
feeValue: {
fontSize: typography.fontSize.sm,
fontWeight: typography.fontWeight.medium,
color: colors.text,
},
totalRow: {
borderTopWidth: 1,
borderTopColor: colors.border,
paddingTop: 12,
marginTop: 4,
marginBottom: 0,
},
totalLabel: {
fontSize: typography.fontSize.md,
fontWeight: typography.fontWeight.bold,
color: colors.text,
},
totalValue: {
fontSize: typography.fontSize.md,
fontWeight: typography.fontWeight.bold,
color: colors.text,
},
savingsBanner: {
flexDirection: 'row',
alignItems: 'center',
backgroundColor: colors.primaryMuted ?? '#E9F7EF',
borderRadius: 10,
paddingVertical: 8,
paddingHorizontal: 12,
marginTop: 14,
},
savingsIcon: {
fontSize: 14,
marginRight: 6,
},
savingsText: {
fontSize: typography.fontSize.xs,
fontWeight: typography.fontWeight.semibold,
color: colors.primary,
},
// ---------- Bottom panel ----------
bottomPanel: {
position: 'absolute',
left: 0,
right: 0,
bottom: 0,
flexDirection: 'row',
alignItems: 'center',
justifyContent: 'space-between',
paddingHorizontal: 16,
paddingTop: 12,
paddingBottom: Platform.OS === 'ios' ? 28 : 16,
backgroundColor: colors.cardBg,
borderTopLeftRadius: 20,
borderTopRightRadius: 20,
...Platform.select({
ios: {
shadowColor: '#000',
shadowOffset: { width: 0, height: -4 },
shadowOpacity: 0.08,
shadowRadius: 12,
},
android: { elevation: 10 },
}),
},
bottomTotalWrap: {},
bottomTotalLabel: {
fontSize: typography.fontSize.xs,
color: colors.textSecondary,
},
bottomTotal: {
fontSize: typography.fontSize.lg,
fontWeight: typography.fontWeight.bold,
color: colors.text,
},
checkoutButton: {
flex: 1,
marginLeft: 16,
},
// ---------- Empty state ----------
emptyWrap: {
flex: 1,
alignItems: 'center',
justifyContent: 'center',
paddingHorizontal: 32,
},
emptyEmoji: {
fontSize: 56,
marginBottom: 16,
},
emptyTitle: {
fontSize: typography.fontSize.lg,
fontWeight: typography.fontWeight.bold,
color: colors.text,
marginBottom: 6,
},
emptySubtitle: {
fontSize: typography.fontSize.sm,
color: colors.textSecondary,
textAlign: 'center',
marginBottom: 24,
},
emptyButton: {
backgroundColor: colors.primary,
paddingHorizontal: 28,
paddingVertical: 14,
borderRadius: 12,
},
emptyButtonText: {
color: '#FFFFFF',
fontWeight: typography.fontWeight.bold,
fontSize: typography.fontSize.md,
},
});

View File

@ -2,71 +2,162 @@ import React, { useState } from 'react';
import { import {
View, View,
Text, Text,
FlatList, ScrollView,
TextInput, TextInput,
TouchableOpacity, TouchableOpacity,
} from 'react-native'; } from 'react-native';
import { useNavigation } from '@react-navigation/native';
import { StackNavigationProp } from '@react-navigation/stack';
import { getStyles } from './cartScreen.styles'; import { getStyles } from './cartScreen.styles';
import { Header, QuantitySelector, PrimaryButton } from '@components'; import { Header, QuantitySelector, PrimaryButton } from '@components';
import { useAppTheme } from '@theme'; import { useAppTheme } from '@theme';
import { useAppDispatch, useAppSelector } from '../../../hooks/useAppDispatch'; import { useAppDispatch, useAppSelector } from '../../../hooks/useAppDispatch';
import { removeItem, applyCoupon, removeCoupon, selectCartTotal } from '../../../store/cartSlice'; import {
removeItem,
applyCoupon,
removeCoupon,
selectCartTotal,
} from '../../../store/commonreducers/cart';
import { AppStackParamList } from '../../../navigation/appStack';
type CartScreenNavProp = StackNavigationProp<AppStackParamList, 'CartScreen'>;
export const CartScreen: React.FC = () => { export const CartScreen: React.FC = () => {
const { colors } = useAppTheme(); const { colors } = useAppTheme();
const styles = getStyles(colors); const styles = getStyles(colors);
const dispatch = useAppDispatch(); const dispatch = useAppDispatch();
const { items, couponCode } = useAppSelector((state) => state.cart); const navigation = useNavigation<CartScreenNavProp>();
const { items, couponCode } = useAppSelector(state => state.cart);
const totals = useAppSelector(selectCartTotal); const totals = useAppSelector(selectCartTotal);
const [couponInput, setCouponInput] = useState(''); const [couponInput, setCouponInput] = useState('');
const handleCouponPress = () => {
if (couponCode) {
dispatch(removeCoupon());
setCouponInput('');
} else if (couponInput) {
dispatch(applyCoupon(couponInput));
}
};
if (items.length === 0) {
return (
<View style={styles.container}>
<Header title="Cart" onBack={() => navigation.goBack()} />
<View style={styles.emptyWrap}>
<Text style={styles.emptyEmoji}>🛒</Text>
<Text style={styles.emptyTitle}>Your cart is empty</Text>
<Text style={styles.emptySubtitle}>
Looks like you haven't added anything yet. Browse providers and find
something you'll love.
</Text>
<TouchableOpacity
style={styles.emptyButton}
activeOpacity={0.85}
onPress={() => navigation.goBack()}
>
<Text style={styles.emptyButtonText}>Start Ordering</Text>
</TouchableOpacity>
</View>
</View>
);
}
return ( return (
<View style={styles.container}> <View style={styles.container}>
<Header title="Cart" onBack={() => {}} /> <Header title="Cart" onBack={() => navigation.goBack()} />
<FlatList
data={items}
keyExtractor={(item) => item.id}
renderItem={({ item }) => (
<View style={styles.cartItem}>
<View style={styles.itemInfo}>
<Text style={styles.itemTitle}>{item.item.title}</Text>
<Text style={styles.itemPrice}>{item.item.price}</Text>
</View>
<QuantitySelector
value={item.quantity}
onIncrement={() => {}}
onDecrement={() => dispatch(removeItem(item.item.id))}
/>
</View>
)}
ListFooterComponent={
<View style={styles.footer}>
<View style={styles.couponRow}>
<TextInput
style={styles.couponInput}
placeholder="Enter coupon code"
placeholderTextColor={colors.placeholder}
value={couponInput}
onChangeText={setCouponInput}
/>
<TouchableOpacity
style={styles.applyButton}
onPress={() => {
if (couponCode) {
dispatch(removeCoupon());
setCouponInput('');
} else if (couponInput) {
dispatch(applyCoupon(couponInput));
}
}}
activeOpacity={0.7}
>
<Text style={styles.applyButtonText}>
{couponCode ? 'Remove' : 'Apply'}
</Text>
</TouchableOpacity>
</View>
<ScrollView
contentContainerStyle={styles.list}
showsVerticalScrollIndicator={false}
>
<View style={styles.etaBanner}>
<Text style={styles.etaIcon}>🛵</Text>
<Text style={styles.etaText}>Delivery in 2025 mins</Text>
</View>
<View style={styles.itemsCard}>
{items.map((item, index) => (
<View
key={item.id}
style={[
styles.cartItem,
index < items.length - 1 && styles.cartItemDivider,
]}
>
<View style={styles.itemThumb}>
<Text style={styles.itemThumbEmoji}>🍕</Text>
</View>
<View style={styles.itemInfo}>
<Text style={styles.itemTitle} numberOfLines={2}>
{item.item.title}
</Text>
<Text style={styles.itemPrice}>{item.item.price}</Text>
</View>
<QuantitySelector
value={item.quantity}
onIncrement={() => {}}
onDecrement={() => dispatch(removeItem(item.item.id))}
/>
</View>
))}
</View>
<TouchableOpacity
style={styles.addMoreRow}
activeOpacity={0.7}
onPress={() => navigation.goBack()}
>
<Text style={styles.addMoreIcon}>+</Text>
<Text style={styles.addMoreText}>Add more items</Text>
</TouchableOpacity>
<View style={styles.footer}>
<Text style={styles.sectionTitle}>Apply Coupon</Text>
<View style={styles.couponCard}>
{couponCode ? (
<View style={styles.couponAppliedRow}>
<View style={styles.couponAppliedLeft}>
<View style={styles.couponCheckBadge}>
<Text style={styles.couponCheckIcon}></Text>
</View>
<View>
<Text style={styles.couponAppliedCode}>{couponCode}</Text>
<Text style={styles.couponAppliedSub}>Coupon applied</Text>
</View>
</View>
<TouchableOpacity
onPress={handleCouponPress}
activeOpacity={0.7}
>
<Text style={styles.couponRemoveText}>Remove</Text>
</TouchableOpacity>
</View>
) : (
<View style={styles.couponRow}>
<Text style={styles.couponIcon}>🏷</Text>
<TextInput
style={styles.couponInput}
placeholder="Enter coupon code"
placeholderTextColor={colors.placeholder}
value={couponInput}
onChangeText={setCouponInput}
autoCapitalize="characters"
/>
<TouchableOpacity
style={styles.applyButton}
onPress={handleCouponPress}
activeOpacity={0.7}
disabled={!couponInput}
>
<Text style={styles.applyButtonText}>Apply</Text>
</TouchableOpacity>
</View>
)}
</View>
<Text style={styles.sectionTitle}>Bill Details</Text>
<View style={styles.billCard}>
<View style={styles.feeRow}> <View style={styles.feeRow}>
<Text style={styles.feeLabel}>Subtotal</Text> <Text style={styles.feeLabel}>Subtotal</Text>
<Text style={styles.feeValue}>{totals.subtotal}</Text> <Text style={styles.feeValue}>{totals.subtotal}</Text>
@ -93,16 +184,28 @@ export const CartScreen: React.FC = () => {
<Text style={styles.totalLabel}>Total</Text> <Text style={styles.totalLabel}>Total</Text>
<Text style={styles.totalValue}>{totals.total}</Text> <Text style={styles.totalValue}>{totals.total}</Text>
</View> </View>
{totals.discount > 0 && (
<View style={styles.savingsBanner}>
<Text style={styles.savingsIcon}>🎉</Text>
<Text style={styles.savingsText}>
You're saving {totals.discount} on this order
</Text>
</View>
)}
</View> </View>
} </View>
contentContainerStyle={styles.list} </ScrollView>
/>
<View style={styles.bottomPanel}> <View style={styles.bottomPanel}>
<Text style={styles.bottomTotal}>Total: {totals.total}</Text> <View style={styles.bottomTotalWrap}>
<Text style={styles.bottomTotalLabel}>Total</Text>
<Text style={styles.bottomTotal}>{totals.total}</Text>
</View>
<PrimaryButton <PrimaryButton
title="View Bill" title="Proceed to Checkout"
onPress={() => {}} onPress={() => navigation.navigate('CheckoutAddressScreen')}
style={{ flex: 1, marginLeft: 12 }} style={styles.checkoutButton}
/> />
</View> </View>
</View> </View>

View File

@ -5,9 +5,14 @@ import {
TouchableOpacity, TouchableOpacity,
ScrollView, ScrollView,
} from 'react-native'; } from 'react-native';
import { useNavigation } from '@react-navigation/native';
import { StackNavigationProp } from '@react-navigation/stack';
import { getStyles } from './checkoutAddressScreen.styles'; import { getStyles } from './checkoutAddressScreen.styles';
import { Header, StepProgress, PrimaryButton } from '@components'; import { Header, StepProgress, PrimaryButton } from '@components';
import { useAppTheme } from '@theme'; import { useAppTheme } from '@theme';
import { AppStackParamList } from '../../../navigation/appStack';
type CheckoutAddressNavProp = StackNavigationProp<AppStackParamList, 'CheckoutAddressScreen'>;
const CHECKOUT_STEPS = [ const CHECKOUT_STEPS = [
{ key: 'address', label: 'Address' }, { key: 'address', label: 'Address' },
@ -18,11 +23,12 @@ const CHECKOUT_STEPS = [
export const CheckoutAddressScreen: React.FC = () => { export const CheckoutAddressScreen: React.FC = () => {
const { colors } = useAppTheme(); const { colors } = useAppTheme();
const styles = getStyles(colors); const styles = getStyles(colors);
const navigation = useNavigation<CheckoutAddressNavProp>();
const [deliveryOption, setDeliveryOption] = useState<'standard' | 'express'>('standard'); const [deliveryOption, setDeliveryOption] = useState<'standard' | 'express'>('standard');
return ( return (
<View style={styles.container}> <View style={styles.container}>
<Header title="Checkout" onBack={() => {}} /> <Header title="Checkout" onBack={() => navigation.goBack()} />
<StepProgress steps={CHECKOUT_STEPS} activeStep={0} /> <StepProgress steps={CHECKOUT_STEPS} activeStep={0} />
<ScrollView contentContainerStyle={styles.content}> <ScrollView contentContainerStyle={styles.content}>
<View style={styles.addressCard}> <View style={styles.addressCard}>
@ -69,7 +75,7 @@ export const CheckoutAddressScreen: React.FC = () => {
</TouchableOpacity> </TouchableOpacity>
</ScrollView> </ScrollView>
<View style={styles.footer}> <View style={styles.footer}>
<PrimaryButton title="Continue to Payment" onPress={() => {}} /> <PrimaryButton title="Continue to Payment" onPress={() => navigation.navigate('CheckoutPaymentScreen')} />
</View> </View>
</View> </View>
); );

View File

@ -1,8 +1,13 @@
import React, { useState } from 'react'; import React, { useState } from 'react';
import { View, Text, ScrollView } from 'react-native'; import { View, Text, ScrollView } from 'react-native';
import { useNavigation } from '@react-navigation/native';
import { StackNavigationProp } from '@react-navigation/stack';
import { getStyles } from './checkoutPaymentScreen.styles'; import { getStyles } from './checkoutPaymentScreen.styles';
import { Header, StepProgress, PaymentOption, PrimaryButton } from '@components'; import { Header, StepProgress, PaymentOption, PrimaryButton } from '@components';
import { useAppTheme } from '@theme'; import { useAppTheme } from '@theme';
import { AppStackParamList } from '../../../navigation/appStack';
type CheckoutPaymentNavProp = StackNavigationProp<AppStackParamList, 'CheckoutPaymentScreen'>;
const CHECKOUT_STEPS = [ const CHECKOUT_STEPS = [
{ key: 'address', label: 'Address' }, { key: 'address', label: 'Address' },
@ -20,11 +25,12 @@ const PAYMENT_METHODS = [
export const CheckoutPaymentScreen: React.FC = () => { export const CheckoutPaymentScreen: React.FC = () => {
const { colors } = useAppTheme(); const { colors } = useAppTheme();
const styles = getStyles(colors); const styles = getStyles(colors);
const navigation = useNavigation<CheckoutPaymentNavProp>();
const [selectedMethod, setSelectedMethod] = useState('upi'); const [selectedMethod, setSelectedMethod] = useState('upi');
return ( return (
<View style={styles.container}> <View style={styles.container}>
<Header title="Checkout" onBack={() => {}} /> <Header title="Checkout" onBack={() => navigation.goBack()} />
<StepProgress steps={CHECKOUT_STEPS} activeStep={1} /> <StepProgress steps={CHECKOUT_STEPS} activeStep={1} />
<ScrollView contentContainerStyle={styles.content}> <ScrollView contentContainerStyle={styles.content}>
<Text style={styles.sectionTitle}>Select Payment Method</Text> <Text style={styles.sectionTitle}>Select Payment Method</Text>
@ -39,7 +45,7 @@ export const CheckoutPaymentScreen: React.FC = () => {
))} ))}
</ScrollView> </ScrollView>
<View style={styles.footer}> <View style={styles.footer}>
<PrimaryButton title="Place Order" onPress={() => {}} /> <PrimaryButton title="Place Order" onPress={() => navigation.navigate('OrderConfirmedScreen', { orderId: 'ORD-' + Math.floor(Math.random() * 900000 + 100000) })} />
</View> </View>
</View> </View>
); );

View File

@ -13,7 +13,7 @@ import { getStyles } from './completeProfileScreen.styles';
import { CustomInput, PrimaryButton } from '@components'; import { CustomInput, PrimaryButton } from '@components';
import { useAppTheme } from '@theme'; import { useAppTheme } from '@theme';
import { useAppDispatch } from '../../../hooks/useAppDispatch'; import { useAppDispatch } from '../../../hooks/useAppDispatch';
import { completeProfile } from '../../../store/authSlice'; import { completeProfile } from '../../../store/commonreducers/auth';
import { AuthStackParamList } from '../../../navigation/authStack'; import { AuthStackParamList } from '../../../navigation/authStack';
type NavProp = StackNavigationProp<AuthStackParamList, 'CompleteProfileScreen'>; type NavProp = StackNavigationProp<AuthStackParamList, 'CompleteProfileScreen'>;

View File

@ -1,8 +1,13 @@
import React from 'react'; import React from 'react';
import { View, Text, ScrollView, TouchableOpacity } from 'react-native'; import { View, Text, ScrollView, TouchableOpacity } from 'react-native';
import { useNavigation } from '@react-navigation/native';
import { StackNavigationProp } from '@react-navigation/stack';
import { getStyles } from './helpSupportScreen.styles'; import { getStyles } from './helpSupportScreen.styles';
import { Header } from '@components'; import { Header } from '@components';
import { useAppTheme } from '@theme'; import { useAppTheme } from '@theme';
import { AppStackParamList } from '../../../navigation/appStack';
type HelpSupportNavProp = StackNavigationProp<AppStackParamList, 'HelpSupportScreen'>;
const FAQS = [ const FAQS = [
{ q: 'How do I track my order?', a: 'Go to Orders tab and tap on your active order to see tracking.' }, { q: 'How do I track my order?', a: 'Go to Orders tab and tap on your active order to see tracking.' },
@ -14,10 +19,11 @@ const FAQS = [
export const HelpSupportScreen: React.FC = () => { export const HelpSupportScreen: React.FC = () => {
const { colors } = useAppTheme(); const { colors } = useAppTheme();
const styles = getStyles(colors); const styles = getStyles(colors);
const navigation = useNavigation<HelpSupportNavProp>();
return ( return (
<View style={styles.container}> <View style={styles.container}>
<Header title="Help & Support" /> <Header title="Help & Support" onBack={() => navigation.goBack()} />
<ScrollView contentContainerStyle={styles.content}> <ScrollView contentContainerStyle={styles.content}>
<Text style={styles.sectionTitle}>Frequently Asked Questions</Text> <Text style={styles.sectionTitle}>Frequently Asked Questions</Text>
{FAQS.map((faq, index) => ( {FAQS.map((faq, index) => (

View File

@ -1,64 +1,388 @@
import { StyleSheet } from 'react-native'; import { StyleSheet, Dimensions, Platform } from 'react-native';
import { typography } from '@theme'; import { typography } from '@theme';
export const getStyles = (colors: any) => StyleSheet.create({ const { width } = Dimensions.get('window');
container: { const BANNER_WIDTH = width - 32;
flex: 1,
backgroundColor: colors.background, export const getStyles = (colors: any) =>
}, StyleSheet.create({
addressBar: { container: {
paddingHorizontal: 16, flex: 1,
paddingTop: 12, backgroundColor: colors.background,
paddingBottom: 4, },
},
addressLabel: { // ---------- Top bar ----------
fontSize: typography.fontSize.xs, topBar: {
color: colors.textSecondary, flexDirection: 'row',
marginBottom: 2, alignItems: 'center',
}, justifyContent: 'space-between',
addressRow: { paddingHorizontal: 16,
flexDirection: 'row', paddingTop: 14,
alignItems: 'center', paddingBottom: 10,
}, },
addressText: { addressBar: {
fontSize: typography.fontSize.md, flexDirection: 'row',
fontWeight: typography.fontWeight.semibold, alignItems: 'center',
color: colors.text, flex: 1,
flex: 1, },
}, pinBadge: {
dropdownArrow: { width: 34,
fontSize: 12, height: 34,
color: colors.textSecondary, borderRadius: 17,
marginLeft: 4, backgroundColor: colors.primaryMuted ?? '#E9F7EF',
}, alignItems: 'center',
banner: { justifyContent: 'center',
backgroundColor: '#05824C', marginRight: 10,
marginHorizontal: 16, },
borderRadius: 12, pinEmoji: {
padding: 20, fontSize: 16,
marginVertical: 8, },
}, addressTextWrap: {
bannerText: { flex: 1,
fontSize: typography.fontSize.xxl, },
fontWeight: typography.fontWeight.bold, addressLabel: {
color: '#FFFFFF', fontSize: typography.fontSize.xs,
}, color: colors.textSecondary,
bannerSubtext: { marginBottom: 1,
fontSize: typography.fontSize.sm, letterSpacing: 0.2,
color: '#FFFFFF', },
opacity: 0.9, addressRow: {
marginTop: 4, flexDirection: 'row',
}, alignItems: 'center',
chipRow: { },
paddingHorizontal: 16, addressText: {
paddingVertical: 12, fontSize: typography.fontSize.md,
}, fontWeight: typography.fontWeight.bold,
sectionTitle: { color: colors.text,
fontSize: typography.fontSize.lg, maxWidth: width * 0.55,
fontWeight: typography.fontWeight.bold, },
color: colors.text, dropdownArrow: {
paddingHorizontal: 16, fontSize: 11,
marginBottom: 8, color: colors.textSecondary,
marginTop: 4, marginLeft: 6,
}, },
}); avatarButton: {
width: 40,
height: 40,
borderRadius: 20,
backgroundColor: colors.surface ?? '#F2F2F2',
alignItems: 'center',
justifyContent: 'center',
borderWidth: 1,
borderColor: colors.border ?? '#ECECEC',
},
avatarEmoji: {
fontSize: 18,
},
notifDot: {
position: 'absolute',
top: 6,
right: 7,
width: 8,
height: 8,
borderRadius: 4,
backgroundColor: '#FF4D4F',
borderWidth: 1.5,
borderColor: colors.background,
},
// ---------- Search ----------
searchWrap: {
paddingHorizontal: 16,
marginTop: 4,
marginBottom: 18,
},
// ---------- Promo carousel ----------
bannerListContent: {
paddingLeft: 16,
paddingRight: 4,
},
bannerCard: {
width: BANNER_WIDTH,
height: 140,
borderRadius: 20,
marginRight: 12,
padding: 20,
overflow: 'hidden',
justifyContent: 'space-between',
...Platform.select({
ios: {
shadowColor: '#000',
shadowOffset: { width: 0, height: 6 },
shadowOpacity: 0.15,
shadowRadius: 12,
},
android: { elevation: 5 },
}),
},
bannerDecoCircleLg: {
position: 'absolute',
width: 140,
height: 140,
borderRadius: 70,
backgroundColor: 'rgba(255,255,255,0.10)',
top: -50,
right: -30,
},
bannerDecoCircleSm: {
position: 'absolute',
width: 80,
height: 80,
borderRadius: 40,
backgroundColor: 'rgba(255,255,255,0.08)',
bottom: -30,
right: 40,
},
bannerEyebrow: {
fontSize: typography.fontSize.xs,
fontWeight: typography.fontWeight.semibold,
color: 'rgba(255,255,255,0.85)',
letterSpacing: 0.6,
textTransform: 'uppercase',
},
bannerText: {
fontSize: 26,
fontWeight: typography.fontWeight.bold,
color: '#FFFFFF',
marginTop: 2,
},
bannerSubtext: {
fontSize: typography.fontSize.sm,
color: 'rgba(255,255,255,0.9)',
marginTop: 2,
},
bannerCta: {
alignSelf: 'flex-start',
backgroundColor: 'rgba(255,255,255,0.95)',
paddingHorizontal: 14,
paddingVertical: 7,
borderRadius: 20,
marginTop: 4,
},
bannerCtaText: {
fontSize: typography.fontSize.xs,
fontWeight: typography.fontWeight.bold,
color: '#111111',
},
dotsRow: {
flexDirection: 'row',
alignSelf: 'center',
marginTop: 10,
marginBottom: 4,
},
dot: {
width: 6,
height: 6,
borderRadius: 3,
backgroundColor: colors.border ?? '#E0E0E0',
marginHorizontal: 3,
},
dotActive: {
width: 16,
backgroundColor: colors.primary ?? '#05824C',
},
// ---------- Categories ----------
categorySection: {
marginTop: 22,
},
chipRow: {
paddingHorizontal: 16,
paddingTop: 4,
},
categoryItem: {
alignItems: 'center',
marginRight: 18,
width: 64,
},
categoryCircle: {
width: 60,
height: 60,
borderRadius: 18,
backgroundColor: colors.surface ?? '#F5F6F8',
alignItems: 'center',
justifyContent: 'center',
marginBottom: 6,
borderWidth: 1.5,
borderColor: 'transparent',
},
categoryCircleSelected: {
backgroundColor: colors.primaryMuted ?? '#E9F7EF',
borderColor: colors.primary ?? '#05824C',
},
categoryIcon: {
fontSize: 24,
},
categoryLabel: {
fontSize: typography.fontSize.xs,
color: colors.textSecondary,
fontWeight: typography.fontWeight.medium,
textAlign: 'center',
},
categoryLabelSelected: {
color: colors.text,
fontWeight: typography.fontWeight.bold,
},
// ---------- Section header ----------
sectionHeaderRow: {
flexDirection: 'row',
alignItems: 'center',
justifyContent: 'space-between',
paddingHorizontal: 16,
marginTop: 26,
marginBottom: 12,
},
sectionTitle: {
fontSize: typography.fontSize.lg,
fontWeight: typography.fontWeight.bold,
color: colors.text,
},
sectionSubtitle: {
fontSize: typography.fontSize.xs,
color: colors.textSecondary,
marginTop: 2,
},
seeAllText: {
fontSize: typography.fontSize.sm,
fontWeight: typography.fontWeight.semibold,
color: colors.primary ?? '#05824C',
},
// ---------- Provider cards ----------
providerList: {
paddingHorizontal: 16,
},
providerCardWrap: {
backgroundColor: colors.surface ?? '#FFFFFF',
borderRadius: 18,
marginBottom: 16,
overflow: 'hidden',
...Platform.select({
ios: {
shadowColor: '#000',
shadowOffset: { width: 0, height: 3 },
shadowOpacity: 0.06,
shadowRadius: 10,
},
android: { elevation: 2 },
}),
},
providerImageWrap: {
width: '100%',
height: 150,
position: 'relative',
},
providerImage: {
width: '100%',
height: '100%',
},
discountRibbon: {
position: 'absolute',
top: 12,
left: 0,
backgroundColor: '#05824C',
paddingVertical: 4,
paddingHorizontal: 10,
borderTopRightRadius: 8,
borderBottomRightRadius: 8,
},
discountRibbonText: {
color: '#FFFFFF',
fontSize: typography.fontSize.xs,
fontWeight: typography.fontWeight.bold,
},
favoriteButton: {
position: 'absolute',
top: 10,
right: 10,
width: 32,
height: 32,
borderRadius: 16,
backgroundColor: 'rgba(255,255,255,0.92)',
alignItems: 'center',
justifyContent: 'center',
},
favoriteIcon: {
fontSize: 15,
},
providerBody: {
padding: 14,
},
providerTopRow: {
flexDirection: 'row',
justifyContent: 'space-between',
alignItems: 'flex-start',
},
providerName: {
fontSize: typography.fontSize.md,
fontWeight: typography.fontWeight.bold,
color: colors.text,
flex: 1,
marginRight: 8,
},
ratingPill: {
flexDirection: 'row',
alignItems: 'center',
backgroundColor: colors.primaryMuted ?? '#E9F7EF',
borderRadius: 8,
paddingHorizontal: 6,
paddingVertical: 3,
},
ratingStar: {
fontSize: 11,
marginRight: 3,
},
ratingText: {
fontSize: typography.fontSize.xs,
fontWeight: typography.fontWeight.bold,
color: colors.primary ?? '#05824C',
},
metaRow: {
flexDirection: 'row',
alignItems: 'center',
marginTop: 8,
},
metaText: {
fontSize: typography.fontSize.sm,
color: colors.textSecondary,
},
metaDivider: {
width: 3,
height: 3,
borderRadius: 1.5,
backgroundColor: colors.textSecondary,
marginHorizontal: 8,
opacity: 0.5,
},
tagPill: {
alignSelf: 'flex-start',
backgroundColor: colors.background,
borderRadius: 6,
paddingHorizontal: 8,
paddingVertical: 3,
marginTop: 10,
borderWidth: 1,
borderColor: colors.border ?? '#ECECEC',
},
tagPillText: {
fontSize: typography.fontSize.xs,
color: colors.textSecondary,
fontWeight: typography.fontWeight.medium,
},
// ---------- Skeleton / empty ----------
emptyWrap: {
alignItems: 'center',
paddingVertical: 40,
},
emptyEmoji: {
fontSize: 36,
marginBottom: 8,
},
emptyText: {
color: colors.textSecondary,
fontSize: typography.fontSize.sm,
},
});

View File

@ -1,30 +1,74 @@
import React, { useState, useEffect, useCallback } from 'react'; import React, { useState, useEffect, useCallback, useRef } from 'react';
import { import {
View, View,
Text, Text,
ScrollView, ScrollView,
TouchableOpacity, TouchableOpacity,
FlatList, FlatList,
Image,
NativeSyntheticEvent,
NativeScrollEvent,
Dimensions,
} from 'react-native'; } from 'react-native';
import { useNavigation } from '@react-navigation/native'; import {
useNavigation,
CompositeNavigationProp,
} from '@react-navigation/native';
import { BottomTabNavigationProp } from '@react-navigation/bottom-tabs'; import { BottomTabNavigationProp } from '@react-navigation/bottom-tabs';
import { StackNavigationProp } from '@react-navigation/stack';
import { getStyles } from './homeScreen.styles'; import { getStyles } from './homeScreen.styles';
import { SearchBar, ProviderCard, CategoryChip } from '@components'; import { ProviderCard, SearchBar } from '@components';
import { useAppTheme } from '@theme'; import { useAppTheme } from '@theme';
import { deliveryService } from '../../../services/deliveryService'; import { getProvidersApi } from '../../../api/deliveryApi';
import { Provider } from '../../../interfaces'; import { Provider } from '../../../interfaces';
import { AppStackParamList } from '../../../navigation/appStack'; import { AppStackParamList } from '../../../navigation/appStack';
import { MainTabParamList } from '../../../navigation/mainTabNavigator';
type NavProp = BottomTabNavigationProp<AppStackParamList, 'HomeScreen'>; type NavProp = CompositeNavigationProp<
BottomTabNavigationProp<MainTabParamList, 'HomeScreen'>,
StackNavigationProp<AppStackParamList>
>;
const { width } = Dimensions.get('window');
const BANNER_STEP = width - 32 + 12; // card width + margin
const CATEGORIES = [ const CATEGORIES = [
{ key: 'all', icon: '🏠', label: 'All' }, { key: 'all', icon: '🏠', label: 'All' },
{ key: 'Food', icon: '🍔', label: 'Food' }, { key: 'Food', icon: '🍔', label: 'Food' },
{ key: 'Groceries', icon: '🛒', label: 'Groceries' }, { key: 'Groceries', icon: '🛒', label: 'Grocery' },
{ key: 'Pharmacy', icon: '💊', label: 'Pharmacy' }, { key: 'Pharmacy', icon: '💊', label: 'Pharmacy' },
{ key: 'Meat', icon: '🥩', label: 'Meat' },
{ key: 'Flowers', icon: '💐', label: 'Flowers' },
{ key: 'More', icon: '📦', label: 'More' }, { key: 'More', icon: '📦', label: 'More' },
]; ];
const PROMOS = [
{
key: 'promo1',
colors: ['#05824C', '#0AA25E'],
eyebrow: 'Limited time',
title: '50% OFF',
subtitle: 'On your first order, up to ₹150',
cta: 'Order now',
},
{
key: 'promo2',
colors: ['#7B3FF2', '#9B6BF5'],
eyebrow: 'Free delivery',
title: '₹0 Delivery',
subtitle: 'On orders above ₹299 today',
cta: 'Explore',
},
{
key: 'promo3',
colors: ['#E85D3D', '#F0805F'],
eyebrow: 'Pharmacy',
title: 'Meds in 20 min',
subtitle: 'Genuine medicines, fast delivery',
cta: 'Shop now',
},
];
export const HomeScreen: React.FC = () => { export const HomeScreen: React.FC = () => {
const { colors } = useAppTheme(); const { colors } = useAppTheme();
const styles = getStyles(colors); const styles = getStyles(colors);
@ -32,73 +76,191 @@ export const HomeScreen: React.FC = () => {
const [providers, setProviders] = useState<Provider[]>([]); const [providers, setProviders] = useState<Provider[]>([]);
const [selectedCategory, setSelectedCategory] = useState('all'); const [selectedCategory, setSelectedCategory] = useState('all');
const [activeBanner, setActiveBanner] = useState(0);
useEffect(() => { useEffect(() => {
deliveryService.getProviders().then(setProviders); getProvidersApi().then(setProviders);
}, []); }, []);
const filteredProviders = selectedCategory === 'all' const filteredProviders =
? providers selectedCategory === 'all'
: providers.filter((p) => p.tag === selectedCategory); ? providers
: providers.filter(p => p.tag === selectedCategory);
const handleSearchFocus = useCallback(() => { const handleSearchFocus = useCallback(() => {
navigation.navigate('SearchScreen'); navigation.navigate('SearchScreen');
}, [navigation]); }, [navigation]);
const handleBannerScroll = useCallback(
(e: NativeSyntheticEvent<NativeScrollEvent>) => {
const index = Math.round(e.nativeEvent.contentOffset.x / BANNER_STEP);
setActiveBanner(index);
},
[],
);
return ( return (
<ScrollView style={styles.container} showsVerticalScrollIndicator={false}> <ScrollView style={styles.container} showsVerticalScrollIndicator={false}>
<View style={styles.addressBar}> {/* Top bar */}
<Text style={styles.addressLabel}>Deliver to</Text> <View style={styles.topBar}>
<TouchableOpacity style={styles.addressRow} activeOpacity={0.7}> <TouchableOpacity style={styles.addressBar} activeOpacity={0.7}>
<Text style={styles.addressText}>Koramangala 4th Block, B... </Text> <View style={styles.pinBadge}>
<Text style={styles.dropdownArrow}></Text> <Text style={styles.pinEmoji}>📍</Text>
</View>
<View style={styles.addressTextWrap}>
<Text style={styles.addressLabel}>Deliver to</Text>
<View style={styles.addressRow}>
<Text style={styles.addressText} numberOfLines={1}>
Koramangala 4th Block
</Text>
<Text style={styles.dropdownArrow}></Text>
</View>
</View>
</TouchableOpacity>
<TouchableOpacity style={styles.avatarButton} activeOpacity={0.7}>
<Text style={styles.avatarEmoji}>🔔</Text>
<View style={styles.notifDot} />
</TouchableOpacity> </TouchableOpacity>
</View> </View>
<SearchBar {/* Search */}
value="" <View style={styles.searchWrap}>
onChangeText={() => {}} <SearchBar
placeholder="Search providers or items..." value=""
onFocus={handleSearchFocus} onChangeText={() => {}}
/> placeholder="Search providers or items..."
onFocus={handleSearchFocus}
<View style={styles.banner}> />
<Text style={styles.bannerText}>50% OFF</Text>
<Text style={styles.bannerSubtext}>on your first order</Text>
</View> </View>
{/* Promo carousel */}
<FlatList <FlatList
data={PROMOS}
horizontal horizontal
data={CATEGORIES} keyExtractor={item => item.key}
keyExtractor={(item) => item.key}
showsHorizontalScrollIndicator={false} showsHorizontalScrollIndicator={false}
contentContainerStyle={styles.chipRow} contentContainerStyle={styles.bannerListContent}
snapToInterval={BANNER_STEP}
decelerationRate="fast"
onScroll={handleBannerScroll}
scrollEventThrottle={16}
renderItem={({ item }) => ( renderItem={({ item }) => (
<CategoryChip <View
icon={item.icon} style={[styles.bannerCard, { backgroundColor: item.colors[0] }]}
label={item.label} >
isSelected={selectedCategory === item.key} <View style={styles.bannerDecoCircleLg} />
onPress={() => setSelectedCategory(item.key)} <View style={styles.bannerDecoCircleSm} />
/> <View>
<Text style={styles.bannerEyebrow}>{item.eyebrow}</Text>
<Text style={styles.bannerText}>{item.title}</Text>
<Text style={styles.bannerSubtext}>{item.subtitle}</Text>
</View>
<View style={styles.bannerCta}>
<Text style={styles.bannerCtaText}>{item.cta}</Text>
</View>
</View>
)} )}
/> />
<View style={styles.dotsRow}>
{PROMOS.map((_, i) => (
<View
key={i}
style={[styles.dot, i === activeBanner && styles.dotActive]}
/>
))}
</View>
<Text style={styles.sectionTitle}> {/* Categories */}
{selectedCategory === 'all' ? 'Popular Providers' : selectedCategory} <View style={styles.categorySection}>
</Text> <FlatList
horizontal
{filteredProviders.map((provider) => ( data={CATEGORIES}
<ProviderCard keyExtractor={item => item.key}
key={provider.id} showsHorizontalScrollIndicator={false}
imageUrl={provider.imageUrl} contentContainerStyle={styles.chipRow}
name={provider.name} renderItem={({ item }) => {
rating={provider.rating} const isSelected = selectedCategory === item.key;
deliveryTime={provider.deliveryTime} return (
tag={provider.tag} <TouchableOpacity
discountText={provider.discountText} style={styles.categoryItem}
onPress={() => navigation.navigate('SearchScreen')} activeOpacity={0.75}
onPress={() => {
setSelectedCategory(item.key);
if (item.key !== 'all') {
navigation.navigate('ProviderListScreen', {
category: item.key,
});
}
}}
>
<View
style={[
styles.categoryCircle,
isSelected && styles.categoryCircleSelected,
]}
>
<Text style={styles.categoryIcon}>{item.icon}</Text>
</View>
<Text
style={[
styles.categoryLabel,
isSelected && styles.categoryLabelSelected,
]}
numberOfLines={1}
>
{item.label}
</Text>
</TouchableOpacity>
);
}}
/> />
))} </View>
{/* Section header */}
<View style={styles.sectionHeaderRow}>
<View>
<Text style={styles.sectionTitle}>
{selectedCategory === 'all' ? 'Popular near you' : selectedCategory}
</Text>
<Text style={styles.sectionSubtitle}>
{filteredProviders.length} place
{filteredProviders.length === 1 ? '' : 's'} delivering to you
</Text>
</View>
<TouchableOpacity activeOpacity={0.7}>
<Text style={styles.seeAllText}>See all</Text>
</TouchableOpacity>
</View>
{/* Provider list */}
<View style={styles.providerList}>
{filteredProviders.length === 0 && (
<View style={styles.emptyWrap}>
<Text style={styles.emptyEmoji}>🍽</Text>
<Text style={styles.emptyText}>
No providers in this category yet
</Text>
</View>
)}
{filteredProviders.map(provider => (
<ProviderCard
key={provider.id}
imageUrl={provider.imageUrl}
name={provider.name}
rating={provider.rating}
deliveryTime={provider.deliveryTime}
tag={provider.tag}
discountText={provider.discountText}
onPress={() =>
navigation.navigate('ProviderDetailsScreen', {
providerId: provider.id,
})
}
/>
))}
</View>
<View style={{ height: 24 }} /> <View style={{ height: 24 }} />
</ScrollView> </ScrollView>

View File

@ -1,4 +1,4 @@
export * from './loginScreen'; export * from './LoginScreen';
export * from './otpScreen'; export * from './otpScreen';
export * from './setLocationScreen'; export * from './setLocationScreen';
export * from './completeProfileScreen'; export * from './completeProfileScreen';

View File

@ -1,27 +1,41 @@
import React from 'react'; import React from 'react';
import { View, Text } from 'react-native'; import { View, Text } from 'react-native';
import { useNavigation, useRoute, RouteProp } from '@react-navigation/native';
import { StackNavigationProp } from '@react-navigation/stack';
import { getStyles } from './liveTrackingScreen.styles'; import { getStyles } from './liveTrackingScreen.styles';
import { Header, PrimaryButton } from '@components'; import { Header, PrimaryButton } from '@components';
import { useAppTheme } from '@theme'; import { useAppTheme } from '@theme';
import { AppStackParamList } from '../../../navigation/appStack';
type LiveTrackingNavProp = StackNavigationProp<AppStackParamList, 'LiveTrackingScreen'>;
type LiveTrackingRouteProp = RouteProp<AppStackParamList, 'LiveTrackingScreen'>;
export const LiveTrackingScreen: React.FC = () => { export const LiveTrackingScreen: React.FC = () => {
const { colors } = useAppTheme(); const { colors } = useAppTheme();
const styles = getStyles(colors); const styles = getStyles(colors);
const navigation = useNavigation<LiveTrackingNavProp>();
const route = useRoute<LiveTrackingRouteProp>();
const orderId = route.params?.orderId || 'ORD-123456';
return ( return (
<View style={styles.container}> <View style={styles.container}>
<Header title="Live Tracking" onBack={() => {}} /> <Header title="Live Tracking" onBack={() => navigation.goBack()} />
<View style={styles.mapPlaceholder}> <View style={styles.mapPlaceholder}>
<Text style={styles.mapIcon}>🗺</Text> <Text style={styles.mapIcon}>🗺</Text>
<Text style={styles.mapText}>Live Map</Text> <Text style={styles.mapText}>Live Map</Text>
<Text style={styles.mapSubtext}> <Text style={styles.mapSubtext}>
Delivery partner location tracking Delivery partner location tracking for {orderId}
</Text> </Text>
<View style={styles.blinkingBadge}> <View style={styles.blinkingBadge}>
<Text style={styles.blinkingText}> Live</Text> <Text style={styles.blinkingText}> Live</Text>
</View> </View>
</View> </View>
<View style={styles.agentSheet}> <View style={styles.agentSheet}>
<PrimaryButton
title="Simulate Order Delivered"
onPress={() => navigation.navigate('OrderDeliveredScreen', { orderId })}
style={{ marginBottom: 16 }}
/>
<Text style={styles.agentName}>Rahul Sharma</Text> <Text style={styles.agentName}>Rahul Sharma</Text>
<Text style={styles.agentRating}> 4.8</Text> <Text style={styles.agentRating}> 4.8</Text>
<Text style={styles.agentVehicle}>KA-01-AB-1234 Honda Activa</Text> <Text style={styles.agentVehicle}>KA-01-AB-1234 Honda Activa</Text>

View File

@ -5,20 +5,31 @@ import {
FlatList, FlatList,
TouchableOpacity, TouchableOpacity,
} from 'react-native'; } from 'react-native';
import { useNavigation, CompositeNavigationProp } from '@react-navigation/native';
import { BottomTabNavigationProp } from '@react-navigation/bottom-tabs';
import { StackNavigationProp } from '@react-navigation/stack';
import { getStyles } from './myOrdersScreen.styles'; import { getStyles } from './myOrdersScreen.styles';
import { Header, OrderHistoryCard } from '@components'; import { Header, OrderHistoryCard } from '@components';
import { useAppTheme } from '@theme'; import { useAppTheme } from '@theme';
import { AppStackParamList } from '../../../navigation/appStack';
import { MainTabParamList } from '../../../navigation/mainTabNavigator';
type MyOrdersNavProp = CompositeNavigationProp<
BottomTabNavigationProp<MainTabParamList, 'MyOrdersScreen'>,
StackNavigationProp<AppStackParamList>
>;
const TABS = ['All', 'Ongoing', 'Completed']; const TABS = ['All', 'Ongoing', 'Completed'];
const mockOrders = [ const mockOrders = [
{ id: '1', providerName: 'Pizza Planet', providerImage: '', orderDate: '29 Jun 2026', status: 'Delivered', total: 599 }, { id: 'ORD-591283', providerName: 'Pizza Planet', providerImage: '', orderDate: '29 Jun 2026', status: 'Delivered', total: 599 },
{ id: '2', providerName: 'Fresh Mart', providerImage: '', orderDate: '28 Jun 2026', status: 'Ongoing', total: 349 }, { id: 'ORD-771239', providerName: 'Fresh Mart', providerImage: '', orderDate: '28 Jun 2026', status: 'Ongoing', total: 349 },
{ id: '3', providerName: 'Burger Barn', providerImage: '', orderDate: '27 Jun 2026', status: 'Delivered', total: 449 }, { id: 'ORD-108239', providerName: 'Burger Barn', providerImage: '', orderDate: '27 Jun 2026', status: 'Delivered', total: 449 },
]; ];
export const MyOrdersScreen: React.FC = () => { export const MyOrdersScreen: React.FC = () => {
const { colors } = useAppTheme(); const { colors } = useAppTheme();
const styles = getStyles(colors); const styles = getStyles(colors);
const navigation = useNavigation<MyOrdersNavProp>();
const [activeTab, setActiveTab] = useState('All'); const [activeTab, setActiveTab] = useState('All');
const filteredOrders = activeTab === 'All' const filteredOrders = activeTab === 'All'
@ -62,8 +73,19 @@ export const MyOrdersScreen: React.FC = () => {
orderDate={item.orderDate} orderDate={item.orderDate}
status={item.status} status={item.status}
total={item.total} total={item.total}
onReorder={() => {}} onReorder={() => {
onDetails={() => {}} navigation.navigate('ProviderDetailsScreen', {
providerId: 'p1',
providerName: item.providerName,
});
}}
onDetails={() => {
if (item.status === 'Ongoing') {
navigation.navigate('OrderTrackingScreen', { orderId: item.id });
} else {
navigation.navigate('OrderDeliveredScreen', { orderId: item.id });
}
}}
/> />
)} )}
contentContainerStyle={styles.list} contentContainerStyle={styles.list}

View File

@ -4,7 +4,7 @@ import { getStyles } from './onboardingCompleteScreen.styles';
import { PrimaryButton } from '@components'; import { PrimaryButton } from '@components';
import { useAppTheme } from '@theme'; import { useAppTheme } from '@theme';
import { useAppDispatch } from '../../../hooks/useAppDispatch'; import { useAppDispatch } from '../../../hooks/useAppDispatch';
import { completeOnboarding } from '../../../store/authSlice'; import { completeOnboarding } from '../../../store/commonreducers/auth';
export const OnboardingCompleteScreen: React.FC = () => { export const OnboardingCompleteScreen: React.FC = () => {
const { colors } = useAppTheme(); const { colors } = useAppTheme();

View File

@ -1,16 +1,25 @@
import React from 'react'; import React from 'react';
import { View, Text } from 'react-native'; import { View, Text } from 'react-native';
import { useNavigation, useRoute, RouteProp } from '@react-navigation/native';
import { StackNavigationProp } from '@react-navigation/stack';
import { getStyles } from './orderConfirmedScreen.styles'; import { getStyles } from './orderConfirmedScreen.styles';
import { Header, PrimaryButton } from '@components'; import { Header, PrimaryButton } from '@components';
import { useAppTheme } from '@theme'; import { useAppTheme } from '@theme';
import { AppStackParamList } from '../../../navigation/appStack';
type OrderConfirmedNavProp = StackNavigationProp<AppStackParamList, 'OrderConfirmedScreen'>;
type OrderConfirmedRouteProp = RouteProp<AppStackParamList, 'OrderConfirmedScreen'>;
export const OrderConfirmedScreen: React.FC = () => { export const OrderConfirmedScreen: React.FC = () => {
const { colors } = useAppTheme(); const { colors } = useAppTheme();
const styles = getStyles(colors); const styles = getStyles(colors);
const navigation = useNavigation<OrderConfirmedNavProp>();
const route = useRoute<OrderConfirmedRouteProp>();
const orderId = route.params?.orderId || 'ORD-123456';
return ( return (
<View style={styles.container}> <View style={styles.container}>
<Header title="Order Confirmed" onBack={() => {}} /> <Header title="Order Confirmed" onBack={() => navigation.navigate('MainTabs')} />
<View style={styles.content}> <View style={styles.content}>
<Text style={styles.icon}></Text> <Text style={styles.icon}></Text>
<Text style={styles.title}>Order Confirmed!</Text> <Text style={styles.title}>Order Confirmed!</Text>
@ -18,12 +27,12 @@ export const OrderConfirmedScreen: React.FC = () => {
Your order has been placed successfully Your order has been placed successfully
</Text> </Text>
<View style={styles.orderCard}> <View style={styles.orderCard}>
<Text style={styles.orderId}>Order ID: ORD-123456</Text> <Text style={styles.orderId}>Order ID: {orderId}</Text>
<Text style={styles.orderEta}>Estimated delivery: 25-30 min</Text> <Text style={styles.orderEta}>Estimated delivery: 25-30 min</Text>
</View> </View>
</View> </View>
<View style={styles.footer}> <View style={styles.footer}>
<PrimaryButton title="View Tracking" onPress={() => {}} /> <PrimaryButton title="View Tracking" onPress={() => navigation.navigate('OrderTrackingScreen', { orderId })} />
</View> </View>
</View> </View>
); );

View File

@ -1,17 +1,23 @@
import React, { useState } from 'react'; import React, { useState } from 'react';
import { View, Text } from 'react-native'; import { View, Text } from 'react-native';
import { useNavigation } from '@react-navigation/native';
import { StackNavigationProp } from '@react-navigation/stack';
import { getStyles } from './orderDeliveredScreen.styles'; import { getStyles } from './orderDeliveredScreen.styles';
import { Header, RatingStars, PrimaryButton } from '@components'; import { Header, RatingStars, PrimaryButton } from '@components';
import { useAppTheme } from '@theme'; import { useAppTheme } from '@theme';
import { AppStackParamList } from '../../../navigation/appStack';
type OrderDeliveredNavProp = StackNavigationProp<AppStackParamList, 'OrderDeliveredScreen'>;
export const OrderDeliveredScreen: React.FC = () => { export const OrderDeliveredScreen: React.FC = () => {
const { colors } = useAppTheme(); const { colors } = useAppTheme();
const styles = getStyles(colors); const styles = getStyles(colors);
const navigation = useNavigation<OrderDeliveredNavProp>();
const [rating, setRating] = useState(0); const [rating, setRating] = useState(0);
return ( return (
<View style={styles.container}> <View style={styles.container}>
<Header title="Order Delivered" onBack={() => {}} /> <Header title="Order Delivered" onBack={() => navigation.navigate('MainTabs')} />
<View style={styles.content}> <View style={styles.content}>
<Text style={styles.icon}>🎉</Text> <Text style={styles.icon}>🎉</Text>
<Text style={styles.title}>Your order has been delivered!</Text> <Text style={styles.title}>Your order has been delivered!</Text>
@ -21,7 +27,7 @@ export const OrderDeliveredScreen: React.FC = () => {
<PrimaryButton <PrimaryButton
title="Rate & Tip" title="Rate & Tip"
onPress={() => {}} onPress={() => navigation.navigate('MainTabs')}
disabled={rating === 0} disabled={rating === 0}
style={{ marginTop: 32 }} style={{ marginTop: 32 }}
/> />

View File

@ -1,8 +1,14 @@
import React from 'react'; import React from 'react';
import { View, Text } from 'react-native'; import { View, Text } from 'react-native';
import { useNavigation, useRoute, RouteProp } from '@react-navigation/native';
import { StackNavigationProp } from '@react-navigation/stack';
import { getStyles } from './orderTrackingScreen.styles'; import { getStyles } from './orderTrackingScreen.styles';
import { Header, PrimaryButton } from '@components'; import { Header, PrimaryButton } from '@components';
import { useAppTheme } from '@theme'; import { useAppTheme } from '@theme';
import { AppStackParamList } from '../../../navigation/appStack';
type OrderTrackingNavProp = StackNavigationProp<AppStackParamList, 'OrderTrackingScreen'>;
type OrderTrackingRouteProp = RouteProp<AppStackParamList, 'OrderTrackingScreen'>;
const MILESTONES = [ const MILESTONES = [
{ key: 'placed', label: 'Order Placed', time: '12:30 PM' }, { key: 'placed', label: 'Order Placed', time: '12:30 PM' },
@ -14,10 +20,13 @@ const MILESTONES = [
export const OrderTrackingScreen: React.FC = () => { export const OrderTrackingScreen: React.FC = () => {
const { colors } = useAppTheme(); const { colors } = useAppTheme();
const styles = getStyles(colors); const styles = getStyles(colors);
const navigation = useNavigation<OrderTrackingNavProp>();
const route = useRoute<OrderTrackingRouteProp>();
const orderId = route.params?.orderId || 'ORD-123456';
return ( return (
<View style={styles.container}> <View style={styles.container}>
<Header title="Track Order" onBack={() => {}} /> <Header title={`Track Order (${orderId})`} onBack={() => navigation.goBack()} />
<View style={styles.content}> <View style={styles.content}>
<View style={styles.timeline}> <View style={styles.timeline}>
{MILESTONES.map((milestone, index) => ( {MILESTONES.map((milestone, index) => (
@ -54,9 +63,14 @@ export const OrderTrackingScreen: React.FC = () => {
))} ))}
</View> </View>
<View style={styles.supportCard}> <View style={styles.supportCard}>
<PrimaryButton
title="View Live Map"
onPress={() => navigation.navigate('LiveTrackingScreen', { orderId })}
style={{ marginBottom: 16 }}
/>
<Text style={styles.supportTitle}>Need help?</Text> <Text style={styles.supportTitle}>Need help?</Text>
<Text style={styles.supportSubtext}>Contact support for assistance</Text> <Text style={styles.supportSubtext}>Contact support for assistance</Text>
<PrimaryButton title="Contact Support" onPress={() => {}} /> <PrimaryButton title="Contact Support" onPress={() => navigation.navigate('HelpSupportScreen')} />
</View> </View>
</View> </View>
</View> </View>

View File

@ -12,7 +12,7 @@ import { getStyles } from './otpScreen.styles';
import { PrimaryButton } from '@components'; import { PrimaryButton } from '@components';
import { useAppTheme } from '@theme'; import { useAppTheme } from '@theme';
import { useAppDispatch } from '../../../hooks/useAppDispatch'; import { useAppDispatch } from '../../../hooks/useAppDispatch';
import { verifyOtp } from '../../../store/authSlice'; import { verifyOtp } from '../../../store/commonreducers/auth';
import { AuthStackParamList } from '../../../navigation/authStack'; import { AuthStackParamList } from '../../../navigation/authStack';
type OtpNavProp = StackNavigationProp<AuthStackParamList, 'OtpScreen'>; type OtpNavProp = StackNavigationProp<AuthStackParamList, 'OtpScreen'>;

View File

@ -1,95 +1,345 @@
import { StyleSheet } from 'react-native'; import { StyleSheet, Dimensions, Platform } from 'react-native';
import { typography } from '@theme'; import { typography } from '@theme';
export const getStyles = (colors: any) => StyleSheet.create({ const { width } = Dimensions.get('window');
container: { const HERO_HEIGHT = 220;
flex: 1,
backgroundColor: colors.background, export const getStyles = (colors: any) =>
}, StyleSheet.create({
content: { container: {
flex: 1, flex: 1,
}, backgroundColor: colors.background,
hero: { },
alignItems: 'center', content: {
paddingVertical: 24, flex: 1,
borderBottomWidth: 1, },
borderBottomColor: colors.border, contentBody: {
}, paddingBottom: 32,
heroImage: { },
width: 100,
height: 100, // ---------- Hero ----------
borderRadius: 50, heroWrap: {
backgroundColor: colors.inputBg, width,
justifyContent: 'center', height: HERO_HEIGHT,
alignItems: 'center', backgroundColor: colors.inputBg,
marginBottom: 12, },
}, heroImage: {
heroEmoji: { width: '100%',
fontSize: 44, height: '100%',
}, },
heroName: { heroFallback: {
fontSize: typography.fontSize.xl, width: '100%',
fontWeight: typography.fontWeight.bold, height: '100%',
color: colors.text, alignItems: 'center',
marginBottom: 8, justifyContent: 'center',
}, backgroundColor: colors.inputBg,
heroStats: { },
flexDirection: 'row', heroFallbackEmoji: {
gap: 16, fontSize: 56,
}, },
heroStat: { heroOverlay: {
fontSize: typography.fontSize.sm, position: 'absolute',
color: colors.textSecondary, left: 0,
}, right: 0,
categoryRow: { bottom: 0,
paddingVertical: 12, height: 90,
paddingHorizontal: 16, backgroundColor: 'rgba(0,0,0,0.28)',
flexGrow: 0, },
},
categoryTab: { // Floating top icon row (back / share / favorite)
paddingHorizontal: 20, topIconRow: {
paddingVertical: 8, position: 'absolute',
borderRadius: 20, top: Platform.OS === 'ios' ? 54 : 16,
borderWidth: 1, left: 16,
borderColor: colors.border, right: 16,
marginRight: 8, flexDirection: 'row',
}, justifyContent: 'space-between',
categoryTabActive: { alignItems: 'center',
borderColor: colors.primary, },
backgroundColor: '#E8F5E9', iconButton: {
}, width: 38,
categoryTabText: { height: 38,
fontSize: typography.fontSize.sm, borderRadius: 19,
color: colors.textSecondary, backgroundColor: 'rgba(255,255,255,0.92)',
}, alignItems: 'center',
categoryTabTextActive: { justifyContent: 'center',
color: colors.primary, ...Platform.select({
fontWeight: typography.fontWeight.semibold, ios: {
}, shadowColor: '#000',
cartStrip: { shadowOffset: { width: 0, height: 2 },
flexDirection: 'row', shadowOpacity: 0.15,
alignItems: 'center', shadowRadius: 4,
justifyContent: 'space-between', },
backgroundColor: colors.primary, android: { elevation: 3 },
paddingHorizontal: 20, }),
paddingVertical: 14, },
borderTopLeftRadius: 16, iconButtonText: {
borderTopRightRadius: 16, fontSize: 16,
}, },
cartStripText: { iconButtonGroup: {
fontSize: typography.fontSize.md, flexDirection: 'row',
fontWeight: typography.fontWeight.semibold, },
color: '#FFFFFF',
}, // ---------- Info card ----------
viewCartButton: { infoCard: {
backgroundColor: '#FFFFFF', backgroundColor: colors.background,
paddingHorizontal: 16, marginTop: -20,
paddingVertical: 8, borderTopLeftRadius: 24,
borderRadius: 8, borderTopRightRadius: 24,
}, paddingTop: 20,
viewCartText: { paddingHorizontal: 20,
fontSize: typography.fontSize.sm, paddingBottom: 16,
fontWeight: typography.fontWeight.semibold, ...Platform.select({
color: colors.primary, ios: {
}, shadowColor: '#000',
}); shadowOffset: { width: 0, height: -4 },
shadowOpacity: 0.05,
shadowRadius: 8,
},
android: { elevation: 4 },
}),
},
infoTopRow: {
flexDirection: 'row',
justifyContent: 'space-between',
alignItems: 'flex-start',
},
heroName: {
fontSize: typography.fontSize.xl,
fontWeight: typography.fontWeight.bold,
color: colors.text,
flex: 1,
marginRight: 12,
},
ratingBadge: {
flexDirection: 'row',
alignItems: 'center',
backgroundColor: colors.primary,
borderRadius: 8,
paddingHorizontal: 8,
paddingVertical: 5,
},
ratingBadgeText: {
fontSize: typography.fontSize.sm,
fontWeight: typography.fontWeight.bold,
color: '#FFFFFF',
marginLeft: 3,
},
cuisineText: {
fontSize: typography.fontSize.sm,
color: colors.textSecondary,
marginTop: 4,
},
metaRow: {
flexDirection: 'row',
alignItems: 'center',
marginTop: 12,
},
metaItem: {
flexDirection: 'row',
alignItems: 'center',
},
metaIcon: {
fontSize: 13,
marginRight: 4,
},
metaText: {
fontSize: typography.fontSize.sm,
color: colors.text,
fontWeight: typography.fontWeight.medium,
},
metaDivider: {
width: 3,
height: 3,
borderRadius: 1.5,
backgroundColor: colors.textSecondary,
marginHorizontal: 10,
opacity: 0.5,
},
statusRow: {
flexDirection: 'row',
alignItems: 'center',
marginTop: 10,
},
statusDot: {
width: 7,
height: 7,
borderRadius: 3.5,
backgroundColor: colors.primary,
marginRight: 6,
},
statusText: {
fontSize: typography.fontSize.xs,
color: colors.primary,
fontWeight: typography.fontWeight.semibold,
},
statusTextMuted: {
fontSize: typography.fontSize.xs,
color: colors.textSecondary,
},
// ---------- Offers ----------
offersSection: {
marginTop: 18,
},
offersScrollContent: {
paddingHorizontal: 20,
},
offerChip: {
flexDirection: 'row',
alignItems: 'center',
borderWidth: 1,
borderStyle: 'dashed',
borderColor: colors.primary,
backgroundColor: colors.primaryMuted ?? '#E9F7EF',
borderRadius: 10,
paddingVertical: 8,
paddingHorizontal: 12,
marginRight: 10,
},
offerIcon: {
fontSize: 15,
marginRight: 6,
},
offerText: {
fontSize: typography.fontSize.xs,
fontWeight: typography.fontWeight.semibold,
color: colors.text,
},
// ---------- Divider ----------
sectionDivider: {
height: 8,
backgroundColor: colors.inputBg,
marginTop: 18,
},
// ---------- Menu search ----------
menuHeaderRow: {
flexDirection: 'row',
alignItems: 'center',
justifyContent: 'space-between',
paddingHorizontal: 20,
paddingTop: 18,
paddingBottom: 4,
},
menuTitle: {
fontSize: typography.fontSize.lg,
fontWeight: typography.fontWeight.bold,
color: colors.text,
},
menuCount: {
fontSize: typography.fontSize.sm,
color: colors.textSecondary,
},
// ---------- Category tabs ----------
categoryRow: {
paddingVertical: 14,
paddingHorizontal: 20,
flexGrow: 0,
},
categoryTab: {
paddingHorizontal: 18,
paddingVertical: 9,
borderRadius: 22,
borderWidth: 1.5,
borderColor: colors.border,
marginRight: 10,
backgroundColor: colors.background,
},
categoryTabActive: {
borderColor: colors.primary,
backgroundColor: colors.primary,
},
categoryTabText: {
fontSize: typography.fontSize.sm,
color: colors.textSecondary,
fontWeight: typography.fontWeight.medium,
},
categoryTabTextActive: {
color: '#FFFFFF',
fontWeight: typography.fontWeight.bold,
},
// ---------- Menu list ----------
menuList: {
paddingHorizontal: 20,
},
emptyMenuWrap: {
alignItems: 'center',
paddingVertical: 40,
},
emptyMenuEmoji: {
fontSize: 34,
marginBottom: 8,
},
emptyMenuText: {
color: colors.textSecondary,
fontSize: typography.fontSize.sm,
},
// ---------- Cart strip ----------
cartStripWrap: {
position: 'absolute',
left: 16,
right: 16,
bottom: Platform.OS === 'ios' ? 28 : 16,
},
cartStrip: {
flexDirection: 'row',
alignItems: 'center',
justifyContent: 'space-between',
backgroundColor: colors.primary,
paddingHorizontal: 18,
paddingVertical: 14,
borderRadius: 16,
...Platform.select({
ios: {
shadowColor: '#000',
shadowOffset: { width: 0, height: 6 },
shadowOpacity: 0.25,
shadowRadius: 12,
},
android: { elevation: 8 },
}),
},
cartStripLeft: {
flexDirection: 'row',
alignItems: 'center',
},
cartBagIcon: {
fontSize: 18,
marginRight: 8,
},
cartStripTextWrap: {},
cartStripCount: {
fontSize: typography.fontSize.xs,
color: 'rgba(255,255,255,0.85)',
},
cartStripPrice: {
fontSize: typography.fontSize.md,
fontWeight: typography.fontWeight.bold,
color: '#FFFFFF',
},
viewCartButton: {
flexDirection: 'row',
alignItems: 'center',
backgroundColor: '#FFFFFF',
paddingHorizontal: 16,
paddingVertical: 9,
borderRadius: 10,
},
viewCartText: {
fontSize: typography.fontSize.sm,
fontWeight: typography.fontWeight.bold,
color: colors.primary,
marginRight: 4,
},
viewCartArrow: {
fontSize: 13,
color: colors.primary,
},
});

View File

@ -1,38 +1,289 @@
import React, { useState, useEffect } from 'react'; import React, { useEffect, useMemo, useState } from 'react';
import { import {
View, View,
Text, Text,
TouchableOpacity, TouchableOpacity,
ScrollView, ScrollView,
Image,
ImageBackground,
} from 'react-native'; } from 'react-native';
import { useNavigation, useRoute, RouteProp } from '@react-navigation/native';
import { StackNavigationProp } from '@react-navigation/stack';
import { getStyles } from './providerDetailsScreen.styles'; import { getStyles } from './providerDetailsScreen.styles';
import { Header, CatalogItemRow } from '@components'; import { CatalogItemRow } from '@components';
import { useAppTheme } from '@theme'; import { useAppTheme } from '@theme';
import { useAppDispatch, useAppSelector } from '../../../hooks/useAppDispatch'; import { useAppDispatch, useAppSelector } from '../../../hooks/useAppDispatch';
import { addItem } from '../../../store/cartSlice'; import { addItem } from '../../../store/commonreducers/cart';
import { deliveryService } from '../../../services/deliveryService'; import { getProviderCatalogApi, getProvidersApi } from '../../../api/deliveryApi';
import { CatalogItem } from '../../../interfaces'; import { CatalogItem, Provider } from '../../../interfaces';
import { AppStackParamList } from '../../../navigation/appStack';
const CATEGORIES = ['Pizza', 'Sides', 'Beverages']; type ProviderDetailsNavProp = StackNavigationProp<
AppStackParamList,
'ProviderDetailsScreen'
>;
type ProviderDetailsRouteProp = RouteProp<
AppStackParamList,
'ProviderDetailsScreen'
>;
const FALLBACK_CATEGORIES = ['Pizza', 'Sides', 'Beverages'];
const OFFERS = [
{ key: 'o1', icon: '🏷️', label: '50% OFF up to ₹100' },
{ key: 'o2', icon: '🚚', label: 'Free delivery above ₹299' },
{ key: 'o3', icon: '🎁', label: 'Buy 1 Get 1 on combos' },
];
// ---------------------------------------------------------------------------
// Presentational subcomponents
// Kept local to this screen since none are reused elsewhere yet. Promote to
// @components if a second screen needs them.
// ---------------------------------------------------------------------------
interface HeroSectionProps {
styles: ReturnType<typeof getStyles>;
imageUrl?: string;
onBack: () => void;
}
const HeroSection: React.FC<HeroSectionProps> = ({
styles,
imageUrl,
onBack,
}) => (
<View style={styles.heroWrap}>
{imageUrl ? (
<ImageBackground source={{ uri: imageUrl }} style={styles.heroImage}>
<View style={styles.heroOverlay} />
</ImageBackground>
) : (
<View style={styles.heroFallback}>
<Text style={styles.heroFallbackEmoji}>🍽</Text>
<View style={styles.heroOverlay} />
</View>
)}
<View style={styles.topIconRow}>
<TouchableOpacity
style={styles.iconButton}
activeOpacity={0.75}
onPress={onBack}
>
<Text style={styles.iconButtonText}></Text>
</TouchableOpacity>
<View style={styles.iconButtonGroup}>
<TouchableOpacity
style={[styles.iconButton, { marginRight: 10 }]}
activeOpacity={0.75}
>
<Text style={styles.iconButtonText}>🔗</Text>
</TouchableOpacity>
<TouchableOpacity style={styles.iconButton} activeOpacity={0.75}>
<Text style={styles.iconButtonText}>🤍</Text>
</TouchableOpacity>
</View>
</View>
</View>
);
interface InfoCardProps {
styles: ReturnType<typeof getStyles>;
name: string;
rating: number;
deliveryTime: string;
tag: string;
}
const InfoCard: React.FC<InfoCardProps> = ({
styles,
name,
rating,
deliveryTime,
tag,
}) => (
<View style={styles.infoCard}>
<View style={styles.infoTopRow}>
<Text style={styles.heroName} numberOfLines={1}>
{name}
</Text>
<View style={styles.ratingBadge}>
<Text style={{ fontSize: 11 }}></Text>
<Text style={styles.ratingBadgeText}>{rating.toFixed(1)}</Text>
</View>
</View>
<Text style={styles.cuisineText}>{tag} Multi-cuisine</Text>
<View style={styles.metaRow}>
<View style={styles.metaItem}>
<Text style={styles.metaIcon}>🕐</Text>
<Text style={styles.metaText}>{deliveryTime}</Text>
</View>
<View style={styles.metaDivider} />
<View style={styles.metaItem}>
<Text style={styles.metaIcon}>📍</Text>
<Text style={styles.metaText}>2.4 km away</Text>
</View>
</View>
<View style={styles.statusRow}>
<View style={styles.statusDot} />
<Text style={styles.statusText}>Open now</Text>
<Text style={styles.statusTextMuted}> Closes 11:30 PM</Text>
</View>
</View>
);
interface OffersRowProps {
styles: ReturnType<typeof getStyles>;
}
const OffersRow: React.FC<OffersRowProps> = ({ styles }) => (
<View style={styles.offersSection}>
<ScrollView
horizontal
showsHorizontalScrollIndicator={false}
contentContainerStyle={styles.offersScrollContent}
>
{OFFERS.map(offer => (
<View key={offer.key} style={styles.offerChip}>
<Text style={styles.offerIcon}>{offer.icon}</Text>
<Text style={styles.offerText}>{offer.label}</Text>
</View>
))}
</ScrollView>
</View>
);
interface CategoryTabsProps {
styles: ReturnType<typeof getStyles>;
categories: string[];
activeCategory: string;
onSelect: (category: string) => void;
}
const CategoryTabs: React.FC<CategoryTabsProps> = ({
styles,
categories,
activeCategory,
onSelect,
}) => (
<ScrollView
horizontal
showsHorizontalScrollIndicator={false}
contentContainerStyle={styles.categoryRow}
>
{categories.map(cat => {
const isActive = activeCategory === cat;
return (
<TouchableOpacity
key={cat}
style={[styles.categoryTab, isActive && styles.categoryTabActive]}
onPress={() => onSelect(cat)}
activeOpacity={0.75}
>
<Text
style={[
styles.categoryTabText,
isActive && styles.categoryTabTextActive,
]}
>
{cat}
</Text>
</TouchableOpacity>
);
})}
</ScrollView>
);
interface CartStripProps {
styles: ReturnType<typeof getStyles>;
totalItems: number;
totalPrice: number;
onPress: () => void;
}
const CartStrip: React.FC<CartStripProps> = ({
styles,
totalItems,
totalPrice,
onPress,
}) => (
<View style={styles.cartStripWrap}>
<TouchableOpacity
style={styles.cartStrip}
activeOpacity={0.85}
onPress={onPress}
>
<View style={styles.cartStripLeft}>
<Text style={styles.cartBagIcon}>🛍</Text>
<View style={styles.cartStripTextWrap}>
<Text style={styles.cartStripCount}>
{totalItems} item{totalItems === 1 ? '' : 's'}
</Text>
<Text style={styles.cartStripPrice}>{totalPrice}</Text>
</View>
</View>
<View style={styles.viewCartButton}>
<Text style={styles.viewCartText}>View Cart</Text>
<Text style={styles.viewCartArrow}></Text>
</View>
</TouchableOpacity>
</View>
);
// ---------------------------------------------------------------------------
// Screen
// ---------------------------------------------------------------------------
export const ProviderDetailsScreen: React.FC = () => { export const ProviderDetailsScreen: React.FC = () => {
const { colors } = useAppTheme(); const { colors } = useAppTheme();
const styles = getStyles(colors); const styles = getStyles(colors);
const dispatch = useAppDispatch(); const dispatch = useAppDispatch();
const cartItems = useAppSelector((state) => state.cart.items); const navigation = useNavigation<ProviderDetailsNavProp>();
const route = useRoute<ProviderDetailsRouteProp>();
const { providerId, providerName } = route.params;
const cartItems = useAppSelector(state => state.cart.items);
const [provider, setProvider] = useState<Provider | null>(null);
const [catalog, setCatalog] = useState<CatalogItem[]>([]); const [catalog, setCatalog] = useState<CatalogItem[]>([]);
const [activeCategory, setActiveCategory] = useState(CATEGORIES[0]); const [activeCategory, setActiveCategory] = useState(FALLBACK_CATEGORIES[0]);
const resolvedProviderId = providerId || 'p1';
const resolvedProviderName = providerName || 'Pizza Planet';
useEffect(() => { useEffect(() => {
deliveryService.getProviderCatalog('p1').then(setCatalog); getProviderCatalogApi(resolvedProviderId).then(setCatalog);
}, []); }, [resolvedProviderId]);
const filteredItems = catalog.filter((item) => item.category === activeCategory); useEffect(() => {
getProvidersApi().then(providers => {
const match = providers.find(p => p.id === resolvedProviderId);
if (match) setProvider(match);
});
}, [resolvedProviderId]);
const categories = useMemo(() => {
const fromCatalog = Array.from(new Set(catalog.map(item => item.category)));
return fromCatalog.length > 0 ? fromCatalog : FALLBACK_CATEGORIES;
}, [catalog]);
useEffect(() => {
if (!categories.includes(activeCategory)) {
setActiveCategory(categories[0]);
}
}, [categories, activeCategory]);
const filteredItems = useMemo(
() => catalog.filter(item => item.category === activeCategory),
[catalog, activeCategory],
);
const getItemQuantity = (itemId: string) => { const getItemQuantity = (itemId: string) => {
const item = cartItems.find((i) => i.item.id === itemId); const match = cartItems.find(i => i.item.id === itemId);
return item ? item.quantity : 0; return match ? match.quantity : 0;
}; };
const totalItems = cartItems.reduce((sum, i) => sum + i.quantity, 0); const totalItems = cartItems.reduce((sum, i) => sum + i.quantity, 0);
@ -41,80 +292,85 @@ export const ProviderDetailsScreen: React.FC = () => {
0, 0,
); );
const handleAddItem = (item: CatalogItem) => {
dispatch(
addItem({
providerId: resolvedProviderId,
providerName: resolvedProviderName,
item,
}),
);
};
return ( return (
<View style={styles.container}> <View style={styles.container}>
<Header title="Provider Details" onBack={() => {}} /> <ScrollView
<ScrollView style={styles.content}> style={styles.content}
<View style={styles.hero}> contentContainerStyle={styles.contentBody}
<View style={styles.heroImage}> showsVerticalScrollIndicator={false}
<Text style={styles.heroEmoji}>🍕</Text> >
</View> <HeroSection
<Text style={styles.heroName}>Pizza Planet</Text> styles={styles}
<View style={styles.heroStats}> imageUrl={provider?.imageUrl}
<Text style={styles.heroStat}> 4.5</Text> onBack={() => navigation.goBack()}
<Text style={styles.heroStat}>🕐 25 min</Text> />
<Text style={styles.heroStat}>🍕 Italian</Text>
</View> <InfoCard
styles={styles}
name={resolvedProviderName}
rating={provider?.rating ?? 4.5}
deliveryTime={provider?.deliveryTime ?? '25 min'}
tag={provider?.tag ?? 'Italian'}
/>
<OffersRow styles={styles} />
<View style={styles.sectionDivider} />
<View style={styles.menuHeaderRow}>
<Text style={styles.menuTitle}>Menu</Text>
<Text style={styles.menuCount}>{catalog.length} items</Text>
</View> </View>
<ScrollView <CategoryTabs
horizontal styles={styles}
showsHorizontalScrollIndicator={false} categories={categories}
style={styles.categoryRow} activeCategory={activeCategory}
> onSelect={setActiveCategory}
{CATEGORIES.map((cat) => ( />
<TouchableOpacity
key={cat}
style={[
styles.categoryTab,
activeCategory === cat && styles.categoryTabActive,
]}
onPress={() => setActiveCategory(cat)}
activeOpacity={0.7}
>
<Text
style={[
styles.categoryTabText,
activeCategory === cat && styles.categoryTabTextActive,
]}
>
{cat}
</Text>
</TouchableOpacity>
))}
</ScrollView>
{filteredItems.map((item) => ( <View style={styles.menuList}>
<CatalogItemRow {filteredItems.length === 0 && (
key={item.id} <View style={styles.emptyMenuWrap}>
imageUrl={item.imageUrl} <Text style={styles.emptyMenuEmoji}>🍽</Text>
title={item.title} <Text style={styles.emptyMenuText}>
description={item.description} No items in this category yet
price={item.price} </Text>
quantity={getItemQuantity(item.id)} </View>
onAdd={() => )}
dispatch(
addItem({ {filteredItems.map(item => (
providerId: 'p1', <CatalogItemRow
providerName: 'Pizza Planet', key={item.id}
item, imageUrl={item.imageUrl}
}), title={item.title}
) description={item.description}
} price={item.price}
onRemove={() => {}} quantity={getItemQuantity(item.id)}
/> onAdd={() => handleAddItem(item)}
))} onRemove={() => {}}
/>
))}
</View>
</ScrollView> </ScrollView>
{totalItems > 0 && ( {totalItems > 0 && (
<View style={styles.cartStrip}> <CartStrip
<Text style={styles.cartStripText}> styles={styles}
{totalItems} Items | {totalPrice} totalItems={totalItems}
</Text> totalPrice={totalPrice}
<TouchableOpacity style={styles.viewCartButton} activeOpacity={0.7}> onPress={() => navigation.navigate('CartScreen')}
<Text style={styles.viewCartText}>View Cart</Text> />
</TouchableOpacity>
</View>
)} )}
</View> </View>
); );

View File

@ -6,27 +6,37 @@ import {
TouchableOpacity, TouchableOpacity,
ScrollView, ScrollView,
} from 'react-native'; } from 'react-native';
import { useNavigation, useRoute, RouteProp } from '@react-navigation/native';
import { StackNavigationProp } from '@react-navigation/stack';
import { getStyles } from './providerListScreen.styles'; import { getStyles } from './providerListScreen.styles';
import { ProviderCard, Header } from '@components'; import { ProviderCard, Header } from '@components';
import { useAppTheme } from '@theme'; import { useAppTheme } from '@theme';
import { deliveryService } from '../../../services/deliveryService'; import { getProvidersApi } from '../../../api/deliveryApi';
import { Provider } from '../../../interfaces'; import { Provider } from '../../../interfaces';
import { AppStackParamList } from '../../../navigation/appStack';
const FILTERS = ['Filters', 'Rating 4.0+', 'Fast Delivery']; const FILTERS = ['Filters', 'Rating 4.0+', 'Fast Delivery'];
type ProviderListNavProp = StackNavigationProp<AppStackParamList, 'ProviderListScreen'>;
type ProviderListRouteProp = RouteProp<AppStackParamList, 'ProviderListScreen'>;
export const ProviderListScreen: React.FC = () => { export const ProviderListScreen: React.FC = () => {
const { colors } = useAppTheme(); const { colors } = useAppTheme();
const styles = getStyles(colors); const styles = getStyles(colors);
const navigation = useNavigation<ProviderListNavProp>();
const route = useRoute<ProviderListRouteProp>();
const category = route.params?.category;
const [providers, setProviders] = useState<Provider[]>([]); const [providers, setProviders] = useState<Provider[]>([]);
const [activeFilter, setActiveFilter] = useState<string | null>(null); const [activeFilter, setActiveFilter] = useState<string | null>(null);
useEffect(() => { useEffect(() => {
deliveryService.getProviders().then(setProviders); // If a category was passed, we can filter or search by it, but for mock data we fetch all
}, []); getProvidersApi().then(setProviders);
}, [category]);
return ( return (
<View style={styles.container}> <View style={styles.container}>
<Header title="Providers" onBack={() => {}} /> <Header title={category ? `${category} Providers` : 'Providers'} onBack={() => navigation.goBack()} />
<ScrollView horizontal showsHorizontalScrollIndicator={false} style={styles.filterRow}> <ScrollView horizontal showsHorizontalScrollIndicator={false} style={styles.filterRow}>
{FILTERS.map((filter) => ( {FILTERS.map((filter) => (
<TouchableOpacity <TouchableOpacity
@ -50,7 +60,7 @@ export const ProviderListScreen: React.FC = () => {
))} ))}
</ScrollView> </ScrollView>
<FlatList <FlatList
data={providers} data={category ? providers.filter(p => p.tag === category) : providers}
keyExtractor={(item) => item.id} keyExtractor={(item) => item.id}
renderItem={({ item }) => ( renderItem={({ item }) => (
<ProviderCard <ProviderCard
@ -60,6 +70,12 @@ export const ProviderListScreen: React.FC = () => {
deliveryTime={item.deliveryTime} deliveryTime={item.deliveryTime}
tag={item.tag} tag={item.tag}
discountText={item.discountText} discountText={item.discountText}
onPress={() =>
navigation.navigate('ProviderDetailsScreen', {
providerId: item.id,
providerName: item.name,
})
}
/> />
)} )}
contentContainerStyle={styles.list} contentContainerStyle={styles.list}

View File

@ -4,22 +4,33 @@ import {
Text, Text,
FlatList, FlatList,
} from 'react-native'; } from 'react-native';
import { useNavigation, CompositeNavigationProp } from '@react-navigation/native';
import { BottomTabNavigationProp } from '@react-navigation/bottom-tabs';
import { StackNavigationProp } from '@react-navigation/stack';
import { getStyles } from './searchScreen.styles'; import { getStyles } from './searchScreen.styles';
import { SearchBar, ProviderCard } from '@components'; import { SearchBar, ProviderCard } from '@components';
import { useAppTheme } from '@theme'; import { useAppTheme } from '@theme';
import { deliveryService } from '../../../services/deliveryService'; import { searchProvidersApi } from '../../../api/deliveryApi';
import { Provider } from '../../../interfaces'; import { Provider } from '../../../interfaces';
import { AppStackParamList } from '../../../navigation/appStack';
import { MainTabParamList } from '../../../navigation/mainTabNavigator';
type NavProp = CompositeNavigationProp<
BottomTabNavigationProp<MainTabParamList, 'SearchScreen'>,
StackNavigationProp<AppStackParamList>
>;
export const SearchScreen: React.FC = () => { export const SearchScreen: React.FC = () => {
const { colors } = useAppTheme(); const { colors } = useAppTheme();
const styles = getStyles(colors); const styles = getStyles(colors);
const navigation = useNavigation<NavProp>();
const [query, setQuery] = useState(''); const [query, setQuery] = useState('');
const [results, setResults] = useState<Provider[]>([]); const [results, setResults] = useState<Provider[]>([]);
useEffect(() => { useEffect(() => {
if (query.trim().length > 0) { if (query.trim().length > 0) {
deliveryService.searchProviders(query).then(setResults); searchProvidersApi(query).then(setResults);
} else { } else {
setResults([]); setResults([]);
} }
@ -43,6 +54,12 @@ export const SearchScreen: React.FC = () => {
deliveryTime={item.deliveryTime} deliveryTime={item.deliveryTime}
tag={item.tag} tag={item.tag}
discountText={item.discountText} discountText={item.discountText}
onPress={() =>
navigation.navigate('ProviderDetailsScreen', {
providerId: item.id,
providerName: item.name,
})
}
/> />
)} )}
ListEmptyComponent={ ListEmptyComponent={

View File

@ -1,75 +1,127 @@
import { StyleSheet } from 'react-native'; import { StyleSheet, Platform, Dimensions } from 'react-native';
import { typography } from '@theme'; import { typography } from '@theme';
export const getStyles = (colors: any) => StyleSheet.create({ const { width } = Dimensions.get('window');
container: {
flex: 1, export const getStyles = (colors: any) =>
backgroundColor: colors.background, StyleSheet.create({
}, container: {
scrollContainer: { flex: 1,
flexGrow: 1, backgroundColor: colors.background,
paddingTop: 60, },
},
mapPlaceholder: { // ---------- Map ----------
height: 240, map: {
backgroundColor: colors.inputBg, flex: 1,
marginHorizontal: 24, },
borderRadius: 16,
justifyContent: 'center', // ---------- Locate me FAB ----------
alignItems: 'center', locateButton: {
marginBottom: 24, position: 'absolute',
borderWidth: 1, right: 16,
borderColor: colors.border, bottom: Platform.OS === 'ios' ? 310 : 290,
}, width: 48,
mapIcon: { height: 48,
fontSize: 48, borderRadius: 24,
marginBottom: 8, backgroundColor: '#FFFFFF',
}, alignItems: 'center',
mapText: { justifyContent: 'center',
fontSize: typography.fontSize.lg, ...Platform.select({
fontWeight: typography.fontWeight.semibold, ios: {
color: colors.textSecondary, shadowColor: '#000',
}, shadowOffset: { width: 0, height: 3 },
mapSubtext: { shadowOpacity: 0.15,
fontSize: typography.fontSize.xs, shadowRadius: 6,
color: colors.placeholder, },
marginTop: 4, android: { elevation: 5 },
}, }),
card: { },
backgroundColor: colors.cardBg, locateIcon: {
borderRadius: 12, fontSize: 22,
padding: 16, },
marginHorizontal: 24,
marginBottom: 24, // ---------- Bottom sheet ----------
borderWidth: 1, bottomSheet: {
borderColor: colors.border, position: 'absolute',
}, left: 0,
cardTitle: { right: 0,
fontSize: typography.fontSize.sm, bottom: 0,
color: colors.textSecondary, backgroundColor: colors.background,
marginBottom: 4, borderTopLeftRadius: 24,
}, borderTopRightRadius: 24,
cardAddress: { paddingHorizontal: 20,
fontSize: typography.fontSize.md, paddingBottom: Platform.OS === 'ios' ? 34 : 20,
fontWeight: typography.fontWeight.semibold, ...Platform.select({
color: colors.text, ios: {
marginBottom: 8, shadowColor: '#000',
}, shadowOffset: { width: 0, height: -4 },
changeLink: { shadowOpacity: 0.1,
alignSelf: 'flex-end', shadowRadius: 12,
}, },
changeText: { android: { elevation: 12 },
fontSize: typography.fontSize.sm, }),
color: colors.primary, },
fontWeight: typography.fontWeight.semibold, sheetHandle: {
}, alignSelf: 'center',
manualButton: { width: 40,
alignItems: 'center', height: 4,
paddingVertical: 16, borderRadius: 2,
}, backgroundColor: colors.border,
manualButtonText: { marginTop: 10,
fontSize: typography.fontSize.sm, marginBottom: 16,
color: colors.primary, },
fontWeight: typography.fontWeight.semibold,
}, // ---------- Location card ----------
}); card: {
backgroundColor: colors.cardBg,
borderRadius: 16,
padding: 16,
marginBottom: 16,
borderWidth: 1,
borderColor: colors.border,
},
cardHeader: {
flexDirection: 'row',
alignItems: 'center',
marginBottom: 8,
},
cardIcon: {
fontSize: 18,
marginRight: 8,
},
cardTitle: {
fontSize: typography.fontSize.sm,
fontWeight: typography.fontWeight.semibold,
color: colors.textSecondary,
},
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: {
borderRadius: 14,
marginBottom: 8,
},
manualButton: {
alignItems: 'center',
paddingVertical: 14,
},
manualButtonText: {
fontSize: typography.fontSize.sm,
color: colors.primary,
fontWeight: typography.fontWeight.semibold,
},
});

View File

@ -1,54 +1,139 @@
import React from 'react'; import React, { useState, useEffect, useRef, useCallback } from 'react';
import { import {
View, View,
Text, Text,
TouchableOpacity, TouchableOpacity,
KeyboardAvoidingView,
Platform, Platform,
ScrollView, ActivityIndicator,
} from 'react-native'; } from 'react-native';
import MapView, { Marker, Region } from 'react-native-maps';
import { useNavigation } from '@react-navigation/native'; import { useNavigation } from '@react-navigation/native';
import { StackNavigationProp } from '@react-navigation/stack'; import { StackNavigationProp } from '@react-navigation/stack';
import { getStyles } from './setLocationScreen.styles'; import { getStyles } from './setLocationScreen.styles';
import { PrimaryButton } from '@components'; import { PrimaryButton } from '@components';
import { useAppTheme } from '@theme'; import { useAppTheme } from '@theme';
import { AuthStackParamList } from '../../../navigation/authStack'; import { AuthStackParamList } from '../../../navigation/authStack';
import {
DEFAULT_LOCATION,
getCurrentLocationWithAddress,
reverseGeocode,
LatLng,
} from '../../../services/locationServices';
type NavProp = StackNavigationProp<AuthStackParamList, 'SetLocationScreen'>; type NavProp = StackNavigationProp<AuthStackParamList, 'SetLocationScreen'>;
const DELTA = { latitudeDelta: 0.006, longitudeDelta: 0.006 };
export const SetLocationScreen: React.FC = () => { export const SetLocationScreen: React.FC = () => {
const { colors } = useAppTheme(); const { colors } = useAppTheme();
const styles = getStyles(colors); const styles = getStyles(colors);
const navigation = useNavigation<NavProp>(); const navigation = useNavigation<NavProp>();
const mapRef = useRef<MapView>(null);
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 () => {
setLoading(true);
try {
console.log('--------------');
const result = await getCurrentLocationWithAddress();
console.log('result', result);
setCoords(result.coords);
setAddress(result.address);
// Animate the map to the user's location
mapRef.current?.animateToRegion({ ...result.coords, ...DELTA }, 600);
} catch {
// Permission denied or GPS unavailable — keep the default location
console.log('--------error------');
setAddress('Could not determine location');
} finally {
setLoading(false);
}
}, []);
useEffect(() => {
fetchCurrentLocation();
}, [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 ( return (
<KeyboardAvoidingView <View style={styles.container}>
style={styles.container} {/* ---------- Map ---------- */}
behavior={Platform.OS === 'ios' ? 'padding' : 'height'} <MapView
> ref={mapRef}
<ScrollView contentContainerStyle={styles.scrollContainer}> style={styles.map}
<View style={styles.mapPlaceholder}> initialRegion={{ ...DEFAULT_LOCATION, ...DELTA }}
<Text style={styles.mapIcon}>🗺</Text> showsUserLocation
<Text style={styles.mapText}>Map View</Text> showsMyLocationButton={true}
<Text style={styles.mapSubtext}> onRegionChangeComplete={handleRegionChangeComplete}
(react-native-maps integration) >
</Text> <Marker
</View> coordinate={coords}
title="Your Location"
description={address}
/>
</MapView>
{/* ---------- "Locate me" floating button ---------- */}
<TouchableOpacity
style={styles.locateButton}
activeOpacity={0.8}
onPress={fetchCurrentLocation}
>
<Text style={styles.locateIcon}>📍</Text>
</TouchableOpacity>
{/* ---------- Bottom sheet ---------- */}
<View style={styles.bottomSheet}>
<View style={styles.sheetHandle} />
<View style={styles.card}> <View style={styles.card}>
<Text style={styles.cardTitle}>Delivery Location</Text> <View style={styles.cardHeader}>
<Text style={styles.cardAddress}> <Text style={styles.cardIcon}>📍</Text>
Koramangala 4th Block, Bengaluru, 560034 <Text style={styles.cardTitle}>My Location</Text>
</Text> </View>
{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}> <TouchableOpacity style={styles.changeLink} activeOpacity={0.7}>
<Text style={styles.changeText}>Change</Text> <Text style={styles.changeText}>Change</Text>
</TouchableOpacity> </TouchableOpacity>
</View> </View>
<PrimaryButton <PrimaryButton
title="Use this location" title="Confirm Location"
onPress={() => navigation.navigate('CompleteProfileScreen')} onPress={() => navigation.navigate('CompleteProfileScreen')}
style={{ marginHorizontal: 24 }} style={styles.confirmButton}
/> />
<TouchableOpacity <TouchableOpacity
@ -58,7 +143,7 @@ export const SetLocationScreen: React.FC = () => {
> >
<Text style={styles.manualButtonText}>Enter address manually</Text> <Text style={styles.manualButtonText}>Enter address manually</Text>
</TouchableOpacity> </TouchableOpacity>
</ScrollView> </View>
</KeyboardAvoidingView> </View>
); );
}; };

1
app/hooks/index.ts Normal file
View File

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

View File

@ -1,5 +0,0 @@
import { useDispatch, useSelector, TypedUseSelectorHook } from 'react-redux';
import type { RootState, AppDispatch } from '../store';
export const useAppDispatch = () => useDispatch<AppDispatch>();
export const useAppSelector: TypedUseSelectorHook<RootState> = useSelector;

View File

@ -0,0 +1,63 @@
import { useState, useCallback } from 'react';
import { useNavigation } from '@react-navigation/native';
import { StackNavigationProp } from '@react-navigation/stack';
import { AuthStackParamList } from 'app/navigation/authStack';
import { loginWithPhone, useAppDispatch } from '../store';
type LoginNavProp = StackNavigationProp<AuthStackParamList, 'LoginScreen'>;
const MOBILE_NUMBER_LENGTH = 10;
interface UseLoginScreenResult {
mobileNumber: string;
error: string | undefined;
onChangeMobileNumber: (text: string) => void;
handleLogin: () => void;
}
export const useLoginScreen = (): UseLoginScreenResult => {
const navigation = useNavigation<LoginNavProp>();
const dispatch = useAppDispatch();
const [mobileNumber, setMobileNumber] = useState('');
const [error, setError] = useState<string | undefined>(undefined);
const onChangeMobileNumber = useCallback(
(text: string) => {
setMobileNumber(text);
if (error) {
setError(undefined);
}
},
[error],
);
const validateMobileNumber = (value: string): string | undefined => {
if (!value) {
return 'Mobile number is required';
}
if (value.length < MOBILE_NUMBER_LENGTH) {
return 'Please enter a valid mobile number';
}
return undefined;
};
const handleLogin = useCallback(() => {
const validationError = validateMobileNumber(mobileNumber);
if (validationError) {
setError(validationError);
return;
}
setError(undefined);
dispatch(loginWithPhone(mobileNumber));
navigation.navigate('OtpScreen', { mobileNumber });
}, [mobileNumber, dispatch, navigation]);
return {
mobileNumber,
error,
onChangeMobileNumber,
handleLogin,
};
};

View File

@ -1,82 +1,50 @@
import React from 'react'; import React from 'react';
import { createBottomTabNavigator } from '@react-navigation/bottom-tabs'; import { createStackNavigator } from '@react-navigation/stack';
import { Text } from 'react-native'; import { NavigatorScreenParams } from '@react-navigation/native';
import { MainTabNavigator, MainTabParamList } from './mainTabNavigator';
import { import {
HomeScreen, ProviderListScreen,
SearchScreen, ProviderDetailsScreen,
MyOrdersScreen, CartScreen,
OffersScreen, CheckoutAddressScreen,
AccountScreen, CheckoutPaymentScreen,
OrderConfirmedScreen,
OrderTrackingScreen,
LiveTrackingScreen,
OrderDeliveredScreen,
HelpSupportScreen,
} from '@features/screens'; } from '@features/screens';
export type AppStackParamList = { export type AppStackParamList = {
HomeScreen: undefined; MainTabs: NavigatorScreenParams<MainTabParamList> | undefined;
SearchScreen: undefined; ProviderListScreen: { category?: string } | undefined;
MyOrdersScreen: undefined; ProviderDetailsScreen: { providerId: string; providerName?: string };
OffersScreen: undefined; CartScreen: undefined;
AccountScreen: undefined; CheckoutAddressScreen: undefined;
CheckoutPaymentScreen: undefined;
OrderConfirmedScreen: { orderId?: string } | undefined;
OrderTrackingScreen: { orderId?: string } | undefined;
LiveTrackingScreen: { orderId?: string } | undefined;
OrderDeliveredScreen: { orderId?: string } | undefined;
HelpSupportScreen: undefined;
}; };
const Tab = createBottomTabNavigator<AppStackParamList>(); const Stack = createStackNavigator<AppStackParamList>();
const tabIcons: Record<string, string> = {
HomeScreen: '🏠',
SearchScreen: '🔍',
MyOrdersScreen: '📦',
OffersScreen: '🏷️',
AccountScreen: '👤',
};
const renderTabIcon = (routeName: string, focused: boolean) => (
<Text style={{ fontSize: 22, opacity: focused ? 1 : 0.5 }}>
{tabIcons[routeName]}
</Text>
);
export const AppStack: React.FC = () => { export const AppStack: React.FC = () => {
return ( return (
<Tab.Navigator <Stack.Navigator screenOptions={{ headerShown: false }}>
screenOptions={({ route }) => ({ <Stack.Screen name="MainTabs" component={MainTabNavigator} />
headerShown: false, <Stack.Screen name="ProviderListScreen" component={ProviderListScreen} />
tabBarIcon: ({ focused }) => renderTabIcon(route.name, focused), <Stack.Screen name="ProviderDetailsScreen" component={ProviderDetailsScreen} />
tabBarActiveTintColor: '#05824C', <Stack.Screen name="CartScreen" component={CartScreen} />
tabBarInactiveTintColor: '#666666', <Stack.Screen name="CheckoutAddressScreen" component={CheckoutAddressScreen} />
tabBarStyle: { <Stack.Screen name="CheckoutPaymentScreen" component={CheckoutPaymentScreen} />
paddingBottom: 8, <Stack.Screen name="OrderConfirmedScreen" component={OrderConfirmedScreen} />
paddingTop: 8, <Stack.Screen name="OrderTrackingScreen" component={OrderTrackingScreen} />
height: 60, <Stack.Screen name="LiveTrackingScreen" component={LiveTrackingScreen} />
}, <Stack.Screen name="OrderDeliveredScreen" component={OrderDeliveredScreen} />
tabBarLabelStyle: { <Stack.Screen name="HelpSupportScreen" component={HelpSupportScreen} />
fontSize: 11, </Stack.Navigator>
fontWeight: '500',
},
})}
>
<Tab.Screen
name="HomeScreen"
component={HomeScreen}
options={{ tabBarLabel: 'Home' }}
/>
<Tab.Screen
name="SearchScreen"
component={SearchScreen}
options={{ tabBarLabel: 'Search' }}
/>
<Tab.Screen
name="MyOrdersScreen"
component={MyOrdersScreen}
options={{ tabBarLabel: 'Orders' }}
/>
<Tab.Screen
name="OffersScreen"
component={OffersScreen}
options={{ tabBarLabel: 'Offers' }}
/>
<Tab.Screen
name="AccountScreen"
component={AccountScreen}
options={{ tabBarLabel: 'Account' }}
/>
</Tab.Navigator>
); );
}; };

View File

@ -0,0 +1,82 @@
import React from 'react';
import { createBottomTabNavigator } from '@react-navigation/bottom-tabs';
import { Text } from 'react-native';
import {
HomeScreen,
SearchScreen,
MyOrdersScreen,
OffersScreen,
AccountScreen,
} from '@features/screens';
export type MainTabParamList = {
HomeScreen: undefined;
SearchScreen: undefined;
MyOrdersScreen: undefined;
OffersScreen: undefined;
AccountScreen: undefined;
};
const Tab = createBottomTabNavigator<MainTabParamList>();
const tabIcons: Record<string, string> = {
HomeScreen: '🏠',
SearchScreen: '🔍',
MyOrdersScreen: '📦',
OffersScreen: '🏷️',
AccountScreen: '👤',
};
const renderTabIcon = (routeName: string, focused: boolean) => (
<Text style={{ fontSize: 22, opacity: focused ? 1 : 0.5 }}>
{tabIcons[routeName]}
</Text>
);
export const MainTabNavigator: React.FC = () => {
return (
<Tab.Navigator
screenOptions={({ route }) => ({
headerShown: false,
tabBarIcon: ({ focused }) => renderTabIcon(route.name, focused),
tabBarActiveTintColor: '#05824C',
tabBarInactiveTintColor: '#666666',
tabBarStyle: {
paddingBottom: 8,
paddingTop: 8,
height: 60,
},
tabBarLabelStyle: {
fontSize: 11,
fontWeight: '500',
},
})}
>
<Tab.Screen
name="HomeScreen"
component={HomeScreen}
options={{ tabBarLabel: 'Home' }}
/>
<Tab.Screen
name="SearchScreen"
component={SearchScreen}
options={{ tabBarLabel: 'Search' }}
/>
<Tab.Screen
name="MyOrdersScreen"
component={MyOrdersScreen}
options={{ tabBarLabel: 'Orders' }}
/>
<Tab.Screen
name="OffersScreen"
component={OffersScreen}
options={{ tabBarLabel: 'Offers' }}
/>
<Tab.Screen
name="AccountScreen"
component={AccountScreen}
options={{ tabBarLabel: 'Account' }}
/>
</Tab.Navigator>
);
};

View File

@ -1,18 +1,212 @@
const delay = (ms: number) => new Promise<void>((resolve) => setTimeout(resolve, ms)); import axios, {
AxiosInstance,
AxiosRequestConfig,
InternalAxiosRequestConfig,
AxiosResponse,
AxiosError,
} from 'axios';
import AsyncStorage from '@react-native-async-storage/async-storage';
import { Store } from '@reduxjs/toolkit';
export const apiClient = { // ─── Storage Keys ────────────────────────────────────────────────────────────
async get<T>(url: string, mockData: T): Promise<T> { const STORAGE_KEYS = {
await delay(800); ACCESS_TOKEN: '@auth_access_token',
return mockData; REFRESH_TOKEN: '@auth_refresh_token',
} as const;
// ─── Config ──────────────────────────────────────────────────────────────────
const BASE_URL = 'https://api.example.com/v1'; // 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 post<T>(url: string, body: unknown, mockResponse: T): Promise<T> { async getRefreshToken(): Promise<string | null> {
await delay(1000); return AsyncStorage.getItem(STORAGE_KEYS.REFRESH_TOKEN);
return mockResponse;
}, },
async put<T>(url: string, body: unknown, mockResponse: T): Promise<T> { async setTokens(accessToken: string, refreshToken: string): Promise<void> {
await delay(800); await AsyncStorage.setItem(STORAGE_KEYS.ACCESS_TOKEN, accessToken);
return mockResponse; 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/login',
'/auth/register',
'/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}`;
}
}
// 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 };

View File

@ -1,49 +0,0 @@
import { apiClient } from './apiClient';
import { User } from '../interfaces';
const mockUser: User = {
id: 'user-001',
mobileNumber: '',
name: '',
email: '',
locationLabels: [],
};
export const authService = {
async login(mobileNumber: string) {
return apiClient.post(
'/auth/login',
{ mobileNumber },
{ success: true, message: 'OTP sent successfully' },
);
},
async verifyOtp(mobileNumber: string, otp: string) {
return apiClient.post(
'/auth/verify-otp',
{ mobileNumber, otp },
{
success: true,
user: {
...mockUser,
mobileNumber,
id: 'user-001',
},
},
);
},
async updateProfile(data: { name: string; email: string; locationLabels: string[] }) {
return apiClient.put('/auth/profile', data, {
success: true,
user: {
...mockUser,
...data,
},
});
},
async logout() {
return apiClient.post('/auth/logout', {}, { success: true });
},
};

View File

@ -1,71 +0,0 @@
import { apiClient } from './apiClient';
import { Provider, CatalogItem, Order, DeliveryAgent, Location } from '../interfaces';
const mockProviders: Provider[] = [
{ id: 'p1', imageUrl: '', name: 'Pizza Planet', rating: 4.5, deliveryTime: '25 min', tag: 'Food', discountText: '50% OFF', serviceType: 'Italian' },
{ id: 'p2', imageUrl: '', name: 'Fresh Mart', rating: 4.2, deliveryTime: '30 min', tag: 'Groceries', discountText: '20% OFF', serviceType: 'Grocery' },
{ id: 'p3', imageUrl: '', name: 'MediQuick', rating: 4.7, deliveryTime: '15 min', tag: 'Pharmacy', discountText: undefined, serviceType: 'Pharmacy' },
{ id: 'p4', imageUrl: '', name: 'Burger Barn', rating: 4.3, deliveryTime: '20 min', tag: 'Food', discountText: 'Buy 1 Get 1', serviceType: 'Fast Food' },
{ id: 'p5', imageUrl: '', name: 'Green Basket', rating: 4.0, deliveryTime: '35 min', tag: 'Groceries', discountText: undefined, serviceType: 'Organic' },
];
const mockCatalogItems: CatalogItem[] = [
{ id: 'i1', imageUrl: '', title: 'Margherita Pizza', description: 'Classic cheese and tomato', price: 299, category: 'Pizza' },
{ id: 'i2', imageUrl: '', title: 'Pepperoni Pizza', description: 'Loaded with pepperoni slices', price: 399, category: 'Pizza' },
{ id: 'i3', imageUrl: '', title: 'French Fries', description: 'Crispy golden fries', price: 99, category: 'Sides' },
{ id: 'i4', imageUrl: '', title: 'Garlic Bread', description: 'Toasted bread with garlic butter', price: 149, category: 'Sides' },
{ id: 'i5', imageUrl: '', title: 'Chocolate Shake', description: 'Rich chocolate milkshake', price: 199, category: 'Beverages' },
{ id: 'i6', imageUrl: '', title: 'Cola', description: 'Refreshing cold drink', price: 49, category: 'Beverages' },
];
export const deliveryService = {
async getProviders() {
return apiClient.get('/providers', mockProviders);
},
async getProviderCatalog(providerId: string) {
return apiClient.get(`/providers/${providerId}/catalog`, mockCatalogItems);
},
async searchProviders(query: string) {
const results = mockProviders.filter(
(p) =>
p.name.toLowerCase().includes(query.toLowerCase()) ||
p.tag.toLowerCase().includes(query.toLowerCase()),
);
return apiClient.get(`/search?q=${query}`, results);
},
async placeOrder(orderData: Partial<Order>) {
return apiClient.post('/orders', orderData, {
success: true,
order: {
id: `ORD-${Date.now()}`,
...orderData,
status: 'placed' as const,
createdAt: new Date().toISOString(),
},
});
},
async getDeliveryAgent() {
const agent: DeliveryAgent = {
name: 'Rahul Sharma',
rating: 4.8,
vehicleDetails: 'KA-01-AB-1234 - Honda Activa',
phoneNumber: '+91 98765 43210',
};
return apiClient.get('/delivery/agent', agent);
},
async getLiveLocation() {
const location: Location = {
latitude: 12.9352,
longitude: 77.6245,
address: 'Koramangala 4th Block',
city: 'Bengaluru',
pincode: '560034',
};
return apiClient.get('/delivery/live-location', location);
},
};

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

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

View File

@ -0,0 +1,182 @@
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
try {
const granted = await PermissionsAndroid.request(
PermissionsAndroid.PERMISSIONS.ACCESS_FINE_LOCATION,
{
title: 'Location Permission',
message: 'This app needs access to your location to set it on the map.',
buttonNeutral: 'Ask Me Later',
buttonNegative: 'Cancel',
buttonPositive: 'OK',
},
);
console.log('granted', granted);
return granted === PermissionsAndroid.RESULTS.GRANTED;
} catch {
return false;
}
};
/**
* Show an alert when the user denies location permission, with an option
* to open app settings.
*/
export const showPermissionDeniedAlert = (): void => {
Alert.alert(
'Location Permission Required',
'Please enable location permission in your device settings to use this feature.',
[
{ text: 'Cancel', style: 'cancel' },
{ text: 'Open Settings', onPress: () => Linking.openSettings() },
],
);
};
// ---------------------------------------------------------------------------
// Position helpers
// ---------------------------------------------------------------------------
/**
* Get the device's current GPS position.
* Wraps the callback-based Geolocation API in a Promise.
*/
export const getCurrentPosition = async (): Promise<LatLng> => {
try {
console.log('get current position');
console.log('Geolocation:', Geolocation);
console.log('getCurrentPosition:', Geolocation?.getCurrentPosition);
const location = await new Promise<LatLng>((resolve, reject) => {
Geolocation.getCurrentPosition(
(position: GeoPosition) => {
resolve({
latitude: position.coords.latitude,
longitude: position.coords.longitude,
});
},
(error: GeoError) => {
console.log('Geolocation Error:', error);
reject(error);
},
{
enableHighAccuracy: true,
timeout: 15000,
maximumAge: 10000,
forceRequestLocation: true,
showLocationDialog: true,
},
);
});
return location;
} catch (error) {
console.error('Failed to get current position:', error);
throw error;
}
};
// ---------------------------------------------------------------------------
// 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;
}
return 'Address not found';
} catch (error) {
console.log('reverseGeocode error', error);
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();
if (!hasPermission) {
showPermissionDeniedAlert();
throw new Error('Location permission denied');
}
const coords = await getCurrentPosition();
const address = await reverseGeocode(coords);
console.log('coords', coords);
console.log('address', address);
return { coords, address };
};

View File

@ -1,102 +0,0 @@
import { createSlice, createAsyncThunk, PayloadAction } from '@reduxjs/toolkit';
import { User, Location } from '../interfaces';
import { authService } from '../services/authService';
interface AuthState {
user: User | null;
isAuthenticated: boolean;
isLoading: boolean;
onboardingComplete: boolean;
error: string | null;
}
const initialState: AuthState = {
user: null,
isAuthenticated: false,
isLoading: false,
onboardingComplete: false,
error: null,
};
export const loginWithPhone = createAsyncThunk(
'auth/loginWithPhone',
async (mobileNumber: string) => {
const response = await authService.login(mobileNumber);
return response;
},
);
export const verifyOtp = createAsyncThunk(
'auth/verifyOtp',
async ({ mobileNumber, otp }: { mobileNumber: string; otp: string }) => {
const response = await authService.verifyOtp(mobileNumber, otp);
return response;
},
);
const authSlice = createSlice({
name: 'auth',
initialState,
reducers: {
setUser(state, action: PayloadAction<User>) {
state.user = action.payload;
},
setLocation(state, _action: PayloadAction<Location>) {
if (state.user) {
state.user = { ...state.user };
}
},
completeProfile(state, action: PayloadAction<{ name: string; email: string; locationLabels: string[] }>) {
if (state.user) {
state.user = {
...state.user,
name: action.payload.name,
email: action.payload.email,
locationLabels: action.payload.locationLabels,
};
}
},
completeOnboarding(state) {
state.onboardingComplete = true;
},
logout(state) {
state.user = null;
state.isAuthenticated = false;
state.onboardingComplete = false;
state.error = null;
},
clearError(state) {
state.error = null;
},
},
extraReducers: (builder) => {
builder
.addCase(loginWithPhone.pending, (state) => {
state.isLoading = true;
state.error = null;
})
.addCase(loginWithPhone.fulfilled, (state) => {
state.isLoading = false;
})
.addCase(loginWithPhone.rejected, (state, action) => {
state.isLoading = false;
state.error = action.error.message || 'Login failed';
})
.addCase(verifyOtp.pending, (state) => {
state.isLoading = true;
state.error = null;
})
.addCase(verifyOtp.fulfilled, (state, action) => {
state.isLoading = false;
state.isAuthenticated = true;
state.user = action.payload.user;
})
.addCase(verifyOtp.rejected, (state, action) => {
state.isLoading = false;
state.error = action.error.message || 'OTP verification failed';
});
},
});
export const { setUser, setLocation, completeProfile, completeOnboarding, logout, clearError } = authSlice.actions;
export default authSlice.reducer;

View File

@ -1,96 +0,0 @@
import { createSlice, PayloadAction } from '@reduxjs/toolkit';
import { CartItem, CatalogItem } from '../interfaces';
interface CartState {
items: CartItem[];
couponCode: string | null;
discount: number;
providerId: string | null;
providerName: string | null;
}
const initialState: CartState = {
items: [],
couponCode: null,
discount: 0,
providerId: null,
providerName: null,
};
const cartSlice = createSlice({
name: 'cart',
initialState,
reducers: {
addItem(state, action: PayloadAction<{ providerId: string; providerName: string; item: CatalogItem }>) {
const { providerId, providerName, item } = action.payload;
if (state.providerId && state.providerId !== providerId) {
state.items = [];
}
state.providerId = providerId;
state.providerName = providerName;
const existing = state.items.find((i) => i.item.id === item.id);
if (existing) {
existing.quantity += 1;
} else {
state.items.push({
id: `${providerId}-${item.id}`,
providerId,
providerName,
item,
quantity: 1,
});
}
},
removeItem(state, action: PayloadAction<string>) {
const existing = state.items.find((i) => i.item.id === action.payload);
if (existing) {
if (existing.quantity > 1) {
existing.quantity -= 1;
} else {
state.items = state.items.filter((i) => i.item.id !== action.payload);
}
}
if (state.items.length === 0) {
state.providerId = null;
state.providerName = null;
}
},
clearCart(state) {
state.items = [];
state.couponCode = null;
state.discount = 0;
state.providerId = null;
state.providerName = null;
},
applyCoupon(state, action: PayloadAction<string>) {
state.couponCode = action.payload;
state.discount = 50;
},
removeCoupon(state) {
state.couponCode = null;
state.discount = 0;
},
},
});
export const { addItem, removeItem, clearCart, applyCoupon, removeCoupon } = cartSlice.actions;
export const selectCartTotal = (state: { cart: CartState }) => {
const subtotal = state.cart.items.reduce(
(sum, item) => sum + item.item.price * item.quantity,
0,
);
const deliveryFee = 40;
const platformFee = 10;
const discount = state.cart.discount;
return {
subtotal,
deliveryFee,
platformFee,
discount,
total: Math.max(0, subtotal + deliveryFee + platformFee - discount),
itemCount: state.cart.items.reduce((sum, item) => sum + item.quantity, 0),
};
};
export default cartSlice.reducer;

View File

@ -0,0 +1,16 @@
export { default as authReducer } from './reducer';
export {
setUser,
setLocation,
completeProfile,
completeOnboarding,
logout,
clearError,
} from './reducer';
export type { AuthState } from './reducer';
export {
loginWithPhone,
verifyOtp,
updateProfile,
logoutUser,
} from './thunk';

View File

@ -0,0 +1,130 @@
import { createAction, createReducer } from '@reduxjs/toolkit';
import { User, Location } from '../../../interfaces';
import { loginWithPhone, verifyOtp, updateProfile, logoutUser } from './thunk';
// ─── State ───────────────────────────────────────────────────────────────────
export interface AuthState {
user: User | null;
isAuthenticated: boolean;
isLoading: boolean;
onboardingComplete: boolean;
error: string | null;
}
const initialState: AuthState = {
user: null,
isAuthenticated: false,
isLoading: false,
onboardingComplete: false,
error: null,
};
// ─── Sync Actions ────────────────────────────────────────────────────────────
export const setUser = createAction<User>('auth/setUser');
export const setLocation = createAction<Location>('auth/setLocation');
export const completeProfile = createAction<{
name: string;
email: string;
locationLabels: string[];
}>('auth/completeProfile');
export const completeOnboarding = createAction('auth/completeOnboarding');
export const logout = createAction('auth/logout');
export const clearError = createAction('auth/clearError');
// ─── Reducer ─────────────────────────────────────────────────────────────────
const authReducer = createReducer(initialState, builder => {
builder
// ── Sync Actions ───────────────────────────────────────────────────────
.addCase(setUser, (state, action) => {
state.user = action.payload;
})
.addCase(setLocation, (state, _action) => {
if (state.user) {
state.user = { ...state.user };
}
})
.addCase(completeProfile, (state, action) => {
if (state.user) {
state.user = {
...state.user,
name: action.payload.name,
email: action.payload.email,
locationLabels: action.payload.locationLabels,
};
}
})
.addCase(completeOnboarding, state => {
state.onboardingComplete = true;
})
.addCase(logout, state => {
state.user = null;
state.isAuthenticated = false;
state.onboardingComplete = false;
state.error = null;
})
.addCase(clearError, state => {
state.error = null;
})
// ── loginWithPhone ─────────────────────────────────────────────────────
.addCase(loginWithPhone.pending, state => {
state.isLoading = true;
state.error = null;
})
.addCase(loginWithPhone.fulfilled, state => {
state.isLoading = false;
})
.addCase(loginWithPhone.rejected, (state, action) => {
state.isLoading = false;
state.error = (action.payload as string) || 'Login failed';
})
// ── verifyOtp ──────────────────────────────────────────────────────────
.addCase(verifyOtp.pending, state => {
state.isLoading = true;
state.error = null;
})
.addCase(verifyOtp.fulfilled, (state, action) => {
state.isLoading = false;
state.isAuthenticated = true;
state.user = action.payload.user;
})
.addCase(verifyOtp.rejected, (state, action) => {
state.isLoading = false;
state.error = (action.payload as string) || 'OTP verification failed';
})
// ── updateProfile ──────────────────────────────────────────────────────
.addCase(updateProfile.pending, state => {
state.isLoading = true;
state.error = null;
})
.addCase(updateProfile.fulfilled, (state, action) => {
state.isLoading = false;
state.user = action.payload.user;
})
.addCase(updateProfile.rejected, (state, action) => {
state.isLoading = false;
state.error = (action.payload as string) || 'Profile update failed';
})
// ── logoutUser ─────────────────────────────────────────────────────────
.addCase(logoutUser.fulfilled, state => {
state.user = null;
state.isAuthenticated = false;
state.onboardingComplete = false;
state.error = null;
})
.addCase(logoutUser.rejected, state => {
// Still clear state even if API fails (tokens already cleared in thunk)
state.user = null;
state.isAuthenticated = false;
state.onboardingComplete = false;
state.error = null;
});
});
export default authReducer;

View File

@ -0,0 +1,85 @@
import { createAsyncThunk } from '@reduxjs/toolkit';
import { loginApi, verifyOtpApi, updateProfileApi, logoutApi } from '../../../api/authApi';
import { tokenManager } from '../../../services/apiClient';
// ─── Login ───────────────────────────────────────────────────────────────────
export const loginWithPhone = createAsyncThunk(
'auth/loginWithPhone',
async (mobileNumber: string, { rejectWithValue }) => {
try {
const response = await loginApi(mobileNumber);
return response;
} catch (error: unknown) {
const message =
error instanceof Error ? error.message : 'Login failed';
return rejectWithValue(message);
}
},
);
// ─── Verify OTP ──────────────────────────────────────────────────────────────
export const verifyOtp = createAsyncThunk(
'auth/verifyOtp',
async (
{ mobileNumber, otp }: { mobileNumber: string; otp: string },
{ rejectWithValue },
) => {
try {
const response = await verifyOtpApi(mobileNumber, otp);
// 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);
}
},
);
// ─── Update Profile ──────────────────────────────────────────────────────────
export const updateProfile = createAsyncThunk(
'auth/updateProfile',
async (
data: { name: string; email: string; locationLabels: string[] },
{ rejectWithValue },
) => {
try {
const response = await updateProfileApi(data);
return response;
} catch (error: unknown) {
const message =
error instanceof Error ? error.message : 'Profile update failed';
return rejectWithValue(message);
}
},
);
// ─── Logout ──────────────────────────────────────────────────────────────────
export const logoutUser = createAsyncThunk(
'auth/logoutUser',
async (_, { rejectWithValue }) => {
try {
const response = await logoutApi();
await tokenManager.clearTokens();
return response;
} catch (error: unknown) {
// Clear tokens even if API call fails
await tokenManager.clearTokens();
const message =
error instanceof Error ? error.message : 'Logout failed';
return rejectWithValue(message);
}
},
);

View File

@ -0,0 +1,10 @@
export { default as cartReducer } from './reducer';
export {
addItem,
removeItem,
clearCart,
applyCoupon,
removeCoupon,
selectCartTotal,
} from './reducer';
export type { CartState } from './reducer';

View File

@ -0,0 +1,114 @@
import { createAction, createReducer } from '@reduxjs/toolkit';
import { CartItem, CatalogItem } from '../../../interfaces';
// ─── State ───────────────────────────────────────────────────────────────────
export interface CartState {
items: CartItem[];
couponCode: string | null;
discount: number;
providerId: string | null;
providerName: string | null;
}
const initialState: CartState = {
items: [],
couponCode: null,
discount: 0,
providerId: null,
providerName: null,
};
// ─── Actions ─────────────────────────────────────────────────────────────────
export const addItem = createAction<{
providerId: string;
providerName: string;
item: CatalogItem;
}>('cart/addItem');
export const removeItem = createAction<string>('cart/removeItem');
export const clearCart = createAction('cart/clearCart');
export const applyCoupon = createAction<string>('cart/applyCoupon');
export const removeCoupon = createAction('cart/removeCoupon');
// ─── Selector ────────────────────────────────────────────────────────────────
export const selectCartTotal = (state: { cart: CartState }) => {
const subtotal = state.cart.items.reduce(
(sum, item) => sum + item.item.price * item.quantity,
0,
);
const deliveryFee = 40;
const platformFee = 10;
const discount = state.cart.discount;
return {
subtotal,
deliveryFee,
platformFee,
discount,
total: Math.max(0, subtotal + deliveryFee + platformFee - discount),
itemCount: state.cart.items.reduce((sum, item) => sum + item.quantity, 0),
};
};
// ─── Reducer ─────────────────────────────────────────────────────────────────
const cartReducer = createReducer(initialState, builder => {
builder
.addCase(addItem, (state, action) => {
const { providerId, providerName, item } = action.payload;
// Clear cart if switching providers
if (state.providerId && state.providerId !== providerId) {
state.items = [];
}
state.providerId = providerId;
state.providerName = providerName;
const existing = state.items.find(i => i.item.id === item.id);
if (existing) {
existing.quantity += 1;
} else {
state.items.push({
id: `${providerId}-${item.id}`,
providerId,
providerName,
item,
quantity: 1,
});
}
})
.addCase(removeItem, (state, action) => {
const existing = state.items.find(i => i.item.id === action.payload);
if (existing) {
if (existing.quantity > 1) {
existing.quantity -= 1;
} else {
state.items = state.items.filter(i => i.item.id !== action.payload);
}
}
if (state.items.length === 0) {
state.providerId = null;
state.providerName = null;
}
})
.addCase(clearCart, state => {
state.items = [];
state.couponCode = null;
state.discount = 0;
state.providerId = null;
state.providerName = null;
})
.addCase(applyCoupon, (state, action) => {
state.couponCode = action.payload;
state.discount = 50;
})
.addCase(removeCoupon, state => {
state.couponCode = null;
state.discount = 0;
});
});
export default cartReducer;

View File

@ -0,0 +1,2 @@
// Cart thunks — placeholder for future async cart operations
// (e.g., syncing cart with backend, applying server-side coupons)

View File

@ -0,0 +1,3 @@
export * from './auth';
export * from './cart';
export * from './order';

View File

@ -0,0 +1,9 @@
export { default as orderReducer } from './reducer';
export {
placeOrder,
updateOrderStatus,
setDeliveryAgent,
updateLiveLocation,
addOrder,
} from './reducer';
export type { OrderState } from './reducer';

View File

@ -0,0 +1,82 @@
import { createAction, createReducer } from '@reduxjs/toolkit';
import { Order, OrderStatus, TrackingStep, DeliveryAgent, Location } from '../../../interfaces';
// ─── State ───────────────────────────────────────────────────────────────────
export interface OrderState {
currentOrder: Order | null;
orders: Order[];
trackingSteps: TrackingStep[];
deliveryAgent: DeliveryAgent | null;
liveLocation: Location | null;
}
const initialState: OrderState = {
currentOrder: null,
orders: [],
trackingSteps: [],
deliveryAgent: null,
liveLocation: null,
};
// ─── Helpers ─────────────────────────────────────────────────────────────────
const buildTrackingSteps = (status: OrderStatus): TrackingStep[] => {
const steps: { key: OrderStatus; label: string }[] = [
{ key: 'placed', label: 'Order Placed' },
{ key: 'confirmed', label: 'Confirmed' },
{ key: 'dispatched', label: 'Dispatched' },
{ key: 'delivered', label: 'Delivered' },
];
const currentIndex = steps.findIndex(s => s.key === status);
return steps.map((step, index) => ({
...step,
isCompleted: index < currentIndex,
isActive: index === currentIndex,
}));
};
// ─── Actions ─────────────────────────────────────────────────────────────────
export const placeOrder = createAction<Order>('order/placeOrder');
export const updateOrderStatus = createAction<OrderStatus>('order/updateOrderStatus');
export const setDeliveryAgent = createAction<DeliveryAgent>('order/setDeliveryAgent');
export const updateLiveLocation = createAction<Location>('order/updateLiveLocation');
export const addOrder = createAction<Order>('order/addOrder');
// ─── Reducer ─────────────────────────────────────────────────────────────────
const orderReducer = createReducer(initialState, builder => {
builder
.addCase(placeOrder, (state, action) => {
state.currentOrder = action.payload;
state.orders.unshift(action.payload);
state.trackingSteps = buildTrackingSteps('placed');
})
.addCase(updateOrderStatus, (state, action) => {
if (state.currentOrder) {
state.currentOrder.status = action.payload;
state.trackingSteps = buildTrackingSteps(action.payload);
const idx = state.orders.findIndex(
o => o.id === state.currentOrder!.id,
);
if (idx !== -1) {
state.orders[idx] = {
...state.orders[idx],
status: action.payload,
};
}
}
})
.addCase(setDeliveryAgent, (state, action) => {
state.deliveryAgent = action.payload;
})
.addCase(updateLiveLocation, (state, action) => {
state.liveLocation = action.payload;
})
.addCase(addOrder, (state, action) => {
state.orders.unshift(action.payload);
});
});
export default orderReducer;

View File

@ -0,0 +1,2 @@
// Order thunks — placeholder for future async order operations
// (e.g., fetching order history, real-time status polling)

View File

@ -1,15 +1,4 @@
import { configureStore } from '@reduxjs/toolkit'; export * from './commonreducers';
import authReducer from './authSlice'; export * from './store';
import cartReducer from './cartSlice'; export * from './rootReducer';
import orderReducer from './orderSlice'; export * from './migration';
export const store = configureStore({
reducer: {
auth: authReducer,
cart: cartReducer,
order: orderReducer,
},
});
export type RootState = ReturnType<typeof store.getState>;
export type AppDispatch = typeof store.dispatch;

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

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

View File

@ -1,67 +0,0 @@
import { createSlice, PayloadAction } from '@reduxjs/toolkit';
import { Order, OrderStatus, TrackingStep, DeliveryAgent, Location } from '../interfaces';
interface OrderState {
currentOrder: Order | null;
orders: Order[];
trackingSteps: TrackingStep[];
deliveryAgent: DeliveryAgent | null;
liveLocation: Location | null;
}
const buildTrackingSteps = (status: OrderStatus): TrackingStep[] => {
const steps: { key: OrderStatus; label: string }[] = [
{ key: 'placed', label: 'Order Placed' },
{ key: 'confirmed', label: 'Confirmed' },
{ key: 'dispatched', label: 'Dispatched' },
{ key: 'delivered', label: 'Delivered' },
];
const currentIndex = steps.findIndex((s) => s.key === status);
return steps.map((step, index) => ({
...step,
isCompleted: index < currentIndex,
isActive: index === currentIndex,
}));
};
const initialState: OrderState = {
currentOrder: null,
orders: [],
trackingSteps: [],
deliveryAgent: null,
liveLocation: null,
};
const orderSlice = createSlice({
name: 'order',
initialState,
reducers: {
placeOrder(state, action: PayloadAction<Order>) {
state.currentOrder = action.payload;
state.orders.unshift(action.payload);
state.trackingSteps = buildTrackingSteps('placed');
},
updateOrderStatus(state, action: PayloadAction<OrderStatus>) {
if (state.currentOrder) {
state.currentOrder.status = action.payload;
state.trackingSteps = buildTrackingSteps(action.payload);
const idx = state.orders.findIndex((o) => o.id === state.currentOrder!.id);
if (idx !== -1) {
state.orders[idx] = { ...state.orders[idx], status: action.payload };
}
}
},
setDeliveryAgent(state, action: PayloadAction<DeliveryAgent>) {
state.deliveryAgent = action.payload;
},
updateLiveLocation(state, action: PayloadAction<Location>) {
state.liveLocation = action.payload;
},
addOrder(state, action: PayloadAction<Order>) {
state.orders.unshift(action.payload);
},
},
});
export const { placeOrder, updateOrderStatus, setDeliveryAgent, updateLiveLocation, addOrder } = orderSlice.actions;
export default orderSlice.reducer;

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

@ -0,0 +1,13 @@
import { combineReducers } from '@reduxjs/toolkit';
import { authReducer } from './commonreducers/auth';
import { cartReducer } from './commonreducers/cart';
import { orderReducer } from './commonreducers/order';
const rootReducer = combineReducers({
auth: authReducer,
cart: cartReducer,
order: orderReducer,
});
export type RootState = ReturnType<typeof rootReducer>;
export default rootReducer;

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

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

View File

@ -10,22 +10,27 @@
"test": "jest" "test": "jest"
}, },
"dependencies": { "dependencies": {
"@react-native-async-storage/async-storage": "^3.1.1",
"@react-native-community/checkbox": "^0.5.20", "@react-native-community/checkbox": "^0.5.20",
"@react-native/new-app-screen": "0.86.0", "@react-native/new-app-screen": "0.86.0",
"@react-navigation/bottom-tabs": "^7.18.3", "@react-navigation/bottom-tabs": "^7.18.3",
"@react-navigation/native": "^7.3.4", "@react-navigation/native": "^7.3.4",
"@react-navigation/stack": "^7.10.6", "@react-navigation/stack": "^7.10.6",
"@reduxjs/toolkit": "^2.12.0", "@reduxjs/toolkit": "^2.12.0",
"axios": "^1.18.1",
"react": "19.2.3", "react": "19.2.3",
"react-native": "0.86.0", "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-maps": "^1.29.0",
"react-native-reanimated": "^4.5.0", "react-native-reanimated": "^4.5.0",
"react-native-safe-area-context": "^5.8.0", "react-native-safe-area-context": "^5.8.0",
"react-native-screens": "^4.25.2", "react-native-screens": "^4.25.2",
"react-native-svg": "^15.15.5", "react-native-svg": "^15.15.5",
"react-native-worklets": "^0.10.0", "react-native-worklets": "^0.10.0",
"react-redux": "^9.3.0" "react-redux": "^9.3.0",
"redux-persist": "^6.0.0",
"redux-thunk": "^3.1.0"
}, },
"devDependencies": { "devDependencies": {
"@babel/core": "^7.25.2", "@babel/core": "^7.25.2",

194
yarn.lock
View File

@ -1026,6 +1026,13 @@
resolved "https://registry.yarnpkg.com/@bcoe/v8-coverage/-/v8-coverage-0.2.3.tgz#75a2e8b51cb758a7553d6804a5932d7aace75c39" resolved "https://registry.yarnpkg.com/@bcoe/v8-coverage/-/v8-coverage-0.2.3.tgz#75a2e8b51cb758a7553d6804a5932d7aace75c39"
integrity sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw== 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": "@eslint-community/eslint-utils@^4.2.0", "@eslint-community/eslint-utils@^4.9.1":
version "4.9.1" version "4.9.1"
resolved "https://registry.yarnpkg.com/@eslint-community/eslint-utils/-/eslint-utils-4.9.1.tgz#4e90af67bc51ddee6cdef5284edf572ec376b595" resolved "https://registry.yarnpkg.com/@eslint-community/eslint-utils/-/eslint-utils-4.9.1.tgz#4e90af67bc51ddee6cdef5284edf572ec376b595"
@ -1372,6 +1379,13 @@
"@nodelib/fs.scandir" "2.1.5" "@nodelib/fs.scandir" "2.1.5"
fastq "^1.6.0" fastq "^1.6.0"
"@react-native-async-storage/async-storage@^3.1.1":
version "3.1.1"
resolved "https://registry.yarnpkg.com/@react-native-async-storage/async-storage/-/async-storage-3.1.1.tgz#051caeb93919260041e53165f1b6c8a975b15105"
integrity sha512-z+PnLz1n6ECKhgoHZHkfc+dijXZEyZnNFSajbtE0NEbsJhmX8x9GlOeiMQIKX2E4DUqPSgfIh4FYBv1M49KgPQ==
dependencies:
idb "8.0.3"
"@react-native-community/checkbox@^0.5.20": "@react-native-community/checkbox@^0.5.20":
version "0.5.20" version "0.5.20"
resolved "https://registry.yarnpkg.com/@react-native-community/checkbox/-/checkbox-0.5.20.tgz#7b8004a80a90988d5dff208693524f069270b849" resolved "https://registry.yarnpkg.com/@react-native-community/checkbox/-/checkbox-0.5.20.tgz#7b8004a80a90988d5dff208693524f069270b849"
@ -1716,18 +1730,18 @@
nullthrows "^1.1.1" nullthrows "^1.1.1"
"@react-navigation/bottom-tabs@^7.18.3": "@react-navigation/bottom-tabs@^7.18.3":
version "7.18.3" version "7.18.4"
resolved "https://registry.yarnpkg.com/@react-navigation/bottom-tabs/-/bottom-tabs-7.18.3.tgz#371c39aee03aa90979d64e8d97da78559fbbcc92" resolved "https://registry.yarnpkg.com/@react-navigation/bottom-tabs/-/bottom-tabs-7.18.4.tgz#a0157c4d449bec8e5440967934c07ef27dca81fc"
integrity sha512-715guj97M1yklVkN9ikUl6KJL9wO6hNot7URyUm+A1r5ltEHVgahKTgR8w7e4dNwiMjUjAnOdSErR0LaX1h+kQ== integrity sha512-L7D1tlPnIsXlp45iZCpAIRRKlZSuqM9WyUNFbooRTlcaaLDSlOr0X6cKbGmJmCvSOoqT6bAsQjCKfb5WRqhtQA==
dependencies: dependencies:
"@react-navigation/elements" "^2.9.26" "@react-navigation/elements" "^2.9.27"
color "^4.2.3" color "^4.2.3"
sf-symbols-typescript "^2.1.0" sf-symbols-typescript "^2.1.0"
"@react-navigation/core@^7.21.2": "@react-navigation/core@^7.21.3":
version "7.21.2" version "7.21.3"
resolved "https://registry.yarnpkg.com/@react-navigation/core/-/core-7.21.2.tgz#448591d27d6c1472491a8ea9b901954ea727a310" resolved "https://registry.yarnpkg.com/@react-navigation/core/-/core-7.21.3.tgz#a4fc63da29f3dcbcbfe63da95b32c88d969c980c"
integrity sha512-EsJhQnsL5NuIhXsWF7URijS5hd2AaJUREdq1fOIowlyNNQBA3jCnkGU0DkW7+eI40cQ+GXH8DQw+ci7TefLIxQ== integrity sha512-HUoGmT9IdpKpbAl9AZJmXFbid+jQWQWjzww9vV9uBSd+8fa1hTwM3UBsZBHrpMru0s1ikwknoAIwV827fqGlhA==
dependencies: dependencies:
"@react-navigation/routers" "^7.6.0" "@react-navigation/routers" "^7.6.0"
escape-string-regexp "^4.0.0" escape-string-regexp "^4.0.0"
@ -1738,21 +1752,21 @@
use-latest-callback "^0.2.4" use-latest-callback "^0.2.4"
use-sync-external-store "^1.5.0" use-sync-external-store "^1.5.0"
"@react-navigation/elements@^2.9.26": "@react-navigation/elements@^2.9.27":
version "2.9.26" version "2.9.27"
resolved "https://registry.yarnpkg.com/@react-navigation/elements/-/elements-2.9.26.tgz#89cd09db890534c115031b1a6d99d916fdcda96c" resolved "https://registry.yarnpkg.com/@react-navigation/elements/-/elements-2.9.27.tgz#74247f508a7ad38fe03321cd6acfbfd3d1898716"
integrity sha512-EaHSJf42MjnH5VFL+KZfQIyqJvdQWK9Z27J7WUSuIVX5NXlR925sPmhWf540DX9gD1ny8BfUGPZAuw/PO4tK2w== integrity sha512-6bka/gWvP3+5Dw9Gf2ac83y/F0XEl2ktezc6Yp3gJBs22Rm9dluGKphe1B2Hj33XoCMRofxL8w3z3kmtlJiC0Q==
dependencies: dependencies:
color "^4.2.3" color "^4.2.3"
use-latest-callback "^0.2.4" use-latest-callback "^0.2.4"
use-sync-external-store "^1.5.0" use-sync-external-store "^1.5.0"
"@react-navigation/native@^7.3.4": "@react-navigation/native@^7.3.4":
version "7.3.4" version "7.3.5"
resolved "https://registry.yarnpkg.com/@react-navigation/native/-/native-7.3.4.tgz#a9ba7b0fde595b019a16a87ab609ad5b618bfbef" resolved "https://registry.yarnpkg.com/@react-navigation/native/-/native-7.3.5.tgz#d56ba57e130844f5eeac4c9d06e57386834c8330"
integrity sha512-zyAqFLeZuaakerMMnhYqBeGAzJ+WFQUTgezP3FxSlXNwFq5XxnjP28vi2XHGqm4zg4pGWls9Ay4JKshIHUOpwA== integrity sha512-lzcRpMoLCIypXXKm8KhpyZ81XpQBO/i8Oy1due4kUR97kG8kBlJxKUeP8yNnT5Fcl85rNdLXWbpwEr+9HPu4Ig==
dependencies: dependencies:
"@react-navigation/core" "^7.21.2" "@react-navigation/core" "^7.21.3"
escape-string-regexp "^4.0.0" escape-string-regexp "^4.0.0"
fast-deep-equal "^3.1.3" fast-deep-equal "^3.1.3"
nanoid "^3.3.11" nanoid "^3.3.11"
@ -1767,11 +1781,11 @@
nanoid "^3.3.11" nanoid "^3.3.11"
"@react-navigation/stack@^7.10.6": "@react-navigation/stack@^7.10.6":
version "7.10.6" version "7.10.7"
resolved "https://registry.yarnpkg.com/@react-navigation/stack/-/stack-7.10.6.tgz#5adf039d106a097bcb5514f07c1afc9e3ec75483" resolved "https://registry.yarnpkg.com/@react-navigation/stack/-/stack-7.10.7.tgz#76b91b693bbc00182090a2c71743cb8c5754409b"
integrity sha512-LWtBSCPNi3Htnb3051wan5hVTsoORxUocBeuAjZ3VS1u6wTtfObeqOOhanz/L+uiAzvTWcAHhevQhtlGeFn/mg== integrity sha512-rkJ9CEJnOHRSBVYNq7M8+0a34munKcTho/HSJiniRM3ZDjWR0aQKDXZcYpKi+IQ8jKv2XyYOvZLGHrBNJc5ipA==
dependencies: dependencies:
"@react-navigation/elements" "^2.9.26" "@react-navigation/elements" "^2.9.27"
color "^4.2.3" color "^4.2.3"
use-latest-callback "^0.2.4" use-latest-callback "^0.2.4"
@ -1878,6 +1892,11 @@
dependencies: dependencies:
"@types/node" "*" "@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": "@types/istanbul-lib-coverage@*", "@types/istanbul-lib-coverage@^2.0.0", "@types/istanbul-lib-coverage@^2.0.1":
version "2.0.6" version "2.0.6"
resolved "https://registry.yarnpkg.com/@types/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.6.tgz#7739c232a1fee9b4d3ce8985f314c0c6d33549d7" resolved "https://registry.yarnpkg.com/@types/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.6.tgz#7739c232a1fee9b4d3ce8985f314c0c6d33549d7"
@ -1906,9 +1925,9 @@
pretty-format "^29.0.0" pretty-format "^29.0.0"
"@types/node@*": "@types/node@*":
version "26.0.1" version "26.1.0"
resolved "https://registry.yarnpkg.com/@types/node/-/node-26.0.1.tgz#4a60e2c7a6d68bd261e265f8983bfe1601263ce3" resolved "https://registry.yarnpkg.com/@types/node/-/node-26.1.0.tgz#aa85f0727fc5611347091c478341c63650903439"
integrity sha512-fc3KiUoBt6kie0N9bIW3E47vZsuaMf0PM2AaUpLCLT0s/LvX1nxAim6Fc049cNxODPpGm6qRAuUOB86SkRuPQw== integrity sha512-O0A1G3xPGy4w7AgQdAQYUlQ+BKk2Oovw8eRpofyp5KdBZULnbe+WqaOVNrm705SHphCiG4XHsACrSmPu1f+Kgw==
dependencies: dependencies:
undici-types "~8.3.0" undici-types "~8.3.0"
@ -2087,6 +2106,13 @@ acorn@^8.15.0, acorn@^8.9.0:
resolved "https://registry.yarnpkg.com/acorn/-/acorn-8.17.0.tgz#1785adb84faf8d8add10369b93826fc2bd08f1fe" resolved "https://registry.yarnpkg.com/acorn/-/acorn-8.17.0.tgz#1785adb84faf8d8add10369b93826fc2bd08f1fe"
integrity sha512-xRQbDb9BnwDafYNn6Vwl839DYVjqXYb1XVGtWAZ1kcDc6iwAL4hg3B1dZlRiuENFeO2H53gFG3in621AdERVAg== 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: agent-base@^7.1.2:
version "7.1.4" version "7.1.4"
resolved "https://registry.yarnpkg.com/agent-base/-/agent-base-7.1.4.tgz#e3cd76d4c548ee895d3c3fd8dc1f6c5b9032e7a8" resolved "https://registry.yarnpkg.com/agent-base/-/agent-base-7.1.4.tgz#e3cd76d4c548ee895d3c3fd8dc1f6c5b9032e7a8"
@ -2275,6 +2301,11 @@ async-limiter@~1.0.0:
resolved "https://registry.yarnpkg.com/async-limiter/-/async-limiter-1.0.1.tgz#dd379e94f0db8310b08291f9d64c3209766617fd" resolved "https://registry.yarnpkg.com/async-limiter/-/async-limiter-1.0.1.tgz#dd379e94f0db8310b08291f9d64c3209766617fd"
integrity sha512-csOlWGAcRFJaI6m+F2WKdnMKr4HhdhFVBk0H/QbJFMCr+uO2kwohwXQPxw/9OCxp05r5ghVBFSyioixx3gfkNQ== 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: available-typed-arrays@^1.0.7:
version "1.0.7" version "1.0.7"
resolved "https://registry.yarnpkg.com/available-typed-arrays/-/available-typed-arrays-1.0.7.tgz#a5cc375d6a03c2efc87a553f3e0b1522def14846" resolved "https://registry.yarnpkg.com/available-typed-arrays/-/available-typed-arrays-1.0.7.tgz#a5cc375d6a03c2efc87a553f3e0b1522def14846"
@ -2282,6 +2313,16 @@ available-typed-arrays@^1.0.7:
dependencies: dependencies:
possible-typed-array-names "^1.0.0" 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: babel-jest@^29.7.0:
version "29.7.0" version "29.7.0"
resolved "https://registry.yarnpkg.com/babel-jest/-/babel-jest-29.7.0.tgz#f4369919225b684c56085998ac63dbd05be020d5" resolved "https://registry.yarnpkg.com/babel-jest/-/babel-jest-29.7.0.tgz#f4369919225b684c56085998ac63dbd05be020d5"
@ -2704,6 +2745,13 @@ colorette@^1.0.7:
resolved "https://registry.yarnpkg.com/colorette/-/colorette-1.4.0.tgz#5190fbb87276259a86ad700bff2c6d6faa3fca40" resolved "https://registry.yarnpkg.com/colorette/-/colorette-1.4.0.tgz#5190fbb87276259a86ad700bff2c6d6faa3fca40"
integrity sha512-Y2oEozpomLn7Q3HFP7dpww7AtMJplbM9lGZP6RDfHqmbeRjiwRg4n6VM6j4KLmRke85uWEI7JqF17f3pqdRA0g== 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: command-exists@^1.2.8:
version "1.2.9" version "1.2.9"
resolved "https://registry.yarnpkg.com/command-exists/-/command-exists-1.2.9.tgz#c50725af3808c8ab0260fd60b01fbfa25b954f69" resolved "https://registry.yarnpkg.com/command-exists/-/command-exists-1.2.9.tgz#c50725af3808c8ab0260fd60b01fbfa25b954f69"
@ -2933,6 +2981,11 @@ define-properties@^1.1.3, define-properties@^1.2.1:
has-property-descriptors "^1.0.0" has-property-descriptors "^1.0.0"
object-keys "^1.1.1" 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: depd@2.0.0, depd@~2.0.0:
version "2.0.0" version "2.0.0"
resolved "https://registry.yarnpkg.com/depd/-/depd-2.0.0.tgz#b696163cc757560d09cf22cc8fad1571b79e76df" resolved "https://registry.yarnpkg.com/depd/-/depd-2.0.0.tgz#b696163cc757560d09cf22cc8fad1571b79e76df"
@ -3012,9 +3065,9 @@ ee-first@1.1.1:
integrity sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow== integrity sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==
electron-to-chromium@^1.5.376: electron-to-chromium@^1.5.376:
version "1.5.382" version "1.5.383"
resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.5.382.tgz#e76f05d3ec337524b9c61761ebbc16fe91ecf5b4" resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.5.383.tgz#5bd22306497d454103b289b0fef97260c56d0855"
integrity sha512-8ETaWbV6SZOrno+G93Ffd9ENsMtetqdnqj4nlfxFW90Sm5GgnuV28Kf62hqQVD6VUgzm7qFQKsTsAPmeUiU3Ug== integrity sha512-I2484/KkAvl8lm9VyjH2JnbOIV0d/UCqT7gbzs6l+o6Vmn9wgB66uVcKX+Vk6HrXtY6fbWTOEXuv8waDTuFNCw==
emittery@^0.13.1: emittery@^0.13.1:
version "0.13.1" version "0.13.1"
@ -3613,6 +3666,11 @@ flow-enums-runtime@^0.0.6:
resolved "https://registry.yarnpkg.com/flow-enums-runtime/-/flow-enums-runtime-0.0.6.tgz#5bb0cd1b0a3e471330f4d109039b7eba5cb3e787" resolved "https://registry.yarnpkg.com/flow-enums-runtime/-/flow-enums-runtime-0.0.6.tgz#5bb0cd1b0a3e471330f4d109039b7eba5cb3e787"
integrity sha512-3PYnM29RFXwvAN6Pc/scUfkI7RwhQ/xqyLUyPNlXUp9S40zI8nup9tUSrTLSVnWGBN38FNiGWbwZOB6uR4OGdw== 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: for-each@^0.3.3, for-each@^0.3.5:
version "0.3.5" version "0.3.5"
resolved "https://registry.yarnpkg.com/for-each/-/for-each-0.3.5.tgz#d650688027826920feeb0af747ee7b9421a41d47" resolved "https://registry.yarnpkg.com/for-each/-/for-each-0.3.5.tgz#d650688027826920feeb0af747ee7b9421a41d47"
@ -3620,6 +3678,17 @@ for-each@^0.3.3, for-each@^0.3.5:
dependencies: dependencies:
is-callable "^1.2.7" 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: fresh@~0.5.2:
version "0.5.2" version "0.5.2"
resolved "https://registry.yarnpkg.com/fresh/-/fresh-0.5.2.tgz#3d8cadd90d976569fa835ab1f8e4b23a105605a7" resolved "https://registry.yarnpkg.com/fresh/-/fresh-0.5.2.tgz#3d8cadd90d976569fa835ab1f8e4b23a105605a7"
@ -3877,6 +3946,13 @@ hermes-parser@^0.25.1:
dependencies: dependencies:
hermes-estree "0.25.1" 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: html-escaper@^2.0.0:
version "2.0.2" version "2.0.2"
resolved "https://registry.yarnpkg.com/html-escaper/-/html-escaper-2.0.2.tgz#dfd60027da36a36dfcbe236262c00a5822681453" resolved "https://registry.yarnpkg.com/html-escaper/-/html-escaper-2.0.2.tgz#dfd60027da36a36dfcbe236262c00a5822681453"
@ -3893,6 +3969,14 @@ http-errors@~2.0.1:
statuses "~2.0.2" statuses "~2.0.2"
toidentifier "~1.0.1" 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: https-proxy-agent@^7.0.5:
version "7.0.6" version "7.0.6"
resolved "https://registry.yarnpkg.com/https-proxy-agent/-/https-proxy-agent-7.0.6.tgz#da8dfeac7da130b05c2ba4b59c9b6cd66611a6b9" resolved "https://registry.yarnpkg.com/https-proxy-agent/-/https-proxy-agent-7.0.6.tgz#da8dfeac7da130b05c2ba4b59c9b6cd66611a6b9"
@ -3913,6 +3997,11 @@ iconv-lite@~0.4.24:
dependencies: dependencies:
safer-buffer ">= 2.1.2 < 3" safer-buffer ">= 2.1.2 < 3"
idb@8.0.3:
version "8.0.3"
resolved "https://registry.yarnpkg.com/idb/-/idb-8.0.3.tgz#c91e558f15a8d53f1d7f53a094d226fc3ad71fd9"
integrity sha512-LtwtVyVYO5BqRvcsKuB2iUMnHwPVByPCXFXOpuU96IZPPoPN6xjOGxZQ74pgSVVLQWtUOYgyeL4GE98BY5D3wg==
ieee754@^1.1.13: ieee754@^1.1.13:
version "1.2.1" version "1.2.1"
resolved "https://registry.yarnpkg.com/ieee754/-/ieee754-1.2.1.tgz#8eb7a10a63fff25d15a57b001586d177d1b0d352" resolved "https://registry.yarnpkg.com/ieee754/-/ieee754-1.2.1.tgz#8eb7a10a63fff25d15a57b001586d177d1b0d352"
@ -3936,9 +4025,9 @@ image-size@^1.0.2:
queue "6.0.2" queue "6.0.2"
immer@^11.0.0: immer@^11.0.0:
version "11.1.8" version "11.1.9"
resolved "https://registry.yarnpkg.com/immer/-/immer-11.1.8.tgz#08a6426f7019dbce8d6dff8c4a43bb25c550a575" resolved "https://registry.yarnpkg.com/immer/-/immer-11.1.9.tgz#e0ce69359d29cafdb14fc5b6d2cd56f826097966"
integrity sha512-/tbkHMW7y10Lx6i1crLjD4/OhNkRG+Fo7byZHtah0547nIeXYcpIXaUh0IAQY6gO5459qpGGYapcEOHtFXkIuA== integrity sha512-sc/z0Cyti70bZa0ZU4sWfAElfovFb9Ni8tArJZLuklYWxegPiK3pDOql1Rq5H0FIRAW9LSQRG6OX4KqBldbhBA==
import-fresh@^3.2.1, import-fresh@^3.3.0: import-fresh@^3.2.1, import-fresh@^3.3.0:
version "3.3.1" version "3.3.1"
@ -5141,6 +5230,13 @@ mime-db@1.52.0:
resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.54.0.tgz#cddb3ee4f9c64530dff640236661d42cb6a314f5" resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.54.0.tgz#cddb3ee4f9c64530dff640236661d42cb6a314f5"
integrity sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ== 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: mime-types@^3.0.0, mime-types@^3.0.1:
version "3.0.2" version "3.0.2"
resolved "https://registry.yarnpkg.com/mime-types/-/mime-types-3.0.2.tgz#39002d4182575d5af036ffa118100f2524b2e2ab" resolved "https://registry.yarnpkg.com/mime-types/-/mime-types-3.0.2.tgz#39002d4182575d5af036ffa118100f2524b2e2ab"
@ -5148,13 +5244,6 @@ mime-types@^3.0.0, mime-types@^3.0.1:
dependencies: dependencies:
mime-db "^1.54.0" 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: mime@1.6.0:
version "1.6.0" version "1.6.0"
resolved "https://registry.yarnpkg.com/mime/-/mime-1.6.0.tgz#32cd9e5c64553bd58d19a568af452acff04981b1" resolved "https://registry.yarnpkg.com/mime/-/mime-1.6.0.tgz#32cd9e5c64553bd58d19a568af452acff04981b1"
@ -5620,6 +5709,11 @@ prop-types@^15.8.1:
object-assign "^4.1.1" object-assign "^4.1.1"
react-is "^16.13.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: punycode@^2.1.0:
version "2.3.1" version "2.3.1"
resolved "https://registry.yarnpkg.com/punycode/-/punycode-2.3.1.tgz#027422e2faec0b25e1549c3e1bd8309b9133b6e5" resolved "https://registry.yarnpkg.com/punycode/-/punycode-2.3.1.tgz#027422e2faec0b25e1549c3e1bd8309b9133b6e5"
@ -5688,7 +5782,7 @@ react-freeze@^1.0.0:
resolved "https://registry.yarnpkg.com/react-freeze/-/react-freeze-1.0.4.tgz#cbbea2762b0368b05cbe407ddc9d518c57c6f3ad" resolved "https://registry.yarnpkg.com/react-freeze/-/react-freeze-1.0.4.tgz#cbbea2762b0368b05cbe407ddc9d518c57c6f3ad"
integrity sha512-r4F0Sec0BLxWicc7HEyo2x3/2icUTrRmDjaaRyzzn+7aDyFZliszMDOgLVwSnQnYENOlL1o569Ze2HZefk8clA== integrity sha512-r4F0Sec0BLxWicc7HEyo2x3/2icUTrRmDjaaRyzzn+7aDyFZliszMDOgLVwSnQnYENOlL1o569Ze2HZefk8clA==
react-is@^16.13.1: react-is@^16.13.1, react-is@^16.7.0:
version "16.13.1" version "16.13.1"
resolved "https://registry.yarnpkg.com/react-is/-/react-is-16.13.1.tgz#789729a4dc36de2999dc156dd6c1d9c18cea56a4" resolved "https://registry.yarnpkg.com/react-is/-/react-is-16.13.1.tgz#789729a4dc36de2999dc156dd6c1d9c18cea56a4"
integrity sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ== integrity sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==
@ -5703,12 +5797,19 @@ react-is@^19.1.0, react-is@^19.2.3:
resolved "https://registry.yarnpkg.com/react-is/-/react-is-19.2.7.tgz#57668ee86a78574a542b0a539455212b2c086df2" resolved "https://registry.yarnpkg.com/react-is/-/react-is-19.2.7.tgz#57668ee86a78574a542b0a539455212b2c086df2"
integrity sha512-kZFnouyVv7eP/Phmrlo9FK+zcAdriZJvzxXHF1Sl1P377WSGe2G/JxVolhTrB/jeV47lKImhNUsijjHAAbcl/A== integrity sha512-kZFnouyVv7eP/Phmrlo9FK+zcAdriZJvzxXHF1Sl1P377WSGe2G/JxVolhTrB/jeV47lKImhNUsijjHAAbcl/A==
react-native-gesture-handler@^3.0.2: react-native-geolocation-service@^5.3.1:
version "3.0.2" version "5.3.1"
resolved "https://registry.yarnpkg.com/react-native-gesture-handler/-/react-native-gesture-handler-3.0.2.tgz#ea80fd2424d08f8b624977221f0544a5201f6d83" resolved "https://registry.yarnpkg.com/react-native-geolocation-service/-/react-native-geolocation-service-5.3.1.tgz#4ce1017789da6fdfcf7576eb6f59435622af4289"
integrity sha512-XBTgmvr2jh/xrMwlHTV2Z1x6IFUl3zfLxyaCAnvj3GSXK5YlY3ZmiIs57hniPmh3I7RtjPFxtGdRr0i+PzyBrg== 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: dependencies:
"@egjs/hammerjs" "^2.0.17"
"@types/react-test-renderer" "^19.1.0" "@types/react-test-renderer" "^19.1.0"
hoist-non-react-statics "^3.3.0"
invariant "^2.2.4" invariant "^2.2.4"
react-native-is-edge-to-edge@^1.3.1: react-native-is-edge-to-edge@^1.3.1:
@ -5753,9 +5854,9 @@ react-native-svg@^15.15.5:
css-tree "^1.1.3" css-tree "^1.1.3"
react-native-worklets@^0.10.0: react-native-worklets@^0.10.0:
version "0.10.0" version "0.10.1"
resolved "https://registry.yarnpkg.com/react-native-worklets/-/react-native-worklets-0.10.0.tgz#e2e8f8e1ba0eccc4e69f3a4e75894ecab4ba19b0" resolved "https://registry.yarnpkg.com/react-native-worklets/-/react-native-worklets-0.10.1.tgz#7a3038a3b5ec66cab030a00e1568b6599696f5f2"
integrity sha512-JhE6IxDf6iabC0qu3+TAKA4v9RlluXmoIngPQX7/QUByf75lfrsHZ6/dQhyjEWnp1EEQiwzz8Cpew140ZcewDw== integrity sha512-62mRM19bDpfpdI8HLkEErcdOsrAPDtE9lA/sw+5lLRpzBHNhxaoj9QyY2KjXqUmirelxkX4zuPGTC3VdA0feJA==
dependencies: dependencies:
"@babel/plugin-transform-arrow-functions" "^7.27.1" "@babel/plugin-transform-arrow-functions" "^7.27.1"
"@babel/plugin-transform-class-properties" "^7.28.6" "@babel/plugin-transform-class-properties" "^7.28.6"
@ -5843,6 +5944,11 @@ readable-stream@^3.4.0:
string_decoder "^1.1.1" string_decoder "^1.1.1"
util-deprecate "^1.0.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: redux-thunk@^3.1.0:
version "3.1.0" version "3.1.0"
resolved "https://registry.yarnpkg.com/redux-thunk/-/redux-thunk-3.1.0.tgz#94aa6e04977c30e14e892eae84978c1af6058ff3" resolved "https://registry.yarnpkg.com/redux-thunk/-/redux-thunk-3.1.0.tgz#94aa6e04977c30e14e892eae84978c1af6058ff3"