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

View File

@ -1,22 +1,29 @@
import React from 'react';
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 { Provider } from 'react-redux';
import { store } from './store';
import { persistor, store } from './store';
import { RootNavigator } from './navigation/rootNavigator';
import { PersistGate } from 'redux-persist/integration/react';
function App() {
const isDarkMode = useColorScheme() === 'dark';
return (
<Provider store={store}>
<PersistGate loading={null} persistor={persistor}>
<SafeAreaProvider>
<StatusBar barStyle={isDarkMode ? 'light-content' : 'dark-content'} />
<NavigationContainer>
<RootNavigator />
</NavigationContainer>
</SafeAreaProvider>
</PersistGate>
</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 {
label: string;
label?: string;
isPassword?: boolean;
rightActionText?: string;
onRightActionPress?: () => void;
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,7 +1,8 @@
import { StyleSheet } from 'react-native';
import { typography } from '@theme';
export const getStyles = (colors: any) => StyleSheet.create({
export const getStyles = (colors: any) =>
StyleSheet.create({
container: {
marginBottom: 20,
width: '100%',
@ -32,6 +33,12 @@ export const getStyles = (colors: any) => StyleSheet.create({
paddingHorizontal: 16,
height: 56,
},
leftElementWrap: {
marginRight: 10,
paddingRight: 10,
borderRightWidth: 1,
borderRightColor: colors.border,
},
input: {
flex: 1,
height: '100%',

View File

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

View File

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

View File

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

View File

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

View File

@ -1,7 +1,7 @@
import React from 'react';
import { TouchableOpacity, Text } from 'react-native';
import { SocialButtonProps } from './socialButton.props';
import { getStyles } from './socialButton.styles';
import { getStyles } from './SocialButton.styles';
import { useAppTheme } from '@theme';
import Svg, { Path } from 'react-native-svg';
@ -26,7 +26,7 @@ const GoogleIcon = ({ style }: { style: any }) => (
</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}>
<Path
fill={color}

View File

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

View File

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

View File

@ -1,9 +1,9 @@
export interface ProviderCardProps {
imageUrl: string;
imageUrl?: string;
name: string;
rating: number;
deliveryTime: string;
tag: 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';
export const getStyles = (colors: any) => StyleSheet.create({
export const getStyles = (colors: any) =>
StyleSheet.create({
container: {
flexDirection: 'row',
backgroundColor: colors.cardBg,
borderRadius: 12,
padding: 12,
borderRadius: 18,
marginHorizontal: 16,
marginVertical: 6,
borderWidth: 1,
borderColor: colors.border,
marginVertical: 8,
overflow: 'hidden',
...Platform.select({
ios: {
shadowColor: '#000',
shadowOffset: { width: 0, height: 3 },
shadowOpacity: 0.06,
shadowRadius: 10,
},
android: { elevation: 2 },
}),
},
// ---------- Image ----------
imageWrap: {
width: '100%',
height: 150,
position: 'relative',
backgroundColor: colors.border,
},
image: {
width: 80,
height: 80,
borderRadius: 8,
backgroundColor: colors.border,
justifyContent: 'center',
alignItems: 'center',
width: '100%',
height: '100%',
},
imagePlaceholder: {
fontSize: 32,
},
info: {
flex: 1,
marginLeft: 12,
width: '100%',
height: '100%',
alignItems: 'center',
justifyContent: 'center',
backgroundColor: colors.border,
},
imagePlaceholderEmoji: {
fontSize: 34,
},
discountRibbon: {
position: 'absolute',
top: 12,
left: 0,
backgroundColor: colors.primary,
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,
},
// ---------- Body ----------
body: {
padding: 14,
},
topRow: {
flexDirection: 'row',
justifyContent: 'space-between',
alignItems: 'flex-start',
},
name: {
fontSize: typography.fontSize.md,
fontWeight: typography.fontWeight.semibold,
fontWeight: typography.fontWeight.bold,
color: colors.text,
marginBottom: 4,
},
row: {
flexDirection: 'row',
alignItems: 'center',
marginBottom: 2,
},
rating: {
fontSize: typography.fontSize.sm,
color: colors.textSecondary,
flex: 1,
marginRight: 8,
},
deliveryTime: {
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,
},
tag: {
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.primary,
color: colors.textSecondary,
fontWeight: typography.fontWeight.medium,
},
discountBadge: {
alignSelf: 'flex-start',
backgroundColor: '#FFE7E7',
paddingHorizontal: 8,
paddingVertical: 2,
borderRadius: 4,
marginTop: 4,
},
discountText: {
fontSize: typography.fontSize.xs,
color: '#D32F2F',
fontWeight: typography.fontWeight.semibold,
},
});

View File

@ -1,5 +1,5 @@
import React from 'react';
import { View, Text, TouchableOpacity } from 'react-native';
import React, { useState } from 'react';
import { View, Text, Image, TouchableOpacity } from 'react-native';
import { ProviderCardProps } from './providerCard.props';
import { getStyles } from './providerCard.styles';
import { useAppTheme } from '@theme';
@ -15,28 +15,62 @@ export const ProviderCard: React.FC<ProviderCardProps> = ({
}) => {
const { colors } = useAppTheme();
const styles = getStyles(colors);
const [isFavorite, setIsFavorite] = useState(false);
return (
<TouchableOpacity
style={styles.container}
onPress={onPress}
activeOpacity={0.8}
activeOpacity={0.85}
>
<View style={styles.image}>
<Text style={styles.imagePlaceholder}>{imageUrl || '🍽️'}</Text>
</View>
<View style={styles.info}>
<Text style={styles.name} numberOfLines={1}>{name}</Text>
<View style={styles.row}>
<Text style={styles.rating}> {rating.toFixed(1)}</Text>
<Text style={styles.deliveryTime}>🕐 {deliveryTime}</Text>
</View>
<Text style={styles.tag}>{tag}</Text>
{discountText && (
<View style={styles.discountBadge}>
<Text style={styles.discountText}>{discountText}</Text>
<View style={styles.imageWrap}>
{imageUrl ? (
<Image
source={{ uri: imageUrl }}
style={styles.image}
resizeMode="cover"
/>
) : (
<View style={styles.imagePlaceholder}>
<Text style={styles.imagePlaceholderEmoji}>🍽</Text>
</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>
</TouchableOpacity>
);

View File

@ -1,20 +1,89 @@
import { StyleSheet } from 'react-native';
import { StyleSheet, Dimensions, Platform } from 'react-native';
import { typography } from '@theme';
export const getStyles = (colors: any) => StyleSheet.create({
const { width } = Dimensions.get('window');
export const getStyles = (colors: any) =>
StyleSheet.create({
container: {
flex: 1,
backgroundColor: colors.background,
paddingHorizontal: 24,
backgroundColor: colors.primary,
},
scrollContainer: {
flexGrow: 1,
},
// ---------- Hero ----------
hero: {
backgroundColor: colors.primary,
paddingTop: Platform.OS === 'ios' ? 70 : 48,
paddingBottom: 56,
alignItems: 'center',
overflow: 'hidden',
},
heroDecoCircleLg: {
position: 'absolute',
width: 220,
height: 220,
borderRadius: 110,
backgroundColor: 'rgba(255,255,255,0.06)',
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: {
marginTop: 40,
marginBottom: 32,
marginBottom: 28,
},
title: {
fontSize: typography.fontSize.xxl,
@ -29,4 +98,45 @@ export const getStyles = (colors: any) => StyleSheet.create({
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 {
View,
Text,
@ -6,39 +6,17 @@ import {
Platform,
ScrollView,
} from 'react-native';
import { useNavigation } from '@react-navigation/native';
import { StackNavigationProp } from '@react-navigation/stack';
import { getStyles } from './loginScreen.styles';
import { CustomInput, PrimaryButton } from '@components';
import { useAppTheme } from '@theme';
import { useAppDispatch } from '../../../hooks/useAppDispatch';
import { loginWithPhone } from '../../../store/authSlice';
import { AuthStackParamList } from '../../../navigation/authStack';
type LoginNavProp = StackNavigationProp<AuthStackParamList, 'LoginScreen'>;
import { useLoginScreen } from '../../../hooks';
export const LoginScreen: React.FC = () => {
const { colors } = useAppTheme();
const styles = getStyles(colors);
const navigation = useNavigation<LoginNavProp>();
const dispatch = useAppDispatch();
const [mobileNumber, setMobileNumber] = useState('');
const [error, setError] = useState<string | undefined>(undefined);
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 });
};
const { mobileNumber, error, onChangeMobileNumber, handleLogin } =
useLoginScreen();
return (
<KeyboardAvoidingView
@ -48,7 +26,21 @@ export const LoginScreen: React.FC = () => {
<ScrollView
contentContainerStyle={styles.scrollContainer}
showsVerticalScrollIndicator={false}
bounces={false}
>
{/* Hero / branding */}
<View style={styles.hero}>
<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>
{/* Form card */}
<View style={styles.formCard}>
<View style={styles.headerContainer}>
<Text style={styles.title}>Welcome back</Text>
<Text style={styles.subtitle}>Login to continue</Text>
@ -57,22 +49,40 @@ export const LoginScreen: React.FC = () => {
<View style={styles.formContainer}>
<CustomInput
label="Mobile number"
placeholder="+91 98765 43210"
placeholder="98765 43210"
keyboardType="phone-pad"
maxLength={10}
value={mobileNumber}
onChangeText={(text) => {
setMobileNumber(text);
if (error) setError(undefined);
}}
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="Login"
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>
</ScrollView>
</KeyboardAvoidingView>
);

View File

@ -1,7 +1,8 @@
import { StyleSheet } from 'react-native';
import { StyleSheet, Platform } from 'react-native';
import { typography } from '@theme';
export const getStyles = (colors: any) => StyleSheet.create({
export const getStyles = (colors: any) =>
StyleSheet.create({
container: {
flex: 1,
backgroundColor: colors.background,
@ -9,60 +10,215 @@ export const getStyles = (colors: any) => StyleSheet.create({
content: {
paddingBottom: 40,
},
// ---------- Profile card ----------
profileCard: {
alignItems: 'center',
paddingVertical: 32,
borderBottomWidth: 1,
borderBottomColor: colors.border,
backgroundColor: colors.cardBg,
marginHorizontal: 16,
marginTop: 16,
borderRadius: 20,
paddingVertical: 28,
paddingHorizontal: 20,
...Platform.select({
ios: {
shadowColor: '#000',
shadowOffset: { width: 0, height: 3 },
shadowOpacity: 0.06,
shadowRadius: 10,
},
android: { elevation: 2 },
}),
},
avatarWrap: {
position: 'relative',
marginBottom: 14,
},
avatar: {
width: 72,
height: 72,
borderRadius: 36,
width: 84,
height: 84,
borderRadius: 42,
backgroundColor: colors.primary,
justifyContent: 'center',
alignItems: 'center',
marginBottom: 12,
borderWidth: 3,
borderColor: colors.primaryMuted ?? '#E9F7EF',
},
avatarText: {
fontSize: 28,
fontSize: 32,
fontWeight: typography.fontWeight.bold,
color: '#FFFFFF',
},
editAvatarBadge: {
position: 'absolute',
bottom: -2,
right: -2,
width: 28,
height: 28,
borderRadius: 14,
backgroundColor: colors.background,
alignItems: 'center',
justifyContent: 'center',
borderWidth: 2,
borderColor: colors.cardBg,
},
editAvatarIcon: {
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: 16,
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: 16,
paddingVertical: 14,
paddingHorizontal: 14,
},
menuItemDivider: {
borderBottomWidth: 1,
borderBottomColor: colors.border,
},
menuIcon: {
fontSize: 20,
menuIconBadge: {
width: 38,
height: 38,
borderRadius: 12,
alignItems: 'center',
justifyContent: 'center',
marginRight: 14,
},
menuLabel: {
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: 18,
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 { 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 { Header, PrimaryButton } from '@components';
import { useAppTheme } from '@theme';
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 = [
{ icon: '📍', label: 'My Addresses' },
{ icon: '💳', label: 'Payment Methods' },
{ icon: '🔔', label: 'Notifications' },
{ icon: '❓', label: 'Help & Support' },
{ icon: '📋', label: 'Terms & Conditions' },
{ icon: '🔒', label: 'Privacy Policy' },
type AccountNavProp = CompositeNavigationProp<
BottomTabNavigationProp<MainTabParamList, 'AccountScreen'>,
StackNavigationProp<AppStackParamList>
>;
interface MenuItemData {
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 = () => {
const { colors } = useAppTheme();
const styles = getStyles(colors);
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 (
<View style={styles.container}>
<Header title="Account" />
<ScrollView contentContainerStyle={styles.content}>
<ScrollView
contentContainerStyle={styles.content}
showsVerticalScrollIndicator={false}
>
{/* Profile card */}
<View style={styles.profileCard}>
<View style={styles.avatarWrap}>
<View style={styles.avatar}>
<Text style={styles.avatarText}>
{user?.name?.charAt(0)?.toUpperCase() || 'U'}
</Text>
</View>
<Text style={styles.profileName}>{user?.name || 'User'}</Text>
<Text style={styles.profilePhone}>{user?.mobileNumber || '+91 98765 43210'}</Text>
</View>
<View style={styles.menuSection}>
{MENU_ITEMS.map((item, index) => (
<TouchableOpacity
key={index}
style={styles.menuItem}
style={styles.editAvatarBadge}
activeOpacity={0.7}
>
<Text style={styles.menuIcon}>{item.icon}</Text>
<Text style={styles.menuLabel}>{item.label}</Text>
<Text style={styles.menuArrow}>{'>'}</Text>
<Text style={styles.editAvatarIcon}></Text>
</TouchableOpacity>
))}
</View>
<PrimaryButton
title="Logout"
onPress={() => dispatch(logout())}
style={{ marginTop: 24 }}
/>
<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>
{/* Quick stats */}
{/* <View style={styles.statsRow}>
<View style={styles.statItem}>
<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>
</View>
);

View File

@ -1,72 +1,232 @@
import { StyleSheet } from 'react-native';
import { StyleSheet, Platform } from 'react-native';
import { typography } from '@theme';
export const getStyles = (colors: any) => StyleSheet.create({
export const getStyles = (colors: any) =>
StyleSheet.create({
container: {
flex: 1,
backgroundColor: colors.background,
},
list: {
paddingBottom: 100,
paddingBottom: 140,
},
// ---------- ETA banner ----------
etaBanner: {
flexDirection: 'row',
alignItems: 'center',
backgroundColor: colors.primaryMuted ?? '#E9F7EF',
marginHorizontal: 16,
marginTop: 12,
marginBottom: 4,
borderRadius: 12,
paddingVertical: 10,
paddingHorizontal: 14,
},
etaIcon: {
fontSize: 16,
marginRight: 8,
},
etaText: {
fontSize: typography.fontSize.sm,
fontWeight: typography.fontWeight.semibold,
color: colors.primary,
},
// ---------- Item cards ----------
itemsCard: {
backgroundColor: colors.cardBg,
marginHorizontal: 16,
marginTop: 14,
borderRadius: 16,
overflow: 'hidden',
...Platform.select({
ios: {
shadowColor: '#000',
shadowOffset: { width: 0, height: 2 },
shadowOpacity: 0.05,
shadowRadius: 8,
},
android: { elevation: 1 },
}),
},
cartItem: {
flexDirection: 'row',
alignItems: 'center',
justifyContent: 'space-between',
paddingVertical: 14,
paddingHorizontal: 16,
paddingHorizontal: 14,
},
cartItemDivider: {
borderBottomWidth: 1,
borderBottomColor: colors.border,
},
itemThumb: {
width: 52,
height: 52,
borderRadius: 10,
backgroundColor: colors.inputBg,
alignItems: 'center',
justifyContent: 'center',
marginRight: 12,
},
itemThumbEmoji: {
fontSize: 22,
},
itemInfo: {
flex: 1,
marginRight: 10,
},
itemTitle: {
fontSize: typography.fontSize.md,
fontWeight: typography.fontWeight.medium,
fontWeight: typography.fontWeight.semibold,
color: colors.text,
marginBottom: 4,
},
itemPrice: {
fontSize: typography.fontSize.md,
fontSize: typography.fontSize.sm,
fontWeight: typography.fontWeight.bold,
color: colors.textSecondary,
},
// ---------- Add more items ----------
addMoreRow: {
flexDirection: 'row',
alignItems: 'center',
justifyContent: 'center',
paddingVertical: 12,
marginHorizontal: 16,
marginTop: 4,
borderWidth: 1.5,
borderStyle: 'dashed',
borderColor: colors.border,
borderRadius: 12,
},
addMoreIcon: {
fontSize: 14,
marginRight: 6,
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,
},
footer: {
padding: 16,
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',
marginBottom: 20,
},
couponIcon: {
fontSize: 18,
marginLeft: 10,
marginRight: 4,
},
couponInput: {
flex: 1,
height: 48,
borderWidth: 1,
borderColor: colors.border,
borderRadius: 12,
paddingHorizontal: 16,
height: 44,
paddingHorizontal: 8,
fontSize: typography.fontSize.md,
color: colors.text,
backgroundColor: colors.inputBg,
marginRight: 8,
},
applyButton: {
paddingHorizontal: 20,
paddingVertical: 14,
borderRadius: 12,
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: 8,
marginBottom: 10,
},
feeLabel: {
fontSize: typography.fontSize.sm,
@ -82,6 +242,7 @@ export const getStyles = (colors: any) => StyleSheet.create({
borderTopColor: colors.border,
paddingTop: 12,
marginTop: 4,
marginBottom: 0,
},
totalLabel: {
fontSize: typography.fontSize.md,
@ -93,17 +254,97 @@ export const getStyles = (colors: any) => StyleSheet.create({
fontWeight: typography.fontWeight.bold,
color: colors.text,
},
bottomPanel: {
savingsBanner: {
flexDirection: 'row',
alignItems: 'center',
padding: 16,
borderTopWidth: 1,
borderTopColor: colors.border,
backgroundColor: colors.background,
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,34 +2,96 @@ import React, { useState } from 'react';
import {
View,
Text,
FlatList,
ScrollView,
TextInput,
TouchableOpacity,
} from 'react-native';
import { useNavigation } from '@react-navigation/native';
import { StackNavigationProp } from '@react-navigation/stack';
import { getStyles } from './cartScreen.styles';
import { Header, QuantitySelector, PrimaryButton } from '@components';
import { useAppTheme } from '@theme';
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 = () => {
const { colors } = useAppTheme();
const styles = getStyles(colors);
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 [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={() => {}} />
<FlatList
data={items}
keyExtractor={(item) => item.id}
renderItem={({ item }) => (
<View style={styles.cartItem}>
<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 (
<View style={styles.container}>
<Header title="Cart" onBack={() => navigation.goBack()} />
<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}>{item.item.title}</Text>
<Text style={styles.itemTitle} numberOfLines={2}>
{item.item.title}
</Text>
<Text style={styles.itemPrice}>{item.item.price}</Text>
</View>
<QuantitySelector
@ -38,35 +100,64 @@ export const CartScreen: React.FC = () => {
onDecrement={() => dispatch(removeItem(item.item.id))}
/>
</View>
)}
ListFooterComponent={
))}
</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={() => {
if (couponCode) {
dispatch(removeCoupon());
setCouponInput('');
} else if (couponInput) {
dispatch(applyCoupon(couponInput));
}
}}
onPress={handleCouponPress}
activeOpacity={0.7}
disabled={!couponInput}
>
<Text style={styles.applyButtonText}>
{couponCode ? 'Remove' : 'Apply'}
</Text>
<Text style={styles.applyButtonText}>Apply</Text>
</TouchableOpacity>
</View>
)}
</View>
<Text style={styles.sectionTitle}>Bill Details</Text>
<View style={styles.billCard}>
<View style={styles.feeRow}>
<Text style={styles.feeLabel}>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.totalValue}>{totals.total}</Text>
</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>
}
contentContainerStyle={styles.list}
/>
)}
</View>
</View>
</ScrollView>
<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
title="View Bill"
onPress={() => {}}
style={{ flex: 1, marginLeft: 12 }}
title="Proceed to Checkout"
onPress={() => navigation.navigate('CheckoutAddressScreen')}
style={styles.checkoutButton}
/>
</View>
</View>

View File

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

View File

@ -1,8 +1,13 @@
import React, { useState } from 'react';
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 { Header, StepProgress, PaymentOption, PrimaryButton } from '@components';
import { useAppTheme } from '@theme';
import { AppStackParamList } from '../../../navigation/appStack';
type CheckoutPaymentNavProp = StackNavigationProp<AppStackParamList, 'CheckoutPaymentScreen'>;
const CHECKOUT_STEPS = [
{ key: 'address', label: 'Address' },
@ -20,11 +25,12 @@ const PAYMENT_METHODS = [
export const CheckoutPaymentScreen: React.FC = () => {
const { colors } = useAppTheme();
const styles = getStyles(colors);
const navigation = useNavigation<CheckoutPaymentNavProp>();
const [selectedMethod, setSelectedMethod] = useState('upi');
return (
<View style={styles.container}>
<Header title="Checkout" onBack={() => {}} />
<Header title="Checkout" onBack={() => navigation.goBack()} />
<StepProgress steps={CHECKOUT_STEPS} activeStep={1} />
<ScrollView contentContainerStyle={styles.content}>
<Text style={styles.sectionTitle}>Select Payment Method</Text>
@ -39,7 +45,7 @@ export const CheckoutPaymentScreen: React.FC = () => {
))}
</ScrollView>
<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 File

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

View File

@ -1,8 +1,13 @@
import React from 'react';
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 { Header } from '@components';
import { useAppTheme } from '@theme';
import { AppStackParamList } from '../../../navigation/appStack';
type HelpSupportNavProp = StackNavigationProp<AppStackParamList, 'HelpSupportScreen'>;
const FAQS = [
{ 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 = () => {
const { colors } = useAppTheme();
const styles = getStyles(colors);
const navigation = useNavigation<HelpSupportNavProp>();
return (
<View style={styles.container}>
<Header title="Help & Support" />
<Header title="Help & Support" onBack={() => navigation.goBack()} />
<ScrollView contentContainerStyle={styles.content}>
<Text style={styles.sectionTitle}>Frequently Asked Questions</Text>
{FAQS.map((faq, index) => (

View File

@ -1,20 +1,50 @@
import { StyleSheet } from 'react-native';
import { StyleSheet, Dimensions, Platform } from 'react-native';
import { typography } from '@theme';
export const getStyles = (colors: any) => StyleSheet.create({
const { width } = Dimensions.get('window');
const BANNER_WIDTH = width - 32;
export const getStyles = (colors: any) =>
StyleSheet.create({
container: {
flex: 1,
backgroundColor: colors.background,
},
addressBar: {
// ---------- Top bar ----------
topBar: {
flexDirection: 'row',
alignItems: 'center',
justifyContent: 'space-between',
paddingHorizontal: 16,
paddingTop: 12,
paddingBottom: 4,
paddingTop: 14,
paddingBottom: 10,
},
addressBar: {
flexDirection: 'row',
alignItems: 'center',
flex: 1,
},
pinBadge: {
width: 34,
height: 34,
borderRadius: 17,
backgroundColor: colors.primaryMuted ?? '#E9F7EF',
alignItems: 'center',
justifyContent: 'center',
marginRight: 10,
},
pinEmoji: {
fontSize: 16,
},
addressTextWrap: {
flex: 1,
},
addressLabel: {
fontSize: typography.fontSize.xs,
color: colors.textSecondary,
marginBottom: 2,
marginBottom: 1,
letterSpacing: 0.2,
},
addressRow: {
flexDirection: 'row',
@ -22,43 +52,337 @@ export const getStyles = (colors: any) => StyleSheet.create({
},
addressText: {
fontSize: typography.fontSize.md,
fontWeight: typography.fontWeight.semibold,
fontWeight: typography.fontWeight.bold,
color: colors.text,
flex: 1,
maxWidth: width * 0.55,
},
dropdownArrow: {
fontSize: 12,
fontSize: 11,
color: colors.textSecondary,
marginLeft: 4,
marginLeft: 6,
},
banner: {
backgroundColor: '#05824C',
marginHorizontal: 16,
borderRadius: 12,
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,
marginVertical: 8,
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: typography.fontSize.xxl,
fontSize: 26,
fontWeight: typography.fontWeight.bold,
color: '#FFFFFF',
marginTop: 2,
},
bannerSubtext: {
fontSize: typography.fontSize.sm,
color: '#FFFFFF',
opacity: 0.9,
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,
paddingVertical: 12,
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,
marginTop: 4,
},
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 {
View,
Text,
ScrollView,
TouchableOpacity,
FlatList,
Image,
NativeSyntheticEvent,
NativeScrollEvent,
Dimensions,
} from 'react-native';
import { useNavigation } from '@react-navigation/native';
import {
useNavigation,
CompositeNavigationProp,
} from '@react-navigation/native';
import { BottomTabNavigationProp } from '@react-navigation/bottom-tabs';
import { StackNavigationProp } from '@react-navigation/stack';
import { getStyles } from './homeScreen.styles';
import { SearchBar, ProviderCard, CategoryChip } from '@components';
import { ProviderCard, SearchBar } from '@components';
import { useAppTheme } from '@theme';
import { deliveryService } from '../../../services/deliveryService';
import { getProvidersApi } from '../../../api/deliveryApi';
import { Provider } from '../../../interfaces';
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 = [
{ key: 'all', icon: '🏠', label: 'All' },
{ key: 'Food', icon: '🍔', label: 'Food' },
{ key: 'Groceries', icon: '🛒', label: 'Groceries' },
{ key: 'Groceries', icon: '🛒', label: 'Grocery' },
{ key: 'Pharmacy', icon: '💊', label: 'Pharmacy' },
{ key: 'Meat', icon: '🥩', label: 'Meat' },
{ key: 'Flowers', icon: '💐', label: 'Flowers' },
{ 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 = () => {
const { colors } = useAppTheme();
const styles = getStyles(colors);
@ -32,62 +76,175 @@ export const HomeScreen: React.FC = () => {
const [providers, setProviders] = useState<Provider[]>([]);
const [selectedCategory, setSelectedCategory] = useState('all');
const [activeBanner, setActiveBanner] = useState(0);
useEffect(() => {
deliveryService.getProviders().then(setProviders);
getProvidersApi().then(setProviders);
}, []);
const filteredProviders = selectedCategory === 'all'
const filteredProviders =
selectedCategory === 'all'
? providers
: providers.filter((p) => p.tag === selectedCategory);
: providers.filter(p => p.tag === selectedCategory);
const handleSearchFocus = useCallback(() => {
navigation.navigate('SearchScreen');
}, [navigation]);
const handleBannerScroll = useCallback(
(e: NativeSyntheticEvent<NativeScrollEvent>) => {
const index = Math.round(e.nativeEvent.contentOffset.x / BANNER_STEP);
setActiveBanner(index);
},
[],
);
return (
<ScrollView style={styles.container} showsVerticalScrollIndicator={false}>
<View style={styles.addressBar}>
{/* Top bar */}
<View style={styles.topBar}>
<TouchableOpacity style={styles.addressBar} activeOpacity={0.7}>
<View style={styles.pinBadge}>
<Text style={styles.pinEmoji}>📍</Text>
</View>
<View style={styles.addressTextWrap}>
<Text style={styles.addressLabel}>Deliver to</Text>
<TouchableOpacity style={styles.addressRow} activeOpacity={0.7}>
<Text style={styles.addressText}>Koramangala 4th Block, B... </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>
</View>
{/* Search */}
<View style={styles.searchWrap}>
<SearchBar
value=""
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>
{/* Promo carousel */}
<FlatList
data={PROMOS}
horizontal
keyExtractor={item => item.key}
showsHorizontalScrollIndicator={false}
contentContainerStyle={styles.bannerListContent}
snapToInterval={BANNER_STEP}
decelerationRate="fast"
onScroll={handleBannerScroll}
scrollEventThrottle={16}
renderItem={({ item }) => (
<View
style={[styles.bannerCard, { backgroundColor: item.colors[0] }]}
>
<View style={styles.bannerDecoCircleLg} />
<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>
{/* Categories */}
<View style={styles.categorySection}>
<FlatList
horizontal
data={CATEGORIES}
keyExtractor={(item) => item.key}
keyExtractor={item => item.key}
showsHorizontalScrollIndicator={false}
contentContainerStyle={styles.chipRow}
renderItem={({ item }) => (
<CategoryChip
icon={item.icon}
label={item.label}
isSelected={selectedCategory === item.key}
onPress={() => setSelectedCategory(item.key)}
/>
)}
/>
<Text style={styles.sectionTitle}>
{selectedCategory === 'all' ? 'Popular Providers' : selectedCategory}
renderItem={({ item }) => {
const isSelected = selectedCategory === item.key;
return (
<TouchableOpacity
style={styles.categoryItem}
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>
{filteredProviders.map((provider) => (
{/* 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}
@ -96,9 +253,14 @@ export const HomeScreen: React.FC = () => {
deliveryTime={provider.deliveryTime}
tag={provider.tag}
discountText={provider.discountText}
onPress={() => navigation.navigate('SearchScreen')}
onPress={() =>
navigation.navigate('ProviderDetailsScreen', {
providerId: provider.id,
})
}
/>
))}
</View>
<View style={{ height: 24 }} />
</ScrollView>

View File

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

View File

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

View File

@ -5,20 +5,31 @@ import {
FlatList,
TouchableOpacity,
} 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 { Header, OrderHistoryCard } from '@components';
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 mockOrders = [
{ id: '1', 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: '3', providerName: 'Burger Barn', providerImage: '', orderDate: '27 Jun 2026', status: 'Delivered', total: 449 },
{ id: 'ORD-591283', providerName: 'Pizza Planet', providerImage: '', orderDate: '29 Jun 2026', status: 'Delivered', total: 599 },
{ id: 'ORD-771239', providerName: 'Fresh Mart', providerImage: '', orderDate: '28 Jun 2026', status: 'Ongoing', total: 349 },
{ id: 'ORD-108239', providerName: 'Burger Barn', providerImage: '', orderDate: '27 Jun 2026', status: 'Delivered', total: 449 },
];
export const MyOrdersScreen: React.FC = () => {
const { colors } = useAppTheme();
const styles = getStyles(colors);
const navigation = useNavigation<MyOrdersNavProp>();
const [activeTab, setActiveTab] = useState('All');
const filteredOrders = activeTab === 'All'
@ -62,8 +73,19 @@ export const MyOrdersScreen: React.FC = () => {
orderDate={item.orderDate}
status={item.status}
total={item.total}
onReorder={() => {}}
onDetails={() => {}}
onReorder={() => {
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}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,7 +1,11 @@
import { StyleSheet } from 'react-native';
import { StyleSheet, Dimensions, Platform } from 'react-native';
import { typography } from '@theme';
export const getStyles = (colors: any) => StyleSheet.create({
const { width } = Dimensions.get('window');
const HERO_HEIGHT = 220;
export const getStyles = (colors: any) =>
StyleSheet.create({
container: {
flex: 1,
backgroundColor: colors.background,
@ -9,87 +13,333 @@ export const getStyles = (colors: any) => StyleSheet.create({
content: {
flex: 1,
},
hero: {
alignItems: 'center',
paddingVertical: 24,
borderBottomWidth: 1,
borderBottomColor: colors.border,
contentBody: {
paddingBottom: 32,
},
// ---------- Hero ----------
heroWrap: {
width,
height: HERO_HEIGHT,
backgroundColor: colors.inputBg,
},
heroImage: {
width: 100,
height: 100,
borderRadius: 50,
backgroundColor: colors.inputBg,
justifyContent: 'center',
alignItems: 'center',
marginBottom: 12,
width: '100%',
height: '100%',
},
heroEmoji: {
fontSize: 44,
heroFallback: {
width: '100%',
height: '100%',
alignItems: 'center',
justifyContent: 'center',
backgroundColor: colors.inputBg,
},
heroFallbackEmoji: {
fontSize: 56,
},
heroOverlay: {
position: 'absolute',
left: 0,
right: 0,
bottom: 0,
height: 90,
backgroundColor: 'rgba(0,0,0,0.28)',
},
// Floating top icon row (back / share / favorite)
topIconRow: {
position: 'absolute',
top: Platform.OS === 'ios' ? 54 : 16,
left: 16,
right: 16,
flexDirection: 'row',
justifyContent: 'space-between',
alignItems: 'center',
},
iconButton: {
width: 38,
height: 38,
borderRadius: 19,
backgroundColor: 'rgba(255,255,255,0.92)',
alignItems: 'center',
justifyContent: 'center',
...Platform.select({
ios: {
shadowColor: '#000',
shadowOffset: { width: 0, height: 2 },
shadowOpacity: 0.15,
shadowRadius: 4,
},
android: { elevation: 3 },
}),
},
iconButtonText: {
fontSize: 16,
},
iconButtonGroup: {
flexDirection: 'row',
},
// ---------- Info card ----------
infoCard: {
backgroundColor: colors.background,
marginTop: -20,
borderTopLeftRadius: 24,
borderTopRightRadius: 24,
paddingTop: 20,
paddingHorizontal: 20,
paddingBottom: 16,
...Platform.select({
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,
marginBottom: 8,
flex: 1,
marginRight: 12,
},
heroStats: {
ratingBadge: {
flexDirection: 'row',
gap: 16,
alignItems: 'center',
backgroundColor: colors.primary,
borderRadius: 8,
paddingHorizontal: 8,
paddingVertical: 5,
},
heroStat: {
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: 12,
paddingHorizontal: 16,
paddingVertical: 14,
paddingHorizontal: 20,
flexGrow: 0,
},
categoryTab: {
paddingHorizontal: 20,
paddingVertical: 8,
borderRadius: 20,
borderWidth: 1,
paddingHorizontal: 18,
paddingVertical: 9,
borderRadius: 22,
borderWidth: 1.5,
borderColor: colors.border,
marginRight: 8,
marginRight: 10,
backgroundColor: colors.background,
},
categoryTabActive: {
borderColor: colors.primary,
backgroundColor: '#E8F5E9',
backgroundColor: colors.primary,
},
categoryTabText: {
fontSize: typography.fontSize.sm,
color: colors.textSecondary,
fontWeight: typography.fontWeight.medium,
},
categoryTabTextActive: {
color: colors.primary,
fontWeight: typography.fontWeight.semibold,
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: 20,
paddingHorizontal: 18,
paddingVertical: 14,
borderTopLeftRadius: 16,
borderTopRightRadius: 16,
borderRadius: 16,
...Platform.select({
ios: {
shadowColor: '#000',
shadowOffset: { width: 0, height: 6 },
shadowOpacity: 0.25,
shadowRadius: 12,
},
cartStripText: {
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.semibold,
fontWeight: typography.fontWeight.bold,
color: '#FFFFFF',
},
viewCartButton: {
flexDirection: 'row',
alignItems: 'center',
backgroundColor: '#FFFFFF',
paddingHorizontal: 16,
paddingVertical: 8,
borderRadius: 8,
paddingVertical: 9,
borderRadius: 10,
},
viewCartText: {
fontSize: typography.fontSize.sm,
fontWeight: typography.fontWeight.semibold,
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 {
View,
Text,
TouchableOpacity,
ScrollView,
Image,
ImageBackground,
} from 'react-native';
import { useNavigation, useRoute, RouteProp } from '@react-navigation/native';
import { StackNavigationProp } from '@react-navigation/stack';
import { getStyles } from './providerDetailsScreen.styles';
import { Header, CatalogItemRow } from '@components';
import { CatalogItemRow } from '@components';
import { useAppTheme } from '@theme';
import { useAppDispatch, useAppSelector } from '../../../hooks/useAppDispatch';
import { addItem } from '../../../store/cartSlice';
import { deliveryService } from '../../../services/deliveryService';
import { CatalogItem } from '../../../interfaces';
import { addItem } from '../../../store/commonreducers/cart';
import { getProviderCatalogApi, getProvidersApi } from '../../../api/deliveryApi';
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 = () => {
const { colors } = useAppTheme();
const styles = getStyles(colors);
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 [activeCategory, setActiveCategory] = useState(CATEGORIES[0]);
const [activeCategory, setActiveCategory] = useState(FALLBACK_CATEGORIES[0]);
const resolvedProviderId = providerId || 'p1';
const resolvedProviderName = providerName || 'Pizza Planet';
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 item = cartItems.find((i) => i.item.id === itemId);
return item ? item.quantity : 0;
const match = cartItems.find(i => i.item.id === itemId);
return match ? match.quantity : 0;
};
const totalItems = cartItems.reduce((sum, i) => sum + i.quantity, 0);
@ -41,50 +292,64 @@ export const ProviderDetailsScreen: React.FC = () => {
0,
);
const handleAddItem = (item: CatalogItem) => {
dispatch(
addItem({
providerId: resolvedProviderId,
providerName: resolvedProviderName,
item,
}),
);
};
return (
<View style={styles.container}>
<Header title="Provider Details" onBack={() => {}} />
<ScrollView style={styles.content}>
<View style={styles.hero}>
<View style={styles.heroImage}>
<Text style={styles.heroEmoji}>🍕</Text>
</View>
<Text style={styles.heroName}>Pizza Planet</Text>
<View style={styles.heroStats}>
<Text style={styles.heroStat}> 4.5</Text>
<Text style={styles.heroStat}>🕐 25 min</Text>
<Text style={styles.heroStat}>🍕 Italian</Text>
</View>
</View>
<ScrollView
horizontal
showsHorizontalScrollIndicator={false}
style={styles.categoryRow}
style={styles.content}
contentContainerStyle={styles.contentBody}
showsVerticalScrollIndicator={false}
>
{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>
<HeroSection
styles={styles}
imageUrl={provider?.imageUrl}
onBack={() => navigation.goBack()}
/>
{filteredItems.map((item) => (
<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>
<CategoryTabs
styles={styles}
categories={categories}
activeCategory={activeCategory}
onSelect={setActiveCategory}
/>
<View style={styles.menuList}>
{filteredItems.length === 0 && (
<View style={styles.emptyMenuWrap}>
<Text style={styles.emptyMenuEmoji}>🍽</Text>
<Text style={styles.emptyMenuText}>
No items in this category yet
</Text>
</View>
)}
{filteredItems.map(item => (
<CatalogItemRow
key={item.id}
imageUrl={item.imageUrl}
@ -92,29 +357,20 @@ export const ProviderDetailsScreen: React.FC = () => {
description={item.description}
price={item.price}
quantity={getItemQuantity(item.id)}
onAdd={() =>
dispatch(
addItem({
providerId: 'p1',
providerName: 'Pizza Planet',
item,
}),
)
}
onAdd={() => handleAddItem(item)}
onRemove={() => {}}
/>
))}
</View>
</ScrollView>
{totalItems > 0 && (
<View style={styles.cartStrip}>
<Text style={styles.cartStripText}>
{totalItems} Items | {totalPrice}
</Text>
<TouchableOpacity style={styles.viewCartButton} activeOpacity={0.7}>
<Text style={styles.viewCartText}>View Cart</Text>
</TouchableOpacity>
</View>
<CartStrip
styles={styles}
totalItems={totalItems}
totalPrice={totalPrice}
onPress={() => navigation.navigate('CartScreen')}
/>
)}
</View>
);

View File

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

View File

@ -4,22 +4,33 @@ import {
Text,
FlatList,
} 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 { SearchBar, ProviderCard } from '@components';
import { useAppTheme } from '@theme';
import { deliveryService } from '../../../services/deliveryService';
import { searchProvidersApi } from '../../../api/deliveryApi';
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 = () => {
const { colors } = useAppTheme();
const styles = getStyles(colors);
const navigation = useNavigation<NavProp>();
const [query, setQuery] = useState('');
const [results, setResults] = useState<Provider[]>([]);
useEffect(() => {
if (query.trim().length > 0) {
deliveryService.searchProviders(query).then(setResults);
searchProvidersApi(query).then(setResults);
} else {
setResults([]);
}
@ -43,6 +54,12 @@ export const SearchScreen: React.FC = () => {
deliveryTime={item.deliveryTime}
tag={item.tag}
discountText={item.discountText}
onPress={() =>
navigation.navigate('ProviderDetailsScreen', {
providerId: item.id,
providerName: item.name,
})
}
/>
)}
ListEmptyComponent={

View File

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

View File

@ -1,54 +1,139 @@
import React from 'react';
import React, { useState, useEffect, useRef, useCallback } from 'react';
import {
View,
Text,
TouchableOpacity,
KeyboardAvoidingView,
Platform,
ScrollView,
ActivityIndicator,
} from 'react-native';
import MapView, { Marker, Region } from 'react-native-maps';
import { useNavigation } from '@react-navigation/native';
import { StackNavigationProp } from '@react-navigation/stack';
import { getStyles } from './setLocationScreen.styles';
import { PrimaryButton } from '@components';
import { useAppTheme } from '@theme';
import { AuthStackParamList } from '../../../navigation/authStack';
import {
DEFAULT_LOCATION,
getCurrentLocationWithAddress,
reverseGeocode,
LatLng,
} from '../../../services/locationServices';
type NavProp = StackNavigationProp<AuthStackParamList, 'SetLocationScreen'>;
const DELTA = { latitudeDelta: 0.006, longitudeDelta: 0.006 };
export const SetLocationScreen: React.FC = () => {
const { colors } = useAppTheme();
const styles = getStyles(colors);
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 (
<KeyboardAvoidingView
style={styles.container}
behavior={Platform.OS === 'ios' ? 'padding' : 'height'}
<View style={styles.container}>
{/* ---------- Map ---------- */}
<MapView
ref={mapRef}
style={styles.map}
initialRegion={{ ...DEFAULT_LOCATION, ...DELTA }}
showsUserLocation
showsMyLocationButton={true}
onRegionChangeComplete={handleRegionChangeComplete}
>
<ScrollView contentContainerStyle={styles.scrollContainer}>
<View style={styles.mapPlaceholder}>
<Text style={styles.mapIcon}>🗺</Text>
<Text style={styles.mapText}>Map View</Text>
<Text style={styles.mapSubtext}>
(react-native-maps integration)
</Text>
</View>
<Marker
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}>
<Text style={styles.cardTitle}>Delivery Location</Text>
<Text style={styles.cardAddress}>
Koramangala 4th Block, Bengaluru, 560034
<View style={styles.cardHeader}>
<Text style={styles.cardIcon}>📍</Text>
<Text style={styles.cardTitle}>My Location</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}>
<Text style={styles.changeText}>Change</Text>
</TouchableOpacity>
</View>
<PrimaryButton
title="Use this location"
title="Confirm Location"
onPress={() => navigation.navigate('CompleteProfileScreen')}
style={{ marginHorizontal: 24 }}
style={styles.confirmButton}
/>
<TouchableOpacity
@ -58,7 +143,7 @@ export const SetLocationScreen: React.FC = () => {
>
<Text style={styles.manualButtonText}>Enter address manually</Text>
</TouchableOpacity>
</ScrollView>
</KeyboardAvoidingView>
</View>
</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 { createBottomTabNavigator } from '@react-navigation/bottom-tabs';
import { Text } from 'react-native';
import { createStackNavigator } from '@react-navigation/stack';
import { NavigatorScreenParams } from '@react-navigation/native';
import { MainTabNavigator, MainTabParamList } from './mainTabNavigator';
import {
HomeScreen,
SearchScreen,
MyOrdersScreen,
OffersScreen,
AccountScreen,
ProviderListScreen,
ProviderDetailsScreen,
CartScreen,
CheckoutAddressScreen,
CheckoutPaymentScreen,
OrderConfirmedScreen,
OrderTrackingScreen,
LiveTrackingScreen,
OrderDeliveredScreen,
HelpSupportScreen,
} from '@features/screens';
export type AppStackParamList = {
HomeScreen: undefined;
SearchScreen: undefined;
MyOrdersScreen: undefined;
OffersScreen: undefined;
AccountScreen: undefined;
MainTabs: NavigatorScreenParams<MainTabParamList> | undefined;
ProviderListScreen: { category?: string } | undefined;
ProviderDetailsScreen: { providerId: string; providerName?: string };
CartScreen: 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 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>
);
const Stack = createStackNavigator<AppStackParamList>();
export const AppStack: 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>
<Stack.Navigator screenOptions={{ headerShown: false }}>
<Stack.Screen name="MainTabs" component={MainTabNavigator} />
<Stack.Screen name="ProviderListScreen" component={ProviderListScreen} />
<Stack.Screen name="ProviderDetailsScreen" component={ProviderDetailsScreen} />
<Stack.Screen name="CartScreen" component={CartScreen} />
<Stack.Screen name="CheckoutAddressScreen" component={CheckoutAddressScreen} />
<Stack.Screen name="CheckoutPaymentScreen" component={CheckoutPaymentScreen} />
<Stack.Screen name="OrderConfirmedScreen" component={OrderConfirmedScreen} />
<Stack.Screen name="OrderTrackingScreen" component={OrderTrackingScreen} />
<Stack.Screen name="LiveTrackingScreen" component={LiveTrackingScreen} />
<Stack.Screen name="OrderDeliveredScreen" component={OrderDeliveredScreen} />
<Stack.Screen name="HelpSupportScreen" component={HelpSupportScreen} />
</Stack.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 = {
async get<T>(url: string, mockData: T): Promise<T> {
await delay(800);
return mockData;
// ─── Storage Keys ────────────────────────────────────────────────────────────
const STORAGE_KEYS = {
ACCESS_TOKEN: '@auth_access_token',
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> {
await delay(1000);
return mockResponse;
async getRefreshToken(): Promise<string | null> {
return AsyncStorage.getItem(STORAGE_KEYS.REFRESH_TOKEN);
},
async put<T>(url: string, body: unknown, mockResponse: T): Promise<T> {
await delay(800);
return mockResponse;
async setTokens(accessToken: string, refreshToken: string): Promise<void> {
await AsyncStorage.setItem(STORAGE_KEYS.ACCESS_TOKEN, accessToken);
await AsyncStorage.setItem(STORAGE_KEYS.REFRESH_TOKEN, refreshToken);
},
async clearTokens(): Promise<void> {
await AsyncStorage.removeItem(STORAGE_KEYS.ACCESS_TOKEN);
await AsyncStorage.removeItem(STORAGE_KEYS.REFRESH_TOKEN);
},
};
// ─── Axios Instance ──────────────────────────────────────────────────────────
const axiosInstance: AxiosInstance = axios.create({
baseURL: BASE_URL,
timeout: 15000,
headers: {
'Content-Type': 'application/json',
Accept: 'application/json',
},
});
// ─── Request Interceptor — Attach Access Token ──────────────────────────────
axiosInstance.interceptors.request.use(
async (config: InternalAxiosRequestConfig) => {
// Skip attaching token for auth endpoints (login, register, refresh)
const publicPaths = [
'/auth/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';
import authReducer from './authSlice';
import cartReducer from './cartSlice';
import orderReducer from './orderSlice';
export const store = configureStore({
reducer: {
auth: authReducer,
cart: cartReducer,
order: orderReducer,
},
});
export type RootState = ReturnType<typeof store.getState>;
export type AppDispatch = typeof store.dispatch;
export * from './commonreducers';
export * from './store';
export * from './rootReducer';
export * from './migration';

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"
},
"dependencies": {
"@react-native-async-storage/async-storage": "^3.1.1",
"@react-native-community/checkbox": "^0.5.20",
"@react-native/new-app-screen": "0.86.0",
"@react-navigation/bottom-tabs": "^7.18.3",
"@react-navigation/native": "^7.3.4",
"@react-navigation/stack": "^7.10.6",
"@reduxjs/toolkit": "^2.12.0",
"axios": "^1.18.1",
"react": "19.2.3",
"react-native": "0.86.0",
"react-native-gesture-handler": "^3.0.2",
"react-native-geolocation-service": "^5.3.1",
"react-native-gesture-handler": "^2.32.0",
"react-native-maps": "^1.29.0",
"react-native-reanimated": "^4.5.0",
"react-native-safe-area-context": "^5.8.0",
"react-native-screens": "^4.25.2",
"react-native-svg": "^15.15.5",
"react-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": {
"@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"
integrity sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw==
"@egjs/hammerjs@^2.0.17":
version "2.0.17"
resolved "https://registry.yarnpkg.com/@egjs/hammerjs/-/hammerjs-2.0.17.tgz#5dc02af75a6a06e4c2db0202cae38c9263895124"
integrity sha512-XQsZgjm2EcVUiZQf11UBJQfmZeEmOW8DpI1gsFeln6w0ae0ii4dMQEQ0kjl6DspdWX1aGY1/loyXnP0JS06e/A==
dependencies:
"@types/hammerjs" "^2.0.36"
"@eslint-community/eslint-utils@^4.2.0", "@eslint-community/eslint-utils@^4.9.1":
version "4.9.1"
resolved "https://registry.yarnpkg.com/@eslint-community/eslint-utils/-/eslint-utils-4.9.1.tgz#4e90af67bc51ddee6cdef5284edf572ec376b595"
@ -1372,6 +1379,13 @@
"@nodelib/fs.scandir" "2.1.5"
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":
version "0.5.20"
resolved "https://registry.yarnpkg.com/@react-native-community/checkbox/-/checkbox-0.5.20.tgz#7b8004a80a90988d5dff208693524f069270b849"
@ -1716,18 +1730,18 @@
nullthrows "^1.1.1"
"@react-navigation/bottom-tabs@^7.18.3":
version "7.18.3"
resolved "https://registry.yarnpkg.com/@react-navigation/bottom-tabs/-/bottom-tabs-7.18.3.tgz#371c39aee03aa90979d64e8d97da78559fbbcc92"
integrity sha512-715guj97M1yklVkN9ikUl6KJL9wO6hNot7URyUm+A1r5ltEHVgahKTgR8w7e4dNwiMjUjAnOdSErR0LaX1h+kQ==
version "7.18.4"
resolved "https://registry.yarnpkg.com/@react-navigation/bottom-tabs/-/bottom-tabs-7.18.4.tgz#a0157c4d449bec8e5440967934c07ef27dca81fc"
integrity sha512-L7D1tlPnIsXlp45iZCpAIRRKlZSuqM9WyUNFbooRTlcaaLDSlOr0X6cKbGmJmCvSOoqT6bAsQjCKfb5WRqhtQA==
dependencies:
"@react-navigation/elements" "^2.9.26"
"@react-navigation/elements" "^2.9.27"
color "^4.2.3"
sf-symbols-typescript "^2.1.0"
"@react-navigation/core@^7.21.2":
version "7.21.2"
resolved "https://registry.yarnpkg.com/@react-navigation/core/-/core-7.21.2.tgz#448591d27d6c1472491a8ea9b901954ea727a310"
integrity sha512-EsJhQnsL5NuIhXsWF7URijS5hd2AaJUREdq1fOIowlyNNQBA3jCnkGU0DkW7+eI40cQ+GXH8DQw+ci7TefLIxQ==
"@react-navigation/core@^7.21.3":
version "7.21.3"
resolved "https://registry.yarnpkg.com/@react-navigation/core/-/core-7.21.3.tgz#a4fc63da29f3dcbcbfe63da95b32c88d969c980c"
integrity sha512-HUoGmT9IdpKpbAl9AZJmXFbid+jQWQWjzww9vV9uBSd+8fa1hTwM3UBsZBHrpMru0s1ikwknoAIwV827fqGlhA==
dependencies:
"@react-navigation/routers" "^7.6.0"
escape-string-regexp "^4.0.0"
@ -1738,21 +1752,21 @@
use-latest-callback "^0.2.4"
use-sync-external-store "^1.5.0"
"@react-navigation/elements@^2.9.26":
version "2.9.26"
resolved "https://registry.yarnpkg.com/@react-navigation/elements/-/elements-2.9.26.tgz#89cd09db890534c115031b1a6d99d916fdcda96c"
integrity sha512-EaHSJf42MjnH5VFL+KZfQIyqJvdQWK9Z27J7WUSuIVX5NXlR925sPmhWf540DX9gD1ny8BfUGPZAuw/PO4tK2w==
"@react-navigation/elements@^2.9.27":
version "2.9.27"
resolved "https://registry.yarnpkg.com/@react-navigation/elements/-/elements-2.9.27.tgz#74247f508a7ad38fe03321cd6acfbfd3d1898716"
integrity sha512-6bka/gWvP3+5Dw9Gf2ac83y/F0XEl2ktezc6Yp3gJBs22Rm9dluGKphe1B2Hj33XoCMRofxL8w3z3kmtlJiC0Q==
dependencies:
color "^4.2.3"
use-latest-callback "^0.2.4"
use-sync-external-store "^1.5.0"
"@react-navigation/native@^7.3.4":
version "7.3.4"
resolved "https://registry.yarnpkg.com/@react-navigation/native/-/native-7.3.4.tgz#a9ba7b0fde595b019a16a87ab609ad5b618bfbef"
integrity sha512-zyAqFLeZuaakerMMnhYqBeGAzJ+WFQUTgezP3FxSlXNwFq5XxnjP28vi2XHGqm4zg4pGWls9Ay4JKshIHUOpwA==
version "7.3.5"
resolved "https://registry.yarnpkg.com/@react-navigation/native/-/native-7.3.5.tgz#d56ba57e130844f5eeac4c9d06e57386834c8330"
integrity sha512-lzcRpMoLCIypXXKm8KhpyZ81XpQBO/i8Oy1due4kUR97kG8kBlJxKUeP8yNnT5Fcl85rNdLXWbpwEr+9HPu4Ig==
dependencies:
"@react-navigation/core" "^7.21.2"
"@react-navigation/core" "^7.21.3"
escape-string-regexp "^4.0.0"
fast-deep-equal "^3.1.3"
nanoid "^3.3.11"
@ -1767,11 +1781,11 @@
nanoid "^3.3.11"
"@react-navigation/stack@^7.10.6":
version "7.10.6"
resolved "https://registry.yarnpkg.com/@react-navigation/stack/-/stack-7.10.6.tgz#5adf039d106a097bcb5514f07c1afc9e3ec75483"
integrity sha512-LWtBSCPNi3Htnb3051wan5hVTsoORxUocBeuAjZ3VS1u6wTtfObeqOOhanz/L+uiAzvTWcAHhevQhtlGeFn/mg==
version "7.10.7"
resolved "https://registry.yarnpkg.com/@react-navigation/stack/-/stack-7.10.7.tgz#76b91b693bbc00182090a2c71743cb8c5754409b"
integrity sha512-rkJ9CEJnOHRSBVYNq7M8+0a34munKcTho/HSJiniRM3ZDjWR0aQKDXZcYpKi+IQ8jKv2XyYOvZLGHrBNJc5ipA==
dependencies:
"@react-navigation/elements" "^2.9.26"
"@react-navigation/elements" "^2.9.27"
color "^4.2.3"
use-latest-callback "^0.2.4"
@ -1878,6 +1892,11 @@
dependencies:
"@types/node" "*"
"@types/hammerjs@^2.0.36":
version "2.0.46"
resolved "https://registry.yarnpkg.com/@types/hammerjs/-/hammerjs-2.0.46.tgz#381daaca1360ff8a7c8dff63f32e69745b9fb1e1"
integrity sha512-ynRvcq6wvqexJ9brDMS4BnBLzmr0e14d6ZJTEShTBWKymQiHwlAyGu0ZPEFI2Fh1U53F7tN9ufClWM5KvqkKOw==
"@types/istanbul-lib-coverage@*", "@types/istanbul-lib-coverage@^2.0.0", "@types/istanbul-lib-coverage@^2.0.1":
version "2.0.6"
resolved "https://registry.yarnpkg.com/@types/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.6.tgz#7739c232a1fee9b4d3ce8985f314c0c6d33549d7"
@ -1906,9 +1925,9 @@
pretty-format "^29.0.0"
"@types/node@*":
version "26.0.1"
resolved "https://registry.yarnpkg.com/@types/node/-/node-26.0.1.tgz#4a60e2c7a6d68bd261e265f8983bfe1601263ce3"
integrity sha512-fc3KiUoBt6kie0N9bIW3E47vZsuaMf0PM2AaUpLCLT0s/LvX1nxAim6Fc049cNxODPpGm6qRAuUOB86SkRuPQw==
version "26.1.0"
resolved "https://registry.yarnpkg.com/@types/node/-/node-26.1.0.tgz#aa85f0727fc5611347091c478341c63650903439"
integrity sha512-O0A1G3xPGy4w7AgQdAQYUlQ+BKk2Oovw8eRpofyp5KdBZULnbe+WqaOVNrm705SHphCiG4XHsACrSmPu1f+Kgw==
dependencies:
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"
integrity sha512-xRQbDb9BnwDafYNn6Vwl839DYVjqXYb1XVGtWAZ1kcDc6iwAL4hg3B1dZlRiuENFeO2H53gFG3in621AdERVAg==
agent-base@6:
version "6.0.2"
resolved "https://registry.yarnpkg.com/agent-base/-/agent-base-6.0.2.tgz#49fff58577cfee3f37176feab4c22e00f86d7f77"
integrity sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==
dependencies:
debug "4"
agent-base@^7.1.2:
version "7.1.4"
resolved "https://registry.yarnpkg.com/agent-base/-/agent-base-7.1.4.tgz#e3cd76d4c548ee895d3c3fd8dc1f6c5b9032e7a8"
@ -2275,6 +2301,11 @@ async-limiter@~1.0.0:
resolved "https://registry.yarnpkg.com/async-limiter/-/async-limiter-1.0.1.tgz#dd379e94f0db8310b08291f9d64c3209766617fd"
integrity sha512-csOlWGAcRFJaI6m+F2WKdnMKr4HhdhFVBk0H/QbJFMCr+uO2kwohwXQPxw/9OCxp05r5ghVBFSyioixx3gfkNQ==
asynckit@^0.4.0:
version "0.4.0"
resolved "https://registry.yarnpkg.com/asynckit/-/asynckit-0.4.0.tgz#c79ed97f7f34cb8f2ba1bc9790bcc366474b4b79"
integrity sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==
available-typed-arrays@^1.0.7:
version "1.0.7"
resolved "https://registry.yarnpkg.com/available-typed-arrays/-/available-typed-arrays-1.0.7.tgz#a5cc375d6a03c2efc87a553f3e0b1522def14846"
@ -2282,6 +2313,16 @@ available-typed-arrays@^1.0.7:
dependencies:
possible-typed-array-names "^1.0.0"
axios@^1.18.1:
version "1.18.1"
resolved "https://registry.yarnpkg.com/axios/-/axios-1.18.1.tgz#d63f9863bcd8938815c86f9e2abd380189d96dfe"
integrity sha512-3nTvFlvpn9Zu/RkHUqtc7/+al4UpRW5az71ap5zccp6e8RAYEzhMTecX8Dz1wWDYrPpUoB1HAQEGEAEvUr7S9g==
dependencies:
follow-redirects "^1.16.0"
form-data "^4.0.5"
https-proxy-agent "^5.0.1"
proxy-from-env "^2.1.0"
babel-jest@^29.7.0:
version "29.7.0"
resolved "https://registry.yarnpkg.com/babel-jest/-/babel-jest-29.7.0.tgz#f4369919225b684c56085998ac63dbd05be020d5"
@ -2704,6 +2745,13 @@ colorette@^1.0.7:
resolved "https://registry.yarnpkg.com/colorette/-/colorette-1.4.0.tgz#5190fbb87276259a86ad700bff2c6d6faa3fca40"
integrity sha512-Y2oEozpomLn7Q3HFP7dpww7AtMJplbM9lGZP6RDfHqmbeRjiwRg4n6VM6j4KLmRke85uWEI7JqF17f3pqdRA0g==
combined-stream@^1.0.8:
version "1.0.8"
resolved "https://registry.yarnpkg.com/combined-stream/-/combined-stream-1.0.8.tgz#c3d45a8b34fd730631a110a8a2520682b31d5a7f"
integrity sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==
dependencies:
delayed-stream "~1.0.0"
command-exists@^1.2.8:
version "1.2.9"
resolved "https://registry.yarnpkg.com/command-exists/-/command-exists-1.2.9.tgz#c50725af3808c8ab0260fd60b01fbfa25b954f69"
@ -2933,6 +2981,11 @@ define-properties@^1.1.3, define-properties@^1.2.1:
has-property-descriptors "^1.0.0"
object-keys "^1.1.1"
delayed-stream@~1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/delayed-stream/-/delayed-stream-1.0.0.tgz#df3ae199acadfb7d440aaae0b29e2272b24ec619"
integrity sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==
depd@2.0.0, depd@~2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/depd/-/depd-2.0.0.tgz#b696163cc757560d09cf22cc8fad1571b79e76df"
@ -3012,9 +3065,9 @@ ee-first@1.1.1:
integrity sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==
electron-to-chromium@^1.5.376:
version "1.5.382"
resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.5.382.tgz#e76f05d3ec337524b9c61761ebbc16fe91ecf5b4"
integrity sha512-8ETaWbV6SZOrno+G93Ffd9ENsMtetqdnqj4nlfxFW90Sm5GgnuV28Kf62hqQVD6VUgzm7qFQKsTsAPmeUiU3Ug==
version "1.5.383"
resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.5.383.tgz#5bd22306497d454103b289b0fef97260c56d0855"
integrity sha512-I2484/KkAvl8lm9VyjH2JnbOIV0d/UCqT7gbzs6l+o6Vmn9wgB66uVcKX+Vk6HrXtY6fbWTOEXuv8waDTuFNCw==
emittery@^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"
integrity sha512-3PYnM29RFXwvAN6Pc/scUfkI7RwhQ/xqyLUyPNlXUp9S40zI8nup9tUSrTLSVnWGBN38FNiGWbwZOB6uR4OGdw==
follow-redirects@^1.16.0:
version "1.16.0"
resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.16.0.tgz#28474a159d3b9d11ef62050a14ed60e4df6d61bc"
integrity sha512-y5rN/uOsadFT/JfYwhxRS5R7Qce+g3zG97+JrtFZlC9klX/W5hD7iiLzScI4nZqUS7DNUdhPgw4xI8W2LuXlUw==
for-each@^0.3.3, for-each@^0.3.5:
version "0.3.5"
resolved "https://registry.yarnpkg.com/for-each/-/for-each-0.3.5.tgz#d650688027826920feeb0af747ee7b9421a41d47"
@ -3620,6 +3678,17 @@ for-each@^0.3.3, for-each@^0.3.5:
dependencies:
is-callable "^1.2.7"
form-data@^4.0.5:
version "4.0.6"
resolved "https://registry.yarnpkg.com/form-data/-/form-data-4.0.6.tgz#28e864e1b786dbebb68db1f452f9635278665827"
integrity sha512-vKatAh4SlVfgbv+YtmhiRjhEMJsYpsG1Y2rMQtR+SVSbytsSD1YGzDIcrAJmdFec88u/+VoGmxnl+80gL1tRCQ==
dependencies:
asynckit "^0.4.0"
combined-stream "^1.0.8"
es-set-tostringtag "^2.1.0"
hasown "^2.0.4"
mime-types "^2.1.35"
fresh@~0.5.2:
version "0.5.2"
resolved "https://registry.yarnpkg.com/fresh/-/fresh-0.5.2.tgz#3d8cadd90d976569fa835ab1f8e4b23a105605a7"
@ -3877,6 +3946,13 @@ hermes-parser@^0.25.1:
dependencies:
hermes-estree "0.25.1"
hoist-non-react-statics@^3.3.0:
version "3.3.2"
resolved "https://registry.yarnpkg.com/hoist-non-react-statics/-/hoist-non-react-statics-3.3.2.tgz#ece0acaf71d62c2969c2ec59feff42a4b1a85b45"
integrity sha512-/gGivxi8JPKWNm/W0jSmzcMPpfpPLc3dY/6GxhX2hQ9iGj3aDfklV4ET7NjKpSinLpJ5vafa9iiGIEZg10SfBw==
dependencies:
react-is "^16.7.0"
html-escaper@^2.0.0:
version "2.0.2"
resolved "https://registry.yarnpkg.com/html-escaper/-/html-escaper-2.0.2.tgz#dfd60027da36a36dfcbe236262c00a5822681453"
@ -3893,6 +3969,14 @@ http-errors@~2.0.1:
statuses "~2.0.2"
toidentifier "~1.0.1"
https-proxy-agent@^5.0.1:
version "5.0.1"
resolved "https://registry.yarnpkg.com/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz#c59ef224a04fe8b754f3db0063a25ea30d0005d6"
integrity sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==
dependencies:
agent-base "6"
debug "4"
https-proxy-agent@^7.0.5:
version "7.0.6"
resolved "https://registry.yarnpkg.com/https-proxy-agent/-/https-proxy-agent-7.0.6.tgz#da8dfeac7da130b05c2ba4b59c9b6cd66611a6b9"
@ -3913,6 +3997,11 @@ iconv-lite@~0.4.24:
dependencies:
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:
version "1.2.1"
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"
immer@^11.0.0:
version "11.1.8"
resolved "https://registry.yarnpkg.com/immer/-/immer-11.1.8.tgz#08a6426f7019dbce8d6dff8c4a43bb25c550a575"
integrity sha512-/tbkHMW7y10Lx6i1crLjD4/OhNkRG+Fo7byZHtah0547nIeXYcpIXaUh0IAQY6gO5459qpGGYapcEOHtFXkIuA==
version "11.1.9"
resolved "https://registry.yarnpkg.com/immer/-/immer-11.1.9.tgz#e0ce69359d29cafdb14fc5b6d2cd56f826097966"
integrity sha512-sc/z0Cyti70bZa0ZU4sWfAElfovFb9Ni8tArJZLuklYWxegPiK3pDOql1Rq5H0FIRAW9LSQRG6OX4KqBldbhBA==
import-fresh@^3.2.1, import-fresh@^3.3.0:
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"
integrity sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==
mime-types@^2.1.35, mime-types@~2.1.24, mime-types@~2.1.34:
version "2.1.35"
resolved "https://registry.yarnpkg.com/mime-types/-/mime-types-2.1.35.tgz#381a871b62a734450660ae3deee44813f70d959a"
integrity sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==
dependencies:
mime-db "1.52.0"
mime-types@^3.0.0, mime-types@^3.0.1:
version "3.0.2"
resolved "https://registry.yarnpkg.com/mime-types/-/mime-types-3.0.2.tgz#39002d4182575d5af036ffa118100f2524b2e2ab"
@ -5148,13 +5244,6 @@ mime-types@^3.0.0, mime-types@^3.0.1:
dependencies:
mime-db "^1.54.0"
mime-types@~2.1.24, mime-types@~2.1.34:
version "2.1.35"
resolved "https://registry.yarnpkg.com/mime-types/-/mime-types-2.1.35.tgz#381a871b62a734450660ae3deee44813f70d959a"
integrity sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==
dependencies:
mime-db "1.52.0"
mime@1.6.0:
version "1.6.0"
resolved "https://registry.yarnpkg.com/mime/-/mime-1.6.0.tgz#32cd9e5c64553bd58d19a568af452acff04981b1"
@ -5620,6 +5709,11 @@ prop-types@^15.8.1:
object-assign "^4.1.1"
react-is "^16.13.1"
proxy-from-env@^2.1.0:
version "2.1.0"
resolved "https://registry.yarnpkg.com/proxy-from-env/-/proxy-from-env-2.1.0.tgz#a7487568adad577cfaaa7e88c49cab3ab3081aba"
integrity sha512-cJ+oHTW1VAEa8cJslgmUZrc+sjRKgAKl3Zyse6+PV38hZe/V6Z14TbCuXcan9F9ghlz4QrFr2c92TNF82UkYHA==
punycode@^2.1.0:
version "2.3.1"
resolved "https://registry.yarnpkg.com/punycode/-/punycode-2.3.1.tgz#027422e2faec0b25e1549c3e1bd8309b9133b6e5"
@ -5688,7 +5782,7 @@ react-freeze@^1.0.0:
resolved "https://registry.yarnpkg.com/react-freeze/-/react-freeze-1.0.4.tgz#cbbea2762b0368b05cbe407ddc9d518c57c6f3ad"
integrity sha512-r4F0Sec0BLxWicc7HEyo2x3/2icUTrRmDjaaRyzzn+7aDyFZliszMDOgLVwSnQnYENOlL1o569Ze2HZefk8clA==
react-is@^16.13.1:
react-is@^16.13.1, react-is@^16.7.0:
version "16.13.1"
resolved "https://registry.yarnpkg.com/react-is/-/react-is-16.13.1.tgz#789729a4dc36de2999dc156dd6c1d9c18cea56a4"
integrity sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==
@ -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"
integrity sha512-kZFnouyVv7eP/Phmrlo9FK+zcAdriZJvzxXHF1Sl1P377WSGe2G/JxVolhTrB/jeV47lKImhNUsijjHAAbcl/A==
react-native-gesture-handler@^3.0.2:
version "3.0.2"
resolved "https://registry.yarnpkg.com/react-native-gesture-handler/-/react-native-gesture-handler-3.0.2.tgz#ea80fd2424d08f8b624977221f0544a5201f6d83"
integrity sha512-XBTgmvr2jh/xrMwlHTV2Z1x6IFUl3zfLxyaCAnvj3GSXK5YlY3ZmiIs57hniPmh3I7RtjPFxtGdRr0i+PzyBrg==
react-native-geolocation-service@^5.3.1:
version "5.3.1"
resolved "https://registry.yarnpkg.com/react-native-geolocation-service/-/react-native-geolocation-service-5.3.1.tgz#4ce1017789da6fdfcf7576eb6f59435622af4289"
integrity sha512-LTXPtPNmrdhx+yeWG47sAaCgQc3nG1z+HLLHlhK/5YfOgfLcAb9HAkhREPjQKPZOUx8pKZMIpdGFUGfJYtimXQ==
react-native-gesture-handler@^2.32.0:
version "2.32.0"
resolved "https://registry.yarnpkg.com/react-native-gesture-handler/-/react-native-gesture-handler-2.32.0.tgz#59b8658a4e1586ce6fb0ca0053ac80f6acf7f882"
integrity sha512-uYIMOKlKENORq2SABE+jIjbPU+h5I/sQKcq2v16zRq848nwEp1fWRVwML4QWqijc8UcXJC25o54S8GQd4Mf2OA==
dependencies:
"@egjs/hammerjs" "^2.0.17"
"@types/react-test-renderer" "^19.1.0"
hoist-non-react-statics "^3.3.0"
invariant "^2.2.4"
react-native-is-edge-to-edge@^1.3.1:
@ -5753,9 +5854,9 @@ react-native-svg@^15.15.5:
css-tree "^1.1.3"
react-native-worklets@^0.10.0:
version "0.10.0"
resolved "https://registry.yarnpkg.com/react-native-worklets/-/react-native-worklets-0.10.0.tgz#e2e8f8e1ba0eccc4e69f3a4e75894ecab4ba19b0"
integrity sha512-JhE6IxDf6iabC0qu3+TAKA4v9RlluXmoIngPQX7/QUByf75lfrsHZ6/dQhyjEWnp1EEQiwzz8Cpew140ZcewDw==
version "0.10.1"
resolved "https://registry.yarnpkg.com/react-native-worklets/-/react-native-worklets-0.10.1.tgz#7a3038a3b5ec66cab030a00e1568b6599696f5f2"
integrity sha512-62mRM19bDpfpdI8HLkEErcdOsrAPDtE9lA/sw+5lLRpzBHNhxaoj9QyY2KjXqUmirelxkX4zuPGTC3VdA0feJA==
dependencies:
"@babel/plugin-transform-arrow-functions" "^7.27.1"
"@babel/plugin-transform-class-properties" "^7.28.6"
@ -5843,6 +5944,11 @@ readable-stream@^3.4.0:
string_decoder "^1.1.1"
util-deprecate "^1.0.1"
redux-persist@^6.0.0:
version "6.0.0"
resolved "https://registry.yarnpkg.com/redux-persist/-/redux-persist-6.0.0.tgz#b4d2972f9859597c130d40d4b146fecdab51b3a8"
integrity sha512-71LLMbUq2r02ng2We9S215LtPu3fY0KgaGE0k8WRgl6RkqxtGfl7HUozz1Dftwsb0D/5mZ8dwAaPbtnzfvbEwQ==
redux-thunk@^3.1.0:
version "3.1.0"
resolved "https://registry.yarnpkg.com/redux-thunk/-/redux-thunk-3.1.0.tgz#94aa6e04977c30e14e892eae84978c1af6058ff3"