diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml index fb78f39..26d410d 100644 --- a/android/app/src/main/AndroidManifest.xml +++ b/android/app/src/main/AndroidManifest.xml @@ -1,6 +1,8 @@ + + + diff --git a/app/App.tsx b/app/App.tsx index 311e27e..22fcca9 100644 --- a/app/App.tsx +++ b/app/App.tsx @@ -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 ( - - - - - - + + + + + + + + ); } diff --git a/app/api/authApi.ts b/app/api/authApi.ts new file mode 100644 index 0000000..481e5a1 --- /dev/null +++ b/app/api/authApi.ts @@ -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('/auth/login', { mobileNumber }); + +export const verifyOtpApi = (mobileNumber: string, otp: string) => + apiClient.post('/auth/verify-otp', { mobileNumber, otp }); + +export const updateProfileApi = (data: { + name: string; + email: string; + locationLabels: string[]; +}) => apiClient.put('/auth/profile', data); + +export const logoutApi = () => + apiClient.post('/auth/logout', {}); diff --git a/app/api/deliveryApi.ts b/app/api/deliveryApi.ts new file mode 100644 index 0000000..a73193a --- /dev/null +++ b/app/api/deliveryApi.ts @@ -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('/providers'); + +export const getProviderCatalogApi = (providerId: string) => + apiClient.get(`/providers/${providerId}/catalog`); + +export const searchProvidersApi = (query: string) => + apiClient.get(`/search?q=${query}`); + +export const placeOrderApi = (orderData: Partial) => + apiClient.post('/orders', orderData); + +export const getDeliveryAgentApi = () => + apiClient.get('/delivery/agent'); + +export const getLiveLocationApi = () => + apiClient.get('/delivery/live-location'); diff --git a/app/api/index.ts b/app/api/index.ts new file mode 100644 index 0000000..63d9991 --- /dev/null +++ b/app/api/index.ts @@ -0,0 +1,2 @@ +export * from './authApi'; +export * from './deliveryApi'; diff --git a/app/components/CustomInput/CustomInput.props.ts b/app/components/CustomInput/CustomInput.props.ts index bfffb6c..374b762 100644 --- a/app/components/CustomInput/CustomInput.props.ts +++ b/app/components/CustomInput/CustomInput.props.ts @@ -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; + /** + * 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; } diff --git a/app/components/CustomInput/CustomInput.styles.ts b/app/components/CustomInput/CustomInput.styles.ts index f5feeb7..6fbdf51 100644 --- a/app/components/CustomInput/CustomInput.styles.ts +++ b/app/components/CustomInput/CustomInput.styles.ts @@ -1,49 +1,56 @@ import { StyleSheet } from 'react-native'; import { typography } from '@theme'; -export const getStyles = (colors: any) => StyleSheet.create({ - container: { - marginBottom: 20, - width: '100%', - }, - labelRow: { - flexDirection: 'row', - justifyContent: 'space-between', - alignItems: 'center', - marginBottom: 8, - }, - label: { - fontSize: typography.fontSize.sm, - fontWeight: typography.fontWeight.medium, - color: colors.textSecondary, - }, - rightAction: { - fontSize: typography.fontSize.sm, - fontWeight: typography.fontWeight.semibold, - color: colors.primary, - }, - inputContainer: { - flexDirection: 'row', - alignItems: 'center', - borderWidth: 1, - borderColor: colors.border, - borderRadius: 12, - backgroundColor: colors.inputBg, - paddingHorizontal: 16, - height: 56, - }, - input: { - flex: 1, - height: '100%', - color: colors.text, - fontSize: typography.fontSize.md, - fontWeight: typography.fontWeight.regular, - paddingVertical: 0, - }, - errorText: { - color: colors.error, - fontSize: typography.fontSize.xs, - marginTop: 4, - marginLeft: 4, - }, -}); +export const getStyles = (colors: any) => + StyleSheet.create({ + container: { + marginBottom: 20, + width: '100%', + }, + labelRow: { + flexDirection: 'row', + justifyContent: 'space-between', + alignItems: 'center', + marginBottom: 8, + }, + label: { + fontSize: typography.fontSize.sm, + fontWeight: typography.fontWeight.medium, + color: colors.textSecondary, + }, + rightAction: { + fontSize: typography.fontSize.sm, + fontWeight: typography.fontWeight.semibold, + color: colors.primary, + }, + inputContainer: { + flexDirection: 'row', + alignItems: 'center', + borderWidth: 1, + borderColor: colors.border, + borderRadius: 12, + backgroundColor: colors.inputBg, + paddingHorizontal: 16, + height: 56, + }, + leftElementWrap: { + marginRight: 10, + paddingRight: 10, + borderRightWidth: 1, + borderRightColor: colors.border, + }, + input: { + flex: 1, + height: '100%', + color: colors.text, + fontSize: typography.fontSize.md, + fontWeight: typography.fontWeight.regular, + paddingVertical: 0, + }, + errorText: { + color: colors.error, + fontSize: typography.fontSize.xs, + marginTop: 4, + marginLeft: 4, + }, + }); diff --git a/app/components/CustomInput/CustomInput.tsx b/app/components/CustomInput/CustomInput.tsx index 7216143..972ca15 100644 --- a/app/components/CustomInput/CustomInput.tsx +++ b/app/components/CustomInput/CustomInput.tsx @@ -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 = ({ @@ -12,6 +12,7 @@ export const CustomInput: React.FC = ({ error, secureTextEntry, style, + leftElement, ...rest }) => { const { colors } = useAppTheme(); @@ -25,7 +26,16 @@ export const CustomInput: React.FC = ({ {label} )} - + + {leftElement && ( + {leftElement} + )} = ({ /> {isPassword && ( - setIsSecure(!isSecure)} activeOpacity={0.7}> - {isSecure ? 'Show' : 'Hide'} + setIsSecure(!isSecure)} + activeOpacity={0.7} + > + + {isSecure ? 'Show' : 'Hide'} + )} {rightActionText && onRightActionPress && ( diff --git a/app/components/CustomInput/index.ts b/app/components/CustomInput/index.ts index 60e1ef1..d483952 100644 --- a/app/components/CustomInput/index.ts +++ b/app/components/CustomInput/index.ts @@ -1,2 +1,2 @@ -export * from './customInput'; -export * from './customInput.props'; +export * from './CustomInput'; +export * from './CustomInput.props'; diff --git a/app/components/PrimaryButton/PrimaryButton.tsx b/app/components/PrimaryButton/PrimaryButton.tsx index 91bb7b2..6aee4b2 100644 --- a/app/components/PrimaryButton/PrimaryButton.tsx +++ b/app/components/PrimaryButton/PrimaryButton.tsx @@ -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 = ({ diff --git a/app/components/PrimaryButton/index.ts b/app/components/PrimaryButton/index.ts index a841b05..78d30a4 100644 --- a/app/components/PrimaryButton/index.ts +++ b/app/components/PrimaryButton/index.ts @@ -1,2 +1,2 @@ -export * from './primaryButton'; -export * from './primaryButton.props'; +export * from './PrimaryButton'; +export * from './PrimaryButton.props'; diff --git a/app/components/SocialButton/SocialButton.tsx b/app/components/SocialButton/SocialButton.tsx index 1d1ffe4..f173fe9 100644 --- a/app/components/SocialButton/SocialButton.tsx +++ b/app/components/SocialButton/SocialButton.tsx @@ -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 }) => ( ); -const AppleIcon = ({ style, color }: { style: any, color: string }) => ( +const AppleIcon = ({ style, color }: { style: any; color: string }) => ( void; + onPress: () => void; } diff --git a/app/components/providerCard/providerCard.styles.ts b/app/components/providerCard/providerCard.styles.ts index d60589a..0e35728 100644 --- a/app/components/providerCard/providerCard.styles.ts +++ b/app/components/providerCard/providerCard.styles.ts @@ -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({ - container: { - flexDirection: 'row', - backgroundColor: colors.cardBg, - borderRadius: 12, - padding: 12, - marginHorizontal: 16, - marginVertical: 6, - borderWidth: 1, - borderColor: colors.border, - }, - image: { - width: 80, - height: 80, - borderRadius: 8, - backgroundColor: colors.border, - justifyContent: 'center', - alignItems: 'center', - }, - imagePlaceholder: { - fontSize: 32, - }, - info: { - flex: 1, - marginLeft: 12, - justifyContent: 'center', - }, - name: { - fontSize: typography.fontSize.md, - fontWeight: typography.fontWeight.semibold, - color: colors.text, - marginBottom: 4, - }, - row: { - flexDirection: 'row', - alignItems: 'center', - marginBottom: 2, - }, - rating: { - fontSize: typography.fontSize.sm, - color: colors.textSecondary, - marginRight: 8, - }, - deliveryTime: { - fontSize: typography.fontSize.sm, - color: colors.textSecondary, - }, - tag: { - fontSize: typography.fontSize.xs, - color: colors.primary, - 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, - }, -}); +export const getStyles = (colors: any) => + StyleSheet.create({ + container: { + backgroundColor: colors.cardBg, + borderRadius: 18, + marginHorizontal: 16, + 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: '100%', + height: '100%', + }, + imagePlaceholder: { + 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.bold, + color: colors.text, + flex: 1, + marginRight: 8, + }, + ratingPill: { + flexDirection: 'row', + alignItems: 'center', + backgroundColor: colors.primaryMuted ?? '#E9F7EF', + borderRadius: 8, + paddingHorizontal: 6, + paddingVertical: 3, + }, + ratingStar: { + fontSize: 11, + marginRight: 3, + }, + ratingText: { + fontSize: typography.fontSize.xs, + fontWeight: typography.fontWeight.bold, + color: colors.primary, + }, + + metaRow: { + flexDirection: 'row', + alignItems: 'center', + marginTop: 8, + }, + metaText: { + fontSize: typography.fontSize.sm, + color: colors.textSecondary, + }, + metaDivider: { + width: 3, + height: 3, + borderRadius: 1.5, + backgroundColor: colors.textSecondary, + marginHorizontal: 8, + opacity: 0.5, + }, + + tagPill: { + alignSelf: 'flex-start', + backgroundColor: colors.background, + borderRadius: 6, + paddingHorizontal: 8, + paddingVertical: 3, + marginTop: 10, + borderWidth: 1, + borderColor: colors.border, + }, + tagPillText: { + fontSize: typography.fontSize.xs, + color: colors.textSecondary, + fontWeight: typography.fontWeight.medium, + }, + }); diff --git a/app/components/providerCard/providerCard.tsx b/app/components/providerCard/providerCard.tsx index 82af2e8..10ae171 100644 --- a/app/components/providerCard/providerCard.tsx +++ b/app/components/providerCard/providerCard.tsx @@ -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 = ({ }) => { const { colors } = useAppTheme(); const styles = getStyles(colors); + const [isFavorite, setIsFavorite] = useState(false); return ( - - {imageUrl || '🍽️'} - - - {name} - - ⭐ {rating.toFixed(1)} - 🕐 {deliveryTime} - - {tag} - {discountText && ( - - {discountText} + + {imageUrl ? ( + + ) : ( + + 🍽️ )} + + {!!discountText && ( + + {discountText} + + )} + + setIsFavorite(prev => !prev)} + > + {isFavorite ? '❤️' : '🤍'} + + + + + + + {name} + + + + {rating.toFixed(1)} + + + + + 🕐 {deliveryTime} + + {tag} + + + + Free delivery over ₹199 + ); diff --git a/app/features/screens/LoginScreen/LoginScreen.styles.ts b/app/features/screens/LoginScreen/LoginScreen.styles.ts index 51a8b22..b65e2d5 100644 --- a/app/features/screens/LoginScreen/LoginScreen.styles.ts +++ b/app/features/screens/LoginScreen/LoginScreen.styles.ts @@ -1,32 +1,142 @@ -import { StyleSheet } from 'react-native'; +import { StyleSheet, Dimensions, Platform } from 'react-native'; import { typography } from '@theme'; -export const getStyles = (colors: any) => StyleSheet.create({ - container: { - flex: 1, - backgroundColor: colors.background, - paddingHorizontal: 24, - }, - scrollContainer: { - flexGrow: 1, - justifyContent: 'center', - paddingBottom: 24, - }, - headerContainer: { - marginTop: 40, - marginBottom: 32, - }, - title: { - fontSize: typography.fontSize.xxl, - fontWeight: typography.fontWeight.bold, - color: colors.text, - marginBottom: 8, - }, - subtitle: { - fontSize: typography.fontSize.md, - color: colors.textSecondary, - }, - formContainer: { - width: '100%', - }, -}); +const { width } = Dimensions.get('window'); + +export const getStyles = (colors: any) => + StyleSheet.create({ + container: { + flex: 1, + 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: { + marginBottom: 28, + }, + title: { + fontSize: typography.fontSize.xxl, + fontWeight: typography.fontWeight.bold, + color: colors.text, + marginBottom: 8, + }, + subtitle: { + fontSize: typography.fontSize.md, + color: colors.textSecondary, + }, + formContainer: { + width: '100%', + }, + + // Phone prefix chip rendered via CustomInput's leftElement + prefixChip: { + flexDirection: 'row', + alignItems: 'center', + }, + prefixFlag: { + fontSize: 16, + marginRight: 6, + }, + prefixText: { + fontSize: typography.fontSize.md, + fontWeight: typography.fontWeight.semibold, + color: colors.text, + }, + + helperText: { + fontSize: typography.fontSize.xs, + color: colors.textSecondary, + marginTop: -12, + marginBottom: 20, + marginLeft: 4, + }, + + // ---------- Footer ---------- + footerSpacer: { + flex: 1, + minHeight: 24, + }, + termsText: { + fontSize: typography.fontSize.xs, + color: colors.textSecondary, + textAlign: 'center', + lineHeight: 18, + marginTop: 20, + paddingHorizontal: 8, + }, + termsLink: { + color: colors.primary, + fontWeight: typography.fontWeight.semibold, + }, + }); diff --git a/app/features/screens/LoginScreen/LoginScreen.tsx b/app/features/screens/LoginScreen/LoginScreen.tsx index c2dfb75..10a8617 100644 --- a/app/features/screens/LoginScreen/LoginScreen.tsx +++ b/app/features/screens/LoginScreen/LoginScreen.tsx @@ -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; +import { useLoginScreen } from '../../../hooks'; export const LoginScreen: React.FC = () => { const { colors } = useAppTheme(); const styles = getStyles(colors); - const navigation = useNavigation(); - const dispatch = useAppDispatch(); - const [mobileNumber, setMobileNumber] = useState(''); - const [error, setError] = useState(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 ( { - - Welcome back - Login to continue + {/* Hero / branding */} + + + + + 🛵 + + QuickCart + Delivered fast, every time - - { - setMobileNumber(text); - if (error) setError(undefined); - }} - error={error} - /> + {/* Form card */} + + + Welcome back + Login to continue + - + + + 🇮🇳 + +91 + + } + /> + {!error && ( + + We'll send a one-time password to verify this number + + )} + + + + + + + + By continuing, you agree to our{' '} + Terms of Service and{' '} + Privacy Policy + diff --git a/app/features/screens/accountScreen/accountScreen.styles.ts b/app/features/screens/accountScreen/accountScreen.styles.ts index 17045d4..3b6b3ea 100644 --- a/app/features/screens/accountScreen/accountScreen.styles.ts +++ b/app/features/screens/accountScreen/accountScreen.styles.ts @@ -1,68 +1,224 @@ -import { StyleSheet } from 'react-native'; +import { StyleSheet, Platform } from 'react-native'; import { typography } from '@theme'; -export const getStyles = (colors: any) => StyleSheet.create({ - container: { - flex: 1, - backgroundColor: colors.background, - }, - content: { - paddingBottom: 40, - }, - profileCard: { - alignItems: 'center', - paddingVertical: 32, - borderBottomWidth: 1, - borderBottomColor: colors.border, - marginHorizontal: 16, - }, - avatar: { - width: 72, - height: 72, - borderRadius: 36, - backgroundColor: colors.primary, - justifyContent: 'center', - alignItems: 'center', - marginBottom: 12, - }, - avatarText: { - fontSize: 28, - fontWeight: typography.fontWeight.bold, - color: '#FFFFFF', - }, - profileName: { - fontSize: typography.fontSize.xl, - fontWeight: typography.fontWeight.bold, - color: colors.text, - marginBottom: 4, - }, - profilePhone: { - fontSize: typography.fontSize.md, - color: colors.textSecondary, - }, - menuSection: { - marginTop: 16, - paddingHorizontal: 16, - }, - menuItem: { - flexDirection: 'row', - alignItems: 'center', - paddingVertical: 16, - borderBottomWidth: 1, - borderBottomColor: colors.border, - }, - menuIcon: { - fontSize: 20, - marginRight: 14, - }, - menuLabel: { - flex: 1, - fontSize: typography.fontSize.md, - fontWeight: typography.fontWeight.medium, - color: colors.text, - }, - menuArrow: { - fontSize: 18, - color: colors.textSecondary, - }, -}); +export const getStyles = (colors: any) => + StyleSheet.create({ + container: { + flex: 1, + backgroundColor: colors.background, + }, + content: { + paddingBottom: 40, + }, + + // ---------- Profile card ---------- + profileCard: { + alignItems: 'center', + 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: 84, + height: 84, + borderRadius: 42, + backgroundColor: colors.primary, + justifyContent: 'center', + alignItems: 'center', + borderWidth: 3, + borderColor: colors.primaryMuted ?? '#E9F7EF', + }, + avatarText: { + 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: 24, + paddingHorizontal: 16, + }, + sectionLabel: { + fontSize: typography.fontSize.xs, + fontWeight: typography.fontWeight.semibold, + color: colors.textSecondary, + letterSpacing: 0.8, + textTransform: 'uppercase', + marginBottom: 10, + marginLeft: 4, + }, + sectionCard: { + backgroundColor: colors.cardBg, + borderRadius: 16, + overflow: 'hidden', + ...Platform.select({ + ios: { + shadowColor: '#000', + shadowOffset: { width: 0, height: 2 }, + shadowOpacity: 0.04, + shadowRadius: 8, + }, + android: { elevation: 1 }, + }), + }, + menuItem: { + flexDirection: 'row', + alignItems: 'center', + paddingVertical: 14, + paddingHorizontal: 14, + }, + menuItemDivider: { + borderBottomWidth: 1, + borderBottomColor: colors.border, + }, + menuIconBadge: { + width: 38, + height: 38, + borderRadius: 12, + alignItems: 'center', + justifyContent: 'center', + marginRight: 14, + }, + menuIcon: { + fontSize: 17, + }, + menuTextWrap: { + flex: 1, + }, + menuLabel: { + fontSize: typography.fontSize.md, + fontWeight: typography.fontWeight.medium, + color: colors.text, + }, + menuSubLabel: { + fontSize: typography.fontSize.xs, + color: colors.textSecondary, + marginTop: 1, + }, + menuArrowBadge: { + width: 24, + height: 24, + borderRadius: 12, + backgroundColor: colors.background, + alignItems: 'center', + justifyContent: 'center', + }, + menuArrow: { + fontSize: 13, + color: colors.textSecondary, + }, + + // ---------- Logout ---------- + logoutSection: { + paddingHorizontal: 16, + marginTop: 28, + }, + versionText: { + textAlign: 'center', + fontSize: typography.fontSize.xs, + color: colors.textSecondary, + marginTop: 18, + opacity: 0.7, + }, + }); diff --git a/app/features/screens/accountScreen/accountScreen.tsx b/app/features/screens/accountScreen/accountScreen.tsx index 5846909..ab3c5e8 100644 --- a/app/features/screens/accountScreen/accountScreen.tsx +++ b/app/features/screens/accountScreen/accountScreen.tsx @@ -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, + StackNavigationProp +>; + +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(); + 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 (
- + + {/* Profile card */} - - - {user?.name?.charAt(0)?.toUpperCase() || 'U'} - - - {user?.name || 'User'} - {user?.mobileNumber || '+91 98765 43210'} - - - - {MENU_ITEMS.map((item, index) => ( + + + + {user?.name?.charAt(0)?.toUpperCase() || 'U'} + + - {item.icon} - {item.label} - {'>'} + ✏️ - ))} + + + {user?.name || 'User'} + + 📞 + + {user?.mobileNumber || '+91 98765 43210'} + + + + + Edit Profile + - dispatch(logout())} - style={{ marginTop: 24 }} - /> + {/* Quick stats */} + {/* + + {ordersCount} + Orders + + + + {savedAddressesCount} + Addresses + + + + ₹{walletBalance} + Wallet + + */} + + {/* Menu sections */} + {MENU_SECTIONS.map(section => ( + + {section.title} + + {section.items.map((item, index) => { + const isLast = index === section.items.length - 1; + return ( + item.onPress?.(navigation)} + > + + {item.icon} + + + {item.label} + {item.subLabel} + + + {'>'} + + + ); + })} + + + ))} + + {/* Logout */} + + dispatch(logout())} /> + App version 1.0.0 +
); diff --git a/app/features/screens/cartScreen/cartScreen.styles.ts b/app/features/screens/cartScreen/cartScreen.styles.ts index 91c2dbc..213e0e0 100644 --- a/app/features/screens/cartScreen/cartScreen.styles.ts +++ b/app/features/screens/cartScreen/cartScreen.styles.ts @@ -1,109 +1,350 @@ -import { StyleSheet } from 'react-native'; +import { StyleSheet, Platform } from 'react-native'; import { typography } from '@theme'; -export const getStyles = (colors: any) => StyleSheet.create({ - container: { - flex: 1, - backgroundColor: colors.background, - }, - list: { - paddingBottom: 100, - }, - cartItem: { - flexDirection: 'row', - alignItems: 'center', - justifyContent: 'space-between', - paddingVertical: 14, - paddingHorizontal: 16, - borderBottomWidth: 1, - borderBottomColor: colors.border, - }, - itemInfo: { - flex: 1, - }, - itemTitle: { - fontSize: typography.fontSize.md, - fontWeight: typography.fontWeight.medium, - color: colors.text, - marginBottom: 4, - }, - itemPrice: { - fontSize: typography.fontSize.md, - fontWeight: typography.fontWeight.bold, - color: colors.text, - }, - footer: { - padding: 16, - }, - couponRow: { - flexDirection: 'row', - alignItems: 'center', - marginBottom: 20, - }, - couponInput: { - flex: 1, - height: 48, - borderWidth: 1, - borderColor: colors.border, - borderRadius: 12, - paddingHorizontal: 16, - fontSize: typography.fontSize.md, - color: colors.text, - backgroundColor: colors.inputBg, - marginRight: 8, - }, - applyButton: { - paddingHorizontal: 20, - paddingVertical: 14, - borderRadius: 12, - backgroundColor: colors.primary, - }, - applyButtonText: { - color: '#FFFFFF', - fontWeight: typography.fontWeight.semibold, - fontSize: typography.fontSize.sm, - }, - feeRow: { - flexDirection: 'row', - justifyContent: 'space-between', - marginBottom: 8, - }, - feeLabel: { - fontSize: typography.fontSize.sm, - color: colors.textSecondary, - }, - feeValue: { - fontSize: typography.fontSize.sm, - fontWeight: typography.fontWeight.medium, - color: colors.text, - }, - totalRow: { - borderTopWidth: 1, - borderTopColor: colors.border, - paddingTop: 12, - marginTop: 4, - }, - totalLabel: { - fontSize: typography.fontSize.md, - fontWeight: typography.fontWeight.bold, - color: colors.text, - }, - totalValue: { - fontSize: typography.fontSize.md, - fontWeight: typography.fontWeight.bold, - color: colors.text, - }, - bottomPanel: { - flexDirection: 'row', - alignItems: 'center', - padding: 16, - borderTopWidth: 1, - borderTopColor: colors.border, - backgroundColor: colors.background, - }, - bottomTotal: { - fontSize: typography.fontSize.lg, - fontWeight: typography.fontWeight.bold, - color: colors.text, - }, -}); +export const getStyles = (colors: any) => + StyleSheet.create({ + container: { + flex: 1, + backgroundColor: colors.background, + }, + list: { + 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', + paddingVertical: 14, + 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.semibold, + color: colors.text, + marginBottom: 4, + }, + itemPrice: { + 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, + }, + couponCard: { + backgroundColor: colors.cardBg, + borderRadius: 14, + padding: 6, + marginBottom: 20, + ...Platform.select({ + ios: { + shadowColor: '#000', + shadowOffset: { width: 0, height: 2 }, + shadowOpacity: 0.05, + shadowRadius: 8, + }, + android: { elevation: 1 }, + }), + }, + couponRow: { + flexDirection: 'row', + alignItems: 'center', + }, + couponIcon: { + fontSize: 18, + marginLeft: 10, + marginRight: 4, + }, + couponInput: { + flex: 1, + height: 44, + paddingHorizontal: 8, + fontSize: typography.fontSize.md, + color: colors.text, + }, + applyButton: { + paddingHorizontal: 18, + paddingVertical: 11, + borderRadius: 10, + backgroundColor: colors.primary, + marginRight: 4, + }, + applyButtonText: { + color: '#FFFFFF', + fontWeight: typography.fontWeight.semibold, + fontSize: typography.fontSize.sm, + }, + couponAppliedRow: { + flexDirection: 'row', + alignItems: 'center', + justifyContent: 'space-between', + paddingVertical: 10, + paddingHorizontal: 12, + }, + couponAppliedLeft: { + flexDirection: 'row', + alignItems: 'center', + flex: 1, + }, + couponCheckBadge: { + width: 28, + height: 28, + borderRadius: 14, + backgroundColor: colors.primaryMuted ?? '#E9F7EF', + alignItems: 'center', + justifyContent: 'center', + marginRight: 10, + }, + couponCheckIcon: { + fontSize: 13, + }, + couponAppliedCode: { + fontSize: typography.fontSize.sm, + fontWeight: typography.fontWeight.bold, + color: colors.text, + }, + couponAppliedSub: { + fontSize: typography.fontSize.xs, + color: colors.primary, + marginTop: 1, + }, + couponRemoveText: { + fontSize: typography.fontSize.sm, + fontWeight: typography.fontWeight.semibold, + color: '#D32F2F', + }, + + // Bill details + billCard: { + backgroundColor: colors.cardBg, + borderRadius: 14, + padding: 16, + ...Platform.select({ + ios: { + shadowColor: '#000', + shadowOffset: { width: 0, height: 2 }, + shadowOpacity: 0.05, + shadowRadius: 8, + }, + android: { elevation: 1 }, + }), + }, + feeRow: { + flexDirection: 'row', + justifyContent: 'space-between', + marginBottom: 10, + }, + feeLabel: { + fontSize: typography.fontSize.sm, + color: colors.textSecondary, + }, + feeValue: { + fontSize: typography.fontSize.sm, + fontWeight: typography.fontWeight.medium, + color: colors.text, + }, + totalRow: { + borderTopWidth: 1, + borderTopColor: colors.border, + paddingTop: 12, + marginTop: 4, + marginBottom: 0, + }, + totalLabel: { + fontSize: typography.fontSize.md, + fontWeight: typography.fontWeight.bold, + color: colors.text, + }, + totalValue: { + fontSize: typography.fontSize.md, + fontWeight: typography.fontWeight.bold, + color: colors.text, + }, + savingsBanner: { + flexDirection: 'row', + alignItems: 'center', + backgroundColor: colors.primaryMuted ?? '#E9F7EF', + borderRadius: 10, + paddingVertical: 8, + paddingHorizontal: 12, + marginTop: 14, + }, + savingsIcon: { + fontSize: 14, + marginRight: 6, + }, + savingsText: { + fontSize: typography.fontSize.xs, + fontWeight: typography.fontWeight.semibold, + color: colors.primary, + }, + + // ---------- Bottom panel ---------- + bottomPanel: { + position: 'absolute', + left: 0, + right: 0, + bottom: 0, + flexDirection: 'row', + alignItems: 'center', + justifyContent: 'space-between', + paddingHorizontal: 16, + paddingTop: 12, + paddingBottom: Platform.OS === 'ios' ? 28 : 16, + backgroundColor: colors.cardBg, + borderTopLeftRadius: 20, + borderTopRightRadius: 20, + ...Platform.select({ + ios: { + shadowColor: '#000', + shadowOffset: { width: 0, height: -4 }, + shadowOpacity: 0.08, + shadowRadius: 12, + }, + android: { elevation: 10 }, + }), + }, + bottomTotalWrap: {}, + bottomTotalLabel: { + fontSize: typography.fontSize.xs, + color: colors.textSecondary, + }, + bottomTotal: { + fontSize: typography.fontSize.lg, + fontWeight: typography.fontWeight.bold, + color: colors.text, + }, + checkoutButton: { + flex: 1, + marginLeft: 16, + }, + + // ---------- Empty state ---------- + emptyWrap: { + flex: 1, + alignItems: 'center', + justifyContent: 'center', + paddingHorizontal: 32, + }, + emptyEmoji: { + fontSize: 56, + marginBottom: 16, + }, + emptyTitle: { + fontSize: typography.fontSize.lg, + fontWeight: typography.fontWeight.bold, + color: colors.text, + marginBottom: 6, + }, + emptySubtitle: { + fontSize: typography.fontSize.sm, + color: colors.textSecondary, + textAlign: 'center', + marginBottom: 24, + }, + emptyButton: { + backgroundColor: colors.primary, + paddingHorizontal: 28, + paddingVertical: 14, + borderRadius: 12, + }, + emptyButtonText: { + color: '#FFFFFF', + fontWeight: typography.fontWeight.bold, + fontSize: typography.fontSize.md, + }, + }); diff --git a/app/features/screens/cartScreen/cartScreen.tsx b/app/features/screens/cartScreen/cartScreen.tsx index 72bc185..4c63f83 100644 --- a/app/features/screens/cartScreen/cartScreen.tsx +++ b/app/features/screens/cartScreen/cartScreen.tsx @@ -2,71 +2,162 @@ 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; 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(); + 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 ( + +
navigation.goBack()} /> + + 🛒 + Your cart is empty + + Looks like you haven't added anything yet. Browse providers and find + something you'll love. + + navigation.goBack()} + > + Start Ordering + + +
+ ); + } + return ( -
{}} /> - item.id} - renderItem={({ item }) => ( - - - {item.item.title} - ₹{item.item.price} - - {}} - onDecrement={() => dispatch(removeItem(item.item.id))} - /> - - )} - ListFooterComponent={ - - - - { - if (couponCode) { - dispatch(removeCoupon()); - setCouponInput(''); - } else if (couponInput) { - dispatch(applyCoupon(couponInput)); - } - }} - activeOpacity={0.7} - > - - {couponCode ? 'Remove' : 'Apply'} - - - +
navigation.goBack()} /> + + + 🛵 + Delivery in 20–25 mins + + + + {items.map((item, index) => ( + + + 🍕 + + + + {item.item.title} + + ₹{item.item.price} + + {}} + onDecrement={() => dispatch(removeItem(item.item.id))} + /> + + ))} + + + navigation.goBack()} + > + + + Add more items + + + + Apply Coupon + + {couponCode ? ( + + + + + + + {couponCode} + Coupon applied + + + + Remove + + + ) : ( + + 🏷️ + + + Apply + + + )} + + + Bill Details + Subtotal ₹{totals.subtotal} @@ -93,16 +184,28 @@ export const CartScreen: React.FC = () => { Total ₹{totals.total} + + {totals.discount > 0 && ( + + 🎉 + + You're saving ₹{totals.discount} on this order + + + )} - } - contentContainerStyle={styles.list} - /> + + + - Total: ₹{totals.total} + + Total + ₹{totals.total} + {}} - style={{ flex: 1, marginLeft: 12 }} + title="Proceed to Checkout" + onPress={() => navigation.navigate('CheckoutAddressScreen')} + style={styles.checkoutButton} />
diff --git a/app/features/screens/checkoutAddressScreen/checkoutAddressScreen.tsx b/app/features/screens/checkoutAddressScreen/checkoutAddressScreen.tsx index 70f2ee2..4747356 100644 --- a/app/features/screens/checkoutAddressScreen/checkoutAddressScreen.tsx +++ b/app/features/screens/checkoutAddressScreen/checkoutAddressScreen.tsx @@ -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; 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(); const [deliveryOption, setDeliveryOption] = useState<'standard' | 'express'>('standard'); return ( -
{}} /> +
navigation.goBack()} /> @@ -69,7 +75,7 @@ export const CheckoutAddressScreen: React.FC = () => {
- {}} /> + navigation.navigate('CheckoutPaymentScreen')} />
); diff --git a/app/features/screens/checkoutPaymentScreen/checkoutPaymentScreen.tsx b/app/features/screens/checkoutPaymentScreen/checkoutPaymentScreen.tsx index dd937bd..a9c0565 100644 --- a/app/features/screens/checkoutPaymentScreen/checkoutPaymentScreen.tsx +++ b/app/features/screens/checkoutPaymentScreen/checkoutPaymentScreen.tsx @@ -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; 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(); const [selectedMethod, setSelectedMethod] = useState('upi'); return ( -
{}} /> +
navigation.goBack()} /> Select Payment Method @@ -39,7 +45,7 @@ export const CheckoutPaymentScreen: React.FC = () => { ))} - {}} /> + navigation.navigate('OrderConfirmedScreen', { orderId: 'ORD-' + Math.floor(Math.random() * 900000 + 100000) })} /> ); diff --git a/app/features/screens/completeProfileScreen/completeProfileScreen.tsx b/app/features/screens/completeProfileScreen/completeProfileScreen.tsx index 8ae1d2e..23f2bbd 100644 --- a/app/features/screens/completeProfileScreen/completeProfileScreen.tsx +++ b/app/features/screens/completeProfileScreen/completeProfileScreen.tsx @@ -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; diff --git a/app/features/screens/helpSupportScreen/helpSupportScreen.tsx b/app/features/screens/helpSupportScreen/helpSupportScreen.tsx index ec1d8d2..0a463e1 100644 --- a/app/features/screens/helpSupportScreen/helpSupportScreen.tsx +++ b/app/features/screens/helpSupportScreen/helpSupportScreen.tsx @@ -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; 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(); return ( -
+
navigation.goBack()} /> Frequently Asked Questions {FAQS.map((faq, index) => ( diff --git a/app/features/screens/homeScreen/homeScreen.styles.ts b/app/features/screens/homeScreen/homeScreen.styles.ts index 8fa542b..39da1da 100644 --- a/app/features/screens/homeScreen/homeScreen.styles.ts +++ b/app/features/screens/homeScreen/homeScreen.styles.ts @@ -1,64 +1,388 @@ -import { StyleSheet } from 'react-native'; +import { StyleSheet, Dimensions, Platform } from 'react-native'; import { typography } from '@theme'; -export const getStyles = (colors: any) => StyleSheet.create({ - container: { - flex: 1, - backgroundColor: colors.background, - }, - addressBar: { - paddingHorizontal: 16, - paddingTop: 12, - paddingBottom: 4, - }, - addressLabel: { - fontSize: typography.fontSize.xs, - color: colors.textSecondary, - marginBottom: 2, - }, - addressRow: { - flexDirection: 'row', - alignItems: 'center', - }, - addressText: { - fontSize: typography.fontSize.md, - fontWeight: typography.fontWeight.semibold, - color: colors.text, - flex: 1, - }, - dropdownArrow: { - fontSize: 12, - color: colors.textSecondary, - marginLeft: 4, - }, - banner: { - backgroundColor: '#05824C', - marginHorizontal: 16, - borderRadius: 12, - padding: 20, - marginVertical: 8, - }, - bannerText: { - fontSize: typography.fontSize.xxl, - fontWeight: typography.fontWeight.bold, - color: '#FFFFFF', - }, - bannerSubtext: { - fontSize: typography.fontSize.sm, - color: '#FFFFFF', - opacity: 0.9, - marginTop: 4, - }, - chipRow: { - paddingHorizontal: 16, - paddingVertical: 12, - }, - sectionTitle: { - fontSize: typography.fontSize.lg, - fontWeight: typography.fontWeight.bold, - color: colors.text, - paddingHorizontal: 16, - marginBottom: 8, - marginTop: 4, - }, -}); +const { width } = Dimensions.get('window'); +const BANNER_WIDTH = width - 32; + +export const getStyles = (colors: any) => + StyleSheet.create({ + container: { + flex: 1, + backgroundColor: colors.background, + }, + + // ---------- Top bar ---------- + topBar: { + flexDirection: 'row', + alignItems: 'center', + justifyContent: 'space-between', + paddingHorizontal: 16, + 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: 1, + letterSpacing: 0.2, + }, + addressRow: { + flexDirection: 'row', + alignItems: 'center', + }, + addressText: { + fontSize: typography.fontSize.md, + fontWeight: typography.fontWeight.bold, + color: colors.text, + maxWidth: width * 0.55, + }, + dropdownArrow: { + fontSize: 11, + color: colors.textSecondary, + marginLeft: 6, + }, + avatarButton: { + width: 40, + height: 40, + borderRadius: 20, + backgroundColor: colors.surface ?? '#F2F2F2', + alignItems: 'center', + justifyContent: 'center', + borderWidth: 1, + borderColor: colors.border ?? '#ECECEC', + }, + avatarEmoji: { + fontSize: 18, + }, + notifDot: { + position: 'absolute', + top: 6, + right: 7, + width: 8, + height: 8, + borderRadius: 4, + backgroundColor: '#FF4D4F', + borderWidth: 1.5, + borderColor: colors.background, + }, + + // ---------- Search ---------- + searchWrap: { + paddingHorizontal: 16, + marginTop: 4, + marginBottom: 18, + }, + + // ---------- Promo carousel ---------- + bannerListContent: { + paddingLeft: 16, + paddingRight: 4, + }, + bannerCard: { + width: BANNER_WIDTH, + height: 140, + borderRadius: 20, + marginRight: 12, + padding: 20, + overflow: 'hidden', + justifyContent: 'space-between', + ...Platform.select({ + ios: { + shadowColor: '#000', + shadowOffset: { width: 0, height: 6 }, + shadowOpacity: 0.15, + shadowRadius: 12, + }, + android: { elevation: 5 }, + }), + }, + bannerDecoCircleLg: { + position: 'absolute', + width: 140, + height: 140, + borderRadius: 70, + backgroundColor: 'rgba(255,255,255,0.10)', + top: -50, + right: -30, + }, + bannerDecoCircleSm: { + position: 'absolute', + width: 80, + height: 80, + borderRadius: 40, + backgroundColor: 'rgba(255,255,255,0.08)', + bottom: -30, + right: 40, + }, + bannerEyebrow: { + fontSize: typography.fontSize.xs, + fontWeight: typography.fontWeight.semibold, + color: 'rgba(255,255,255,0.85)', + letterSpacing: 0.6, + textTransform: 'uppercase', + }, + bannerText: { + fontSize: 26, + fontWeight: typography.fontWeight.bold, + color: '#FFFFFF', + marginTop: 2, + }, + bannerSubtext: { + fontSize: typography.fontSize.sm, + color: 'rgba(255,255,255,0.9)', + marginTop: 2, + }, + bannerCta: { + alignSelf: 'flex-start', + backgroundColor: 'rgba(255,255,255,0.95)', + paddingHorizontal: 14, + paddingVertical: 7, + borderRadius: 20, + marginTop: 4, + }, + bannerCtaText: { + fontSize: typography.fontSize.xs, + fontWeight: typography.fontWeight.bold, + color: '#111111', + }, + dotsRow: { + flexDirection: 'row', + alignSelf: 'center', + marginTop: 10, + marginBottom: 4, + }, + dot: { + width: 6, + height: 6, + borderRadius: 3, + backgroundColor: colors.border ?? '#E0E0E0', + marginHorizontal: 3, + }, + dotActive: { + width: 16, + backgroundColor: colors.primary ?? '#05824C', + }, + + // ---------- Categories ---------- + categorySection: { + marginTop: 22, + }, + chipRow: { + paddingHorizontal: 16, + paddingTop: 4, + }, + categoryItem: { + alignItems: 'center', + marginRight: 18, + width: 64, + }, + categoryCircle: { + width: 60, + height: 60, + borderRadius: 18, + backgroundColor: colors.surface ?? '#F5F6F8', + alignItems: 'center', + justifyContent: 'center', + marginBottom: 6, + borderWidth: 1.5, + borderColor: 'transparent', + }, + categoryCircleSelected: { + backgroundColor: colors.primaryMuted ?? '#E9F7EF', + borderColor: colors.primary ?? '#05824C', + }, + categoryIcon: { + fontSize: 24, + }, + categoryLabel: { + fontSize: typography.fontSize.xs, + color: colors.textSecondary, + fontWeight: typography.fontWeight.medium, + textAlign: 'center', + }, + categoryLabelSelected: { + color: colors.text, + fontWeight: typography.fontWeight.bold, + }, + + // ---------- Section header ---------- + sectionHeaderRow: { + flexDirection: 'row', + alignItems: 'center', + justifyContent: 'space-between', + paddingHorizontal: 16, + marginTop: 26, + marginBottom: 12, + }, + sectionTitle: { + fontSize: typography.fontSize.lg, + fontWeight: typography.fontWeight.bold, + color: colors.text, + }, + sectionSubtitle: { + fontSize: typography.fontSize.xs, + color: colors.textSecondary, + marginTop: 2, + }, + seeAllText: { + fontSize: typography.fontSize.sm, + fontWeight: typography.fontWeight.semibold, + color: colors.primary ?? '#05824C', + }, + + // ---------- Provider cards ---------- + providerList: { + paddingHorizontal: 16, + }, + providerCardWrap: { + backgroundColor: colors.surface ?? '#FFFFFF', + borderRadius: 18, + marginBottom: 16, + overflow: 'hidden', + ...Platform.select({ + ios: { + shadowColor: '#000', + shadowOffset: { width: 0, height: 3 }, + shadowOpacity: 0.06, + shadowRadius: 10, + }, + android: { elevation: 2 }, + }), + }, + providerImageWrap: { + width: '100%', + height: 150, + position: 'relative', + }, + providerImage: { + width: '100%', + height: '100%', + }, + discountRibbon: { + position: 'absolute', + top: 12, + left: 0, + backgroundColor: '#05824C', + paddingVertical: 4, + paddingHorizontal: 10, + borderTopRightRadius: 8, + borderBottomRightRadius: 8, + }, + discountRibbonText: { + color: '#FFFFFF', + fontSize: typography.fontSize.xs, + fontWeight: typography.fontWeight.bold, + }, + favoriteButton: { + position: 'absolute', + top: 10, + right: 10, + width: 32, + height: 32, + borderRadius: 16, + backgroundColor: 'rgba(255,255,255,0.92)', + alignItems: 'center', + justifyContent: 'center', + }, + favoriteIcon: { + fontSize: 15, + }, + providerBody: { + padding: 14, + }, + providerTopRow: { + flexDirection: 'row', + justifyContent: 'space-between', + alignItems: 'flex-start', + }, + providerName: { + fontSize: typography.fontSize.md, + fontWeight: typography.fontWeight.bold, + color: colors.text, + flex: 1, + marginRight: 8, + }, + ratingPill: { + flexDirection: 'row', + alignItems: 'center', + backgroundColor: colors.primaryMuted ?? '#E9F7EF', + borderRadius: 8, + paddingHorizontal: 6, + paddingVertical: 3, + }, + ratingStar: { + fontSize: 11, + marginRight: 3, + }, + ratingText: { + fontSize: typography.fontSize.xs, + fontWeight: typography.fontWeight.bold, + color: colors.primary ?? '#05824C', + }, + metaRow: { + flexDirection: 'row', + alignItems: 'center', + marginTop: 8, + }, + metaText: { + fontSize: typography.fontSize.sm, + color: colors.textSecondary, + }, + metaDivider: { + width: 3, + height: 3, + borderRadius: 1.5, + backgroundColor: colors.textSecondary, + marginHorizontal: 8, + opacity: 0.5, + }, + tagPill: { + alignSelf: 'flex-start', + backgroundColor: colors.background, + borderRadius: 6, + paddingHorizontal: 8, + paddingVertical: 3, + marginTop: 10, + borderWidth: 1, + borderColor: colors.border ?? '#ECECEC', + }, + tagPillText: { + fontSize: typography.fontSize.xs, + color: colors.textSecondary, + fontWeight: typography.fontWeight.medium, + }, + + // ---------- Skeleton / empty ---------- + emptyWrap: { + alignItems: 'center', + paddingVertical: 40, + }, + emptyEmoji: { + fontSize: 36, + marginBottom: 8, + }, + emptyText: { + color: colors.textSecondary, + fontSize: typography.fontSize.sm, + }, + }); diff --git a/app/features/screens/homeScreen/homeScreen.tsx b/app/features/screens/homeScreen/homeScreen.tsx index aaec42e..d63e2ec 100644 --- a/app/features/screens/homeScreen/homeScreen.tsx +++ b/app/features/screens/homeScreen/homeScreen.tsx @@ -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; +type NavProp = CompositeNavigationProp< + BottomTabNavigationProp, + StackNavigationProp +>; + +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,73 +76,191 @@ export const HomeScreen: React.FC = () => { const [providers, setProviders] = useState([]); const [selectedCategory, setSelectedCategory] = useState('all'); + const [activeBanner, setActiveBanner] = useState(0); useEffect(() => { - deliveryService.getProviders().then(setProviders); + getProvidersApi().then(setProviders); }, []); - const filteredProviders = selectedCategory === 'all' - ? providers - : providers.filter((p) => p.tag === selectedCategory); + const filteredProviders = + selectedCategory === 'all' + ? providers + : providers.filter(p => p.tag === selectedCategory); const handleSearchFocus = useCallback(() => { navigation.navigate('SearchScreen'); }, [navigation]); + const handleBannerScroll = useCallback( + (e: NativeSyntheticEvent) => { + const index = Math.round(e.nativeEvent.contentOffset.x / BANNER_STEP); + setActiveBanner(index); + }, + [], + ); + return ( - - Deliver to - - Koramangala 4th Block, B... - + {/* Top bar */} + + + + 📍 + + + Deliver to + + + Koramangala 4th Block + + + + + + + + 🔔 + - {}} - placeholder="Search providers or items..." - onFocus={handleSearchFocus} - /> - - - 50% OFF - on your first order + {/* Search */} + + {}} + placeholder="Search providers or items..." + onFocus={handleSearchFocus} + /> + {/* Promo carousel */} item.key} + keyExtractor={item => item.key} showsHorizontalScrollIndicator={false} - contentContainerStyle={styles.chipRow} + contentContainerStyle={styles.bannerListContent} + snapToInterval={BANNER_STEP} + decelerationRate="fast" + onScroll={handleBannerScroll} + scrollEventThrottle={16} renderItem={({ item }) => ( - setSelectedCategory(item.key)} - /> + + + + + {item.eyebrow} + {item.title} + {item.subtitle} + + + {item.cta} + + )} /> + + {PROMOS.map((_, i) => ( + + ))} + - - {selectedCategory === 'all' ? 'Popular Providers' : selectedCategory} - - - {filteredProviders.map((provider) => ( - navigation.navigate('SearchScreen')} + {/* Categories */} + + item.key} + showsHorizontalScrollIndicator={false} + contentContainerStyle={styles.chipRow} + renderItem={({ item }) => { + const isSelected = selectedCategory === item.key; + return ( + { + setSelectedCategory(item.key); + if (item.key !== 'all') { + navigation.navigate('ProviderListScreen', { + category: item.key, + }); + } + }} + > + + {item.icon} + + + {item.label} + + + ); + }} /> - ))} + + + {/* Section header */} + + + + {selectedCategory === 'all' ? 'Popular near you' : selectedCategory} + + + {filteredProviders.length} place + {filteredProviders.length === 1 ? '' : 's'} delivering to you + + + + See all + + + + {/* Provider list */} + + {filteredProviders.length === 0 && ( + + 🍽️ + + No providers in this category yet + + + )} + + {filteredProviders.map(provider => ( + + navigation.navigate('ProviderDetailsScreen', { + providerId: provider.id, + }) + } + /> + ))} + diff --git a/app/features/screens/index.ts b/app/features/screens/index.ts index fcbffc6..dc530f7 100644 --- a/app/features/screens/index.ts +++ b/app/features/screens/index.ts @@ -1,4 +1,4 @@ -export * from './loginScreen'; +export * from './LoginScreen'; export * from './otpScreen'; export * from './setLocationScreen'; export * from './completeProfileScreen'; diff --git a/app/features/screens/liveTrackingScreen/liveTrackingScreen.tsx b/app/features/screens/liveTrackingScreen/liveTrackingScreen.tsx index a31fab9..0e5304b 100644 --- a/app/features/screens/liveTrackingScreen/liveTrackingScreen.tsx +++ b/app/features/screens/liveTrackingScreen/liveTrackingScreen.tsx @@ -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; +type LiveTrackingRouteProp = RouteProp; export const LiveTrackingScreen: React.FC = () => { const { colors } = useAppTheme(); const styles = getStyles(colors); + const navigation = useNavigation(); + const route = useRoute(); + const orderId = route.params?.orderId || 'ORD-123456'; return ( -
{}} /> +
navigation.goBack()} /> 🗺️ Live Map - Delivery partner location tracking + Delivery partner location tracking for {orderId} ● Live + navigation.navigate('OrderDeliveredScreen', { orderId })} + style={{ marginBottom: 16 }} + /> Rahul Sharma ⭐ 4.8 KA-01-AB-1234 • Honda Activa diff --git a/app/features/screens/myOrdersScreen/myOrdersScreen.tsx b/app/features/screens/myOrdersScreen/myOrdersScreen.tsx index 8bb9be8..6a882b4 100644 --- a/app/features/screens/myOrdersScreen/myOrdersScreen.tsx +++ b/app/features/screens/myOrdersScreen/myOrdersScreen.tsx @@ -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, + StackNavigationProp +>; 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(); 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} diff --git a/app/features/screens/onboardingCompleteScreen/onboardingCompleteScreen.tsx b/app/features/screens/onboardingCompleteScreen/onboardingCompleteScreen.tsx index 8046efd..71cdf4f 100644 --- a/app/features/screens/onboardingCompleteScreen/onboardingCompleteScreen.tsx +++ b/app/features/screens/onboardingCompleteScreen/onboardingCompleteScreen.tsx @@ -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(); diff --git a/app/features/screens/orderConfirmedScreen/orderConfirmedScreen.tsx b/app/features/screens/orderConfirmedScreen/orderConfirmedScreen.tsx index 8a1d5e1..43a8e84 100644 --- a/app/features/screens/orderConfirmedScreen/orderConfirmedScreen.tsx +++ b/app/features/screens/orderConfirmedScreen/orderConfirmedScreen.tsx @@ -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; +type OrderConfirmedRouteProp = RouteProp; export const OrderConfirmedScreen: React.FC = () => { const { colors } = useAppTheme(); const styles = getStyles(colors); + const navigation = useNavigation(); + const route = useRoute(); + const orderId = route.params?.orderId || 'ORD-123456'; return ( -
{}} /> +
navigation.navigate('MainTabs')} /> Order Confirmed! @@ -18,12 +27,12 @@ export const OrderConfirmedScreen: React.FC = () => { Your order has been placed successfully - Order ID: ORD-123456 + Order ID: {orderId} Estimated delivery: 25-30 min - {}} /> + navigation.navigate('OrderTrackingScreen', { orderId })} /> ); diff --git a/app/features/screens/orderDeliveredScreen/orderDeliveredScreen.tsx b/app/features/screens/orderDeliveredScreen/orderDeliveredScreen.tsx index 655de4e..21bf53e 100644 --- a/app/features/screens/orderDeliveredScreen/orderDeliveredScreen.tsx +++ b/app/features/screens/orderDeliveredScreen/orderDeliveredScreen.tsx @@ -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; export const OrderDeliveredScreen: React.FC = () => { const { colors } = useAppTheme(); const styles = getStyles(colors); + const navigation = useNavigation(); const [rating, setRating] = useState(0); return ( -
{}} /> +
navigation.navigate('MainTabs')} /> 🎉 Your order has been delivered! @@ -21,7 +27,7 @@ export const OrderDeliveredScreen: React.FC = () => { {}} + onPress={() => navigation.navigate('MainTabs')} disabled={rating === 0} style={{ marginTop: 32 }} /> diff --git a/app/features/screens/orderTrackingScreen/orderTrackingScreen.tsx b/app/features/screens/orderTrackingScreen/orderTrackingScreen.tsx index 5cabb87..3f4a391 100644 --- a/app/features/screens/orderTrackingScreen/orderTrackingScreen.tsx +++ b/app/features/screens/orderTrackingScreen/orderTrackingScreen.tsx @@ -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; +type OrderTrackingRouteProp = RouteProp; 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(); + const route = useRoute(); + const orderId = route.params?.orderId || 'ORD-123456'; return ( -
{}} /> +
navigation.goBack()} /> {MILESTONES.map((milestone, index) => ( @@ -54,9 +63,14 @@ export const OrderTrackingScreen: React.FC = () => { ))} + navigation.navigate('LiveTrackingScreen', { orderId })} + style={{ marginBottom: 16 }} + /> Need help? Contact support for assistance - {}} /> + navigation.navigate('HelpSupportScreen')} /> diff --git a/app/features/screens/otpScreen/otpScreen.tsx b/app/features/screens/otpScreen/otpScreen.tsx index 9e83cdf..f4e27c8 100644 --- a/app/features/screens/otpScreen/otpScreen.tsx +++ b/app/features/screens/otpScreen/otpScreen.tsx @@ -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; diff --git a/app/features/screens/providerDetailsScreen/providerDetailsScreen.styles.ts b/app/features/screens/providerDetailsScreen/providerDetailsScreen.styles.ts index 9e80219..cec6da1 100644 --- a/app/features/screens/providerDetailsScreen/providerDetailsScreen.styles.ts +++ b/app/features/screens/providerDetailsScreen/providerDetailsScreen.styles.ts @@ -1,95 +1,345 @@ -import { StyleSheet } from 'react-native'; +import { StyleSheet, Dimensions, Platform } from 'react-native'; import { typography } from '@theme'; -export const getStyles = (colors: any) => StyleSheet.create({ - container: { - flex: 1, - backgroundColor: colors.background, - }, - content: { - flex: 1, - }, - hero: { - alignItems: 'center', - paddingVertical: 24, - borderBottomWidth: 1, - borderBottomColor: colors.border, - }, - heroImage: { - width: 100, - height: 100, - borderRadius: 50, - backgroundColor: colors.inputBg, - justifyContent: 'center', - alignItems: 'center', - marginBottom: 12, - }, - heroEmoji: { - fontSize: 44, - }, - heroName: { - fontSize: typography.fontSize.xl, - fontWeight: typography.fontWeight.bold, - color: colors.text, - marginBottom: 8, - }, - heroStats: { - flexDirection: 'row', - gap: 16, - }, - heroStat: { - fontSize: typography.fontSize.sm, - color: colors.textSecondary, - }, - categoryRow: { - paddingVertical: 12, - paddingHorizontal: 16, - flexGrow: 0, - }, - categoryTab: { - paddingHorizontal: 20, - paddingVertical: 8, - borderRadius: 20, - borderWidth: 1, - borderColor: colors.border, - marginRight: 8, - }, - categoryTabActive: { - borderColor: colors.primary, - backgroundColor: '#E8F5E9', - }, - categoryTabText: { - fontSize: typography.fontSize.sm, - color: colors.textSecondary, - }, - categoryTabTextActive: { - color: colors.primary, - fontWeight: typography.fontWeight.semibold, - }, - cartStrip: { - flexDirection: 'row', - alignItems: 'center', - justifyContent: 'space-between', - backgroundColor: colors.primary, - paddingHorizontal: 20, - paddingVertical: 14, - borderTopLeftRadius: 16, - borderTopRightRadius: 16, - }, - cartStripText: { - fontSize: typography.fontSize.md, - fontWeight: typography.fontWeight.semibold, - color: '#FFFFFF', - }, - viewCartButton: { - backgroundColor: '#FFFFFF', - paddingHorizontal: 16, - paddingVertical: 8, - borderRadius: 8, - }, - viewCartText: { - fontSize: typography.fontSize.sm, - fontWeight: typography.fontWeight.semibold, - color: colors.primary, - }, -}); +const { width } = Dimensions.get('window'); +const HERO_HEIGHT = 220; + +export const getStyles = (colors: any) => + StyleSheet.create({ + container: { + flex: 1, + backgroundColor: colors.background, + }, + content: { + flex: 1, + }, + contentBody: { + paddingBottom: 32, + }, + + // ---------- Hero ---------- + heroWrap: { + width, + height: HERO_HEIGHT, + backgroundColor: colors.inputBg, + }, + heroImage: { + width: '100%', + height: '100%', + }, + 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, + flex: 1, + marginRight: 12, + }, + ratingBadge: { + flexDirection: 'row', + alignItems: 'center', + backgroundColor: colors.primary, + borderRadius: 8, + paddingHorizontal: 8, + paddingVertical: 5, + }, + ratingBadgeText: { + fontSize: typography.fontSize.sm, + fontWeight: typography.fontWeight.bold, + color: '#FFFFFF', + marginLeft: 3, + }, + cuisineText: { + fontSize: typography.fontSize.sm, + color: colors.textSecondary, + marginTop: 4, + }, + + metaRow: { + flexDirection: 'row', + alignItems: 'center', + marginTop: 12, + }, + metaItem: { + flexDirection: 'row', + alignItems: 'center', + }, + metaIcon: { + fontSize: 13, + marginRight: 4, + }, + metaText: { + fontSize: typography.fontSize.sm, + color: colors.text, + fontWeight: typography.fontWeight.medium, + }, + metaDivider: { + width: 3, + height: 3, + borderRadius: 1.5, + backgroundColor: colors.textSecondary, + marginHorizontal: 10, + opacity: 0.5, + }, + + statusRow: { + flexDirection: 'row', + alignItems: 'center', + marginTop: 10, + }, + statusDot: { + width: 7, + height: 7, + borderRadius: 3.5, + backgroundColor: colors.primary, + marginRight: 6, + }, + statusText: { + fontSize: typography.fontSize.xs, + color: colors.primary, + fontWeight: typography.fontWeight.semibold, + }, + statusTextMuted: { + fontSize: typography.fontSize.xs, + color: colors.textSecondary, + }, + + // ---------- Offers ---------- + offersSection: { + marginTop: 18, + }, + offersScrollContent: { + paddingHorizontal: 20, + }, + offerChip: { + flexDirection: 'row', + alignItems: 'center', + borderWidth: 1, + borderStyle: 'dashed', + borderColor: colors.primary, + backgroundColor: colors.primaryMuted ?? '#E9F7EF', + borderRadius: 10, + paddingVertical: 8, + paddingHorizontal: 12, + marginRight: 10, + }, + offerIcon: { + fontSize: 15, + marginRight: 6, + }, + offerText: { + fontSize: typography.fontSize.xs, + fontWeight: typography.fontWeight.semibold, + color: colors.text, + }, + + // ---------- Divider ---------- + sectionDivider: { + height: 8, + backgroundColor: colors.inputBg, + marginTop: 18, + }, + + // ---------- Menu search ---------- + menuHeaderRow: { + flexDirection: 'row', + alignItems: 'center', + justifyContent: 'space-between', + paddingHorizontal: 20, + paddingTop: 18, + paddingBottom: 4, + }, + menuTitle: { + fontSize: typography.fontSize.lg, + fontWeight: typography.fontWeight.bold, + color: colors.text, + }, + menuCount: { + fontSize: typography.fontSize.sm, + color: colors.textSecondary, + }, + + // ---------- Category tabs ---------- + categoryRow: { + paddingVertical: 14, + paddingHorizontal: 20, + flexGrow: 0, + }, + categoryTab: { + paddingHorizontal: 18, + paddingVertical: 9, + borderRadius: 22, + borderWidth: 1.5, + borderColor: colors.border, + marginRight: 10, + backgroundColor: colors.background, + }, + categoryTabActive: { + borderColor: colors.primary, + backgroundColor: colors.primary, + }, + categoryTabText: { + fontSize: typography.fontSize.sm, + color: colors.textSecondary, + fontWeight: typography.fontWeight.medium, + }, + categoryTabTextActive: { + color: '#FFFFFF', + fontWeight: typography.fontWeight.bold, + }, + + // ---------- Menu list ---------- + menuList: { + paddingHorizontal: 20, + }, + emptyMenuWrap: { + alignItems: 'center', + paddingVertical: 40, + }, + emptyMenuEmoji: { + fontSize: 34, + marginBottom: 8, + }, + emptyMenuText: { + color: colors.textSecondary, + fontSize: typography.fontSize.sm, + }, + + // ---------- Cart strip ---------- + cartStripWrap: { + position: 'absolute', + left: 16, + right: 16, + bottom: Platform.OS === 'ios' ? 28 : 16, + }, + cartStrip: { + flexDirection: 'row', + alignItems: 'center', + justifyContent: 'space-between', + backgroundColor: colors.primary, + paddingHorizontal: 18, + paddingVertical: 14, + borderRadius: 16, + ...Platform.select({ + ios: { + shadowColor: '#000', + shadowOffset: { width: 0, height: 6 }, + shadowOpacity: 0.25, + shadowRadius: 12, + }, + android: { elevation: 8 }, + }), + }, + cartStripLeft: { + flexDirection: 'row', + alignItems: 'center', + }, + cartBagIcon: { + fontSize: 18, + marginRight: 8, + }, + cartStripTextWrap: {}, + cartStripCount: { + fontSize: typography.fontSize.xs, + color: 'rgba(255,255,255,0.85)', + }, + cartStripPrice: { + fontSize: typography.fontSize.md, + fontWeight: typography.fontWeight.bold, + color: '#FFFFFF', + }, + viewCartButton: { + flexDirection: 'row', + alignItems: 'center', + backgroundColor: '#FFFFFF', + paddingHorizontal: 16, + paddingVertical: 9, + borderRadius: 10, + }, + viewCartText: { + fontSize: typography.fontSize.sm, + fontWeight: typography.fontWeight.bold, + color: colors.primary, + marginRight: 4, + }, + viewCartArrow: { + fontSize: 13, + color: colors.primary, + }, + }); diff --git a/app/features/screens/providerDetailsScreen/providerDetailsScreen.tsx b/app/features/screens/providerDetailsScreen/providerDetailsScreen.tsx index 24c55a0..d10cab8 100644 --- a/app/features/screens/providerDetailsScreen/providerDetailsScreen.tsx +++ b/app/features/screens/providerDetailsScreen/providerDetailsScreen.tsx @@ -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; + imageUrl?: string; + onBack: () => void; +} + +const HeroSection: React.FC = ({ + styles, + imageUrl, + onBack, +}) => ( + + {imageUrl ? ( + + + + ) : ( + + 🍽️ + + + )} + + + + + + + + 🔗 + + + 🤍 + + + + +); + +interface InfoCardProps { + styles: ReturnType; + name: string; + rating: number; + deliveryTime: string; + tag: string; +} + +const InfoCard: React.FC = ({ + styles, + name, + rating, + deliveryTime, + tag, +}) => ( + + + + {name} + + + + {rating.toFixed(1)} + + + + {tag} • Multi-cuisine + + + + 🕐 + {deliveryTime} + + + + 📍 + 2.4 km away + + + + + + Open now + • Closes 11:30 PM + + +); + +interface OffersRowProps { + styles: ReturnType; +} + +const OffersRow: React.FC = ({ styles }) => ( + + + {OFFERS.map(offer => ( + + {offer.icon} + {offer.label} + + ))} + + +); + +interface CategoryTabsProps { + styles: ReturnType; + categories: string[]; + activeCategory: string; + onSelect: (category: string) => void; +} + +const CategoryTabs: React.FC = ({ + styles, + categories, + activeCategory, + onSelect, +}) => ( + + {categories.map(cat => { + const isActive = activeCategory === cat; + return ( + onSelect(cat)} + activeOpacity={0.75} + > + + {cat} + + + ); + })} + +); + +interface CartStripProps { + styles: ReturnType; + totalItems: number; + totalPrice: number; + onPress: () => void; +} + +const CartStrip: React.FC = ({ + styles, + totalItems, + totalPrice, + onPress, +}) => ( + + + + 🛍️ + + + {totalItems} item{totalItems === 1 ? '' : 's'} + + ₹{totalPrice} + + + + View Cart + + + + +); + +// --------------------------------------------------------------------------- +// 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(); + const route = useRoute(); + const { providerId, providerName } = route.params; + const cartItems = useAppSelector(state => state.cart.items); + + const [provider, setProvider] = useState(null); const [catalog, setCatalog] = useState([]); - 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,80 +292,85 @@ export const ProviderDetailsScreen: React.FC = () => { 0, ); + const handleAddItem = (item: CatalogItem) => { + dispatch( + addItem({ + providerId: resolvedProviderId, + providerName: resolvedProviderName, + item, + }), + ); + }; + return ( -
{}} /> - - - - 🍕 - - Pizza Planet - - ⭐ 4.5 - 🕐 25 min - 🍕 Italian - + + navigation.goBack()} + /> + + + + + + + + + Menu + {catalog.length} items - - {CATEGORIES.map((cat) => ( - setActiveCategory(cat)} - activeOpacity={0.7} - > - - {cat} - - - ))} - + - {filteredItems.map((item) => ( - - dispatch( - addItem({ - providerId: 'p1', - providerName: 'Pizza Planet', - item, - }), - ) - } - onRemove={() => {}} - /> - ))} + + {filteredItems.length === 0 && ( + + 🍽️ + + No items in this category yet + + + )} + + {filteredItems.map(item => ( + handleAddItem(item)} + onRemove={() => {}} + /> + ))} + {totalItems > 0 && ( - - - {totalItems} Items | ₹{totalPrice} - - - View Cart - - + navigation.navigate('CartScreen')} + /> )} ); diff --git a/app/features/screens/providerListScreen/providerListScreen.tsx b/app/features/screens/providerListScreen/providerListScreen.tsx index b0b314a..0c9f231 100644 --- a/app/features/screens/providerListScreen/providerListScreen.tsx +++ b/app/features/screens/providerListScreen/providerListScreen.tsx @@ -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; +type ProviderListRouteProp = RouteProp; + export const ProviderListScreen: React.FC = () => { const { colors } = useAppTheme(); const styles = getStyles(colors); + const navigation = useNavigation(); + const route = useRoute(); + const category = route.params?.category; const [providers, setProviders] = useState([]); const [activeFilter, setActiveFilter] = useState(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 ( -
{}} /> +
navigation.goBack()} /> {FILTERS.map((filter) => ( { ))} p.tag === category) : providers} keyExtractor={(item) => item.id} renderItem={({ item }) => ( { deliveryTime={item.deliveryTime} tag={item.tag} discountText={item.discountText} + onPress={() => + navigation.navigate('ProviderDetailsScreen', { + providerId: item.id, + providerName: item.name, + }) + } /> )} contentContainerStyle={styles.list} diff --git a/app/features/screens/searchScreen/searchScreen.tsx b/app/features/screens/searchScreen/searchScreen.tsx index 3686f21..53e2ab1 100644 --- a/app/features/screens/searchScreen/searchScreen.tsx +++ b/app/features/screens/searchScreen/searchScreen.tsx @@ -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, + StackNavigationProp +>; export const SearchScreen: React.FC = () => { const { colors } = useAppTheme(); const styles = getStyles(colors); + const navigation = useNavigation(); const [query, setQuery] = useState(''); const [results, setResults] = useState([]); 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={ diff --git a/app/features/screens/setLocationScreen/setLocationScreen.styles.ts b/app/features/screens/setLocationScreen/setLocationScreen.styles.ts index f768f02..3202dca 100644 --- a/app/features/screens/setLocationScreen/setLocationScreen.styles.ts +++ b/app/features/screens/setLocationScreen/setLocationScreen.styles.ts @@ -1,75 +1,127 @@ -import { StyleSheet } from 'react-native'; +import { StyleSheet, Platform, Dimensions } from 'react-native'; import { typography } from '@theme'; -export const getStyles = (colors: any) => StyleSheet.create({ - container: { - flex: 1, - backgroundColor: colors.background, - }, - scrollContainer: { - flexGrow: 1, - paddingTop: 60, - }, - mapPlaceholder: { - height: 240, - backgroundColor: colors.inputBg, - marginHorizontal: 24, - borderRadius: 16, - justifyContent: 'center', - alignItems: 'center', - marginBottom: 24, - borderWidth: 1, - borderColor: colors.border, - }, - mapIcon: { - fontSize: 48, - marginBottom: 8, - }, - mapText: { - fontSize: typography.fontSize.lg, - fontWeight: typography.fontWeight.semibold, - color: colors.textSecondary, - }, - mapSubtext: { - fontSize: typography.fontSize.xs, - color: colors.placeholder, - marginTop: 4, - }, - card: { - backgroundColor: colors.cardBg, - borderRadius: 12, - padding: 16, - marginHorizontal: 24, - marginBottom: 24, - borderWidth: 1, - borderColor: colors.border, - }, - cardTitle: { - fontSize: typography.fontSize.sm, - color: colors.textSecondary, - marginBottom: 4, - }, - cardAddress: { - fontSize: typography.fontSize.md, - fontWeight: typography.fontWeight.semibold, - color: colors.text, - marginBottom: 8, - }, - changeLink: { - alignSelf: 'flex-end', - }, - changeText: { - fontSize: typography.fontSize.sm, - color: colors.primary, - fontWeight: typography.fontWeight.semibold, - }, - manualButton: { - alignItems: 'center', - paddingVertical: 16, - }, - manualButtonText: { - fontSize: typography.fontSize.sm, - color: colors.primary, - fontWeight: typography.fontWeight.semibold, - }, -}); +const { width } = Dimensions.get('window'); + +export const getStyles = (colors: any) => + StyleSheet.create({ + container: { + flex: 1, + backgroundColor: colors.background, + }, + + // ---------- Map ---------- + map: { + flex: 1, + }, + + // ---------- Locate me FAB ---------- + locateButton: { + position: 'absolute', + right: 16, + bottom: Platform.OS === 'ios' ? 310 : 290, + width: 48, + height: 48, + borderRadius: 24, + backgroundColor: '#FFFFFF', + alignItems: 'center', + justifyContent: 'center', + ...Platform.select({ + ios: { + shadowColor: '#000', + shadowOffset: { width: 0, height: 3 }, + shadowOpacity: 0.15, + shadowRadius: 6, + }, + android: { elevation: 5 }, + }), + }, + locateIcon: { + fontSize: 22, + }, + + // ---------- 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: 16, + padding: 16, + marginBottom: 16, + borderWidth: 1, + borderColor: colors.border, + }, + cardHeader: { + flexDirection: 'row', + alignItems: 'center', + marginBottom: 8, + }, + cardIcon: { + fontSize: 18, + marginRight: 8, + }, + cardTitle: { + fontSize: typography.fontSize.sm, + fontWeight: typography.fontWeight.semibold, + color: colors.textSecondary, + }, + cardAddress: { + fontSize: typography.fontSize.md, + fontWeight: typography.fontWeight.semibold, + color: colors.text, + lineHeight: 22, + marginBottom: 8, + }, + changeLink: { + alignSelf: 'flex-end', + }, + changeText: { + fontSize: typography.fontSize.sm, + color: colors.primary, + fontWeight: typography.fontWeight.semibold, + }, + + // ---------- Buttons ---------- + confirmButton: { + borderRadius: 14, + marginBottom: 8, + }, + manualButton: { + alignItems: 'center', + paddingVertical: 14, + }, + manualButtonText: { + fontSize: typography.fontSize.sm, + color: colors.primary, + fontWeight: typography.fontWeight.semibold, + }, + }); diff --git a/app/features/screens/setLocationScreen/setLocationScreen.tsx b/app/features/screens/setLocationScreen/setLocationScreen.tsx index 4922c02..db6d097 100644 --- a/app/features/screens/setLocationScreen/setLocationScreen.tsx +++ b/app/features/screens/setLocationScreen/setLocationScreen.tsx @@ -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; +const DELTA = { latitudeDelta: 0.006, longitudeDelta: 0.006 }; + export const SetLocationScreen: React.FC = () => { const { colors } = useAppTheme(); const styles = getStyles(colors); const navigation = useNavigation(); + const mapRef = useRef(null); + + const [coords, setCoords] = useState(DEFAULT_LOCATION); + const [address, setAddress] = useState('Fetching your location…'); + const [loading, setLoading] = useState(true); + + // ------------------------------------------------------------------ + // Fetch current location on mount + // ------------------------------------------------------------------ + const fetchCurrentLocation = useCallback(async () => { + 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 ( - - - - 🗺️ - Map View - - (react-native-maps integration) - - + + {/* ---------- Map ---------- */} + + + + + {/* ---------- "Locate me" floating button ---------- */} + + 📍 + + + {/* ---------- Bottom sheet ---------- */} + + - Delivery Location - - Koramangala 4th Block, Bengaluru, 560034 - + + 📍 + My Location + + + {loading ? ( + + ) : ( + + {address} + + )} + Change navigation.navigate('CompleteProfileScreen')} - style={{ marginHorizontal: 24 }} + style={styles.confirmButton} /> { > Enter address manually - - + + ); }; diff --git a/app/hooks/index.ts b/app/hooks/index.ts new file mode 100644 index 0000000..d97e760 --- /dev/null +++ b/app/hooks/index.ts @@ -0,0 +1 @@ +export * from './useLoginScreen'; diff --git a/app/hooks/useAppDispatch.ts b/app/hooks/useAppDispatch.ts deleted file mode 100644 index 39f730f..0000000 --- a/app/hooks/useAppDispatch.ts +++ /dev/null @@ -1,5 +0,0 @@ -import { useDispatch, useSelector, TypedUseSelectorHook } from 'react-redux'; -import type { RootState, AppDispatch } from '../store'; - -export const useAppDispatch = () => useDispatch(); -export const useAppSelector: TypedUseSelectorHook = useSelector; diff --git a/app/hooks/useLoginScreen.ts b/app/hooks/useLoginScreen.ts new file mode 100644 index 0000000..5c742fe --- /dev/null +++ b/app/hooks/useLoginScreen.ts @@ -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; + +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(); + const dispatch = useAppDispatch(); + + const [mobileNumber, setMobileNumber] = useState(''); + const [error, setError] = useState(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, + }; +}; diff --git a/app/navigation/appStack.tsx b/app/navigation/appStack.tsx index 9fc5e43..ac4230b 100644 --- a/app/navigation/appStack.tsx +++ b/app/navigation/appStack.tsx @@ -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 | 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(); - -const tabIcons: Record = { - HomeScreen: '🏠', - SearchScreen: '🔍', - MyOrdersScreen: '📦', - OffersScreen: '🏷️', - AccountScreen: '👤', -}; - -const renderTabIcon = (routeName: string, focused: boolean) => ( - - {tabIcons[routeName]} - -); +const Stack = createStackNavigator(); export const AppStack: React.FC = () => { return ( - ({ - headerShown: false, - tabBarIcon: ({ focused }) => renderTabIcon(route.name, focused), - tabBarActiveTintColor: '#05824C', - tabBarInactiveTintColor: '#666666', - tabBarStyle: { - paddingBottom: 8, - paddingTop: 8, - height: 60, - }, - tabBarLabelStyle: { - fontSize: 11, - fontWeight: '500', - }, - })} - > - - - - - - + + + + + + + + + + + + + ); }; diff --git a/app/navigation/mainTabNavigator.tsx b/app/navigation/mainTabNavigator.tsx new file mode 100644 index 0000000..3931ecb --- /dev/null +++ b/app/navigation/mainTabNavigator.tsx @@ -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(); + +const tabIcons: Record = { + HomeScreen: '🏠', + SearchScreen: '🔍', + MyOrdersScreen: '📦', + OffersScreen: '🏷️', + AccountScreen: '👤', +}; + +const renderTabIcon = (routeName: string, focused: boolean) => ( + + {tabIcons[routeName]} + +); + +export const MainTabNavigator: React.FC = () => { + return ( + ({ + headerShown: false, + tabBarIcon: ({ focused }) => renderTabIcon(route.name, focused), + tabBarActiveTintColor: '#05824C', + tabBarInactiveTintColor: '#666666', + tabBarStyle: { + paddingBottom: 8, + paddingTop: 8, + height: 60, + }, + tabBarLabelStyle: { + fontSize: 11, + fontWeight: '500', + }, + })} + > + + + + + + + ); +}; diff --git a/app/services/apiClient.ts b/app/services/apiClient.ts index 5bbe5f4..b47e0af 100644 --- a/app/services/apiClient.ts +++ b/app/services/apiClient.ts @@ -1,18 +1,212 @@ -const delay = (ms: number) => new Promise((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(url: string, mockData: T): Promise { - 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 { + return AsyncStorage.getItem(STORAGE_KEYS.ACCESS_TOKEN); }, - async post(url: string, body: unknown, mockResponse: T): Promise { - await delay(1000); - return mockResponse; + async getRefreshToken(): Promise { + return AsyncStorage.getItem(STORAGE_KEYS.REFRESH_TOKEN); }, - async put(url: string, body: unknown, mockResponse: T): Promise { - await delay(800); - return mockResponse; + async setTokens(accessToken: string, refreshToken: string): Promise { + await AsyncStorage.setItem(STORAGE_KEYS.ACCESS_TOKEN, accessToken); + await AsyncStorage.setItem(STORAGE_KEYS.REFRESH_TOKEN, refreshToken); + }, + + async clearTokens(): Promise { + await AsyncStorage.removeItem(STORAGE_KEYS.ACCESS_TOKEN); + await AsyncStorage.removeItem(STORAGE_KEYS.REFRESH_TOKEN); }, }; + +// ─── Axios Instance ────────────────────────────────────────────────────────── +const axiosInstance: AxiosInstance = axios.create({ + baseURL: BASE_URL, + timeout: 15000, + headers: { + 'Content-Type': 'application/json', + Accept: 'application/json', + }, +}); + +// ─── Request Interceptor — Attach Access Token ────────────────────────────── +axiosInstance.interceptors.request.use( + async (config: InternalAxiosRequestConfig) => { + // Skip attaching token for auth endpoints (login, register, refresh) + const publicPaths = [ + '/auth/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((resolve, reject) => { + failedQueue.push({ + resolve: (token: string) => { + originalRequest.headers.Authorization = `Bearer ${token}`; + resolve(axiosInstance(originalRequest)); + }, + reject, + }); + }); + } + + originalRequest._retry = true; + isRefreshing = true; + + try { + const refreshToken = await tokenManager.getRefreshToken(); + + if (!refreshToken) { + throw new Error('No refresh token available'); + } + + // Call refresh endpoint (uses a fresh axios call to avoid interceptor loop) + const { data } = await axios.post(`${BASE_URL}/auth/refresh-token`, { + refreshToken, + }); + + const { accessToken: newAccessToken, refreshToken: newRefreshToken } = + data; + + await tokenManager.setTokens(newAccessToken, newRefreshToken); + + // Retry all queued requests with the new token + processQueue(null, newAccessToken); + + // Retry the original request + originalRequest.headers.Authorization = `Bearer ${newAccessToken}`; + return axiosInstance(originalRequest); + } catch (refreshError) { + processQueue(refreshError, null); + await tokenManager.clearTokens(); + + // TODO: Navigate to login screen or dispatch a logout action + // e.g. store.dispatch(logout()); + + return Promise.reject(refreshError); + } finally { + isRefreshing = false; + } + } + + // For all other errors, reject as-is + return Promise.reject(error); + }, +); +let storeInstance: Store | null = null; +export const injectStore = (_store: Store | null): void => { + storeInstance = _store; +}; + +// ─── API Methods ───────────────────────────────────────────────────────────── +export const apiClient = { + async get(url: string, config?: AxiosRequestConfig): Promise { + const response = await axiosInstance.get(url, config); + return response.data; + }, + + async post( + url: string, + body?: unknown, + config?: AxiosRequestConfig, + ): Promise { + const response = await axiosInstance.post(url, body, config); + return response.data; + }, + + async put( + url: string, + body?: unknown, + config?: AxiosRequestConfig, + ): Promise { + const response = await axiosInstance.put(url, body, config); + return response.data; + }, + + async patch( + url: string, + body?: unknown, + config?: AxiosRequestConfig, + ): Promise { + const response = await axiosInstance.patch(url, body, config); + return response.data; + }, + + async delete(url: string, config?: AxiosRequestConfig): Promise { + const response = await axiosInstance.delete(url, config); + return response.data; + }, +}; + +// Export the raw instance if needed for advanced usage +export { axiosInstance }; diff --git a/app/services/authService.ts b/app/services/authService.ts deleted file mode 100644 index 8945b3c..0000000 --- a/app/services/authService.ts +++ /dev/null @@ -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 }); - }, -}; diff --git a/app/services/deliveryService.ts b/app/services/deliveryService.ts deleted file mode 100644 index 9c9d8b0..0000000 --- a/app/services/deliveryService.ts +++ /dev/null @@ -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) { - 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); - }, -}; diff --git a/app/services/index.ts b/app/services/index.ts new file mode 100644 index 0000000..ca175f6 --- /dev/null +++ b/app/services/index.ts @@ -0,0 +1,2 @@ +export * from './apiClient'; +export * from './locationServices'; diff --git a/app/services/locationServices.ts b/app/services/locationServices.ts new file mode 100644 index 0000000..9b10f77 --- /dev/null +++ b/app/services/locationServices.ts @@ -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 => { + 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 => { + try { + console.log('get current position'); + console.log('Geolocation:', Geolocation); + console.log('getCurrentPosition:', Geolocation?.getCurrentPosition); + + const location = await new Promise((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 => { + 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 => { + 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 }; + }; diff --git a/app/store/authSlice.ts b/app/store/authSlice.ts deleted file mode 100644 index 915498f..0000000 --- a/app/store/authSlice.ts +++ /dev/null @@ -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) { - state.user = action.payload; - }, - setLocation(state, _action: PayloadAction) { - 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; diff --git a/app/store/cartSlice.ts b/app/store/cartSlice.ts deleted file mode 100644 index c8986d8..0000000 --- a/app/store/cartSlice.ts +++ /dev/null @@ -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) { - 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) { - 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; diff --git a/app/store/commonreducers/auth/index.ts b/app/store/commonreducers/auth/index.ts new file mode 100644 index 0000000..b16cae8 --- /dev/null +++ b/app/store/commonreducers/auth/index.ts @@ -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'; diff --git a/app/store/commonreducers/auth/reducer.ts b/app/store/commonreducers/auth/reducer.ts new file mode 100644 index 0000000..bfcdf99 --- /dev/null +++ b/app/store/commonreducers/auth/reducer.ts @@ -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('auth/setUser'); +export const setLocation = createAction('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; diff --git a/app/store/commonreducers/auth/thunk.ts b/app/store/commonreducers/auth/thunk.ts new file mode 100644 index 0000000..fe654de --- /dev/null +++ b/app/store/commonreducers/auth/thunk.ts @@ -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); + } + }, +); diff --git a/app/store/commonreducers/cart/index.ts b/app/store/commonreducers/cart/index.ts new file mode 100644 index 0000000..07d1dde --- /dev/null +++ b/app/store/commonreducers/cart/index.ts @@ -0,0 +1,10 @@ +export { default as cartReducer } from './reducer'; +export { + addItem, + removeItem, + clearCart, + applyCoupon, + removeCoupon, + selectCartTotal, +} from './reducer'; +export type { CartState } from './reducer'; diff --git a/app/store/commonreducers/cart/reducer.ts b/app/store/commonreducers/cart/reducer.ts new file mode 100644 index 0000000..0d7a2f4 --- /dev/null +++ b/app/store/commonreducers/cart/reducer.ts @@ -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('cart/removeItem'); +export const clearCart = createAction('cart/clearCart'); +export const applyCoupon = createAction('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; diff --git a/app/store/commonreducers/cart/thunk.ts b/app/store/commonreducers/cart/thunk.ts new file mode 100644 index 0000000..c6e601d --- /dev/null +++ b/app/store/commonreducers/cart/thunk.ts @@ -0,0 +1,2 @@ +// Cart thunks — placeholder for future async cart operations +// (e.g., syncing cart with backend, applying server-side coupons) diff --git a/app/store/commonreducers/index.ts b/app/store/commonreducers/index.ts new file mode 100644 index 0000000..32cc23d --- /dev/null +++ b/app/store/commonreducers/index.ts @@ -0,0 +1,3 @@ +export * from './auth'; +export * from './cart'; +export * from './order'; diff --git a/app/store/commonreducers/order/index.ts b/app/store/commonreducers/order/index.ts new file mode 100644 index 0000000..7399033 --- /dev/null +++ b/app/store/commonreducers/order/index.ts @@ -0,0 +1,9 @@ +export { default as orderReducer } from './reducer'; +export { + placeOrder, + updateOrderStatus, + setDeliveryAgent, + updateLiveLocation, + addOrder, +} from './reducer'; +export type { OrderState } from './reducer'; diff --git a/app/store/commonreducers/order/reducer.ts b/app/store/commonreducers/order/reducer.ts new file mode 100644 index 0000000..d370b46 --- /dev/null +++ b/app/store/commonreducers/order/reducer.ts @@ -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/placeOrder'); +export const updateOrderStatus = createAction('order/updateOrderStatus'); +export const setDeliveryAgent = createAction('order/setDeliveryAgent'); +export const updateLiveLocation = createAction('order/updateLiveLocation'); +export const addOrder = createAction('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; diff --git a/app/store/commonreducers/order/thunk.ts b/app/store/commonreducers/order/thunk.ts new file mode 100644 index 0000000..5a3a039 --- /dev/null +++ b/app/store/commonreducers/order/thunk.ts @@ -0,0 +1,2 @@ +// Order thunks — placeholder for future async order operations +// (e.g., fetching order history, real-time status polling) diff --git a/app/store/index.ts b/app/store/index.ts index 4c0bcbd..d052c93 100644 --- a/app/store/index.ts +++ b/app/store/index.ts @@ -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; -export type AppDispatch = typeof store.dispatch; +export * from './commonreducers'; +export * from './store'; +export * from './rootReducer'; +export * from './migration'; diff --git a/app/store/migration.ts b/app/store/migration.ts new file mode 100644 index 0000000..f649179 --- /dev/null +++ b/app/store/migration.ts @@ -0,0 +1,10 @@ +/* eslint-disable @typescript-eslint/explicit-module-boundary-types */ +// Here we avoid typing things as the more migrations we have the more complex types we will have to create + +export const migrations = { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + 1: (state: any) => state, +}; + +const migrationVersions: number[] = Object.keys(migrations).map(k => Number(k)); +export const persistVersion = migrationVersions[migrationVersions.length - 1]; diff --git a/app/store/orderSlice.ts b/app/store/orderSlice.ts deleted file mode 100644 index 17ae137..0000000 --- a/app/store/orderSlice.ts +++ /dev/null @@ -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) { - state.currentOrder = action.payload; - state.orders.unshift(action.payload); - state.trackingSteps = buildTrackingSteps('placed'); - }, - updateOrderStatus(state, action: PayloadAction) { - 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) { - state.deliveryAgent = action.payload; - }, - updateLiveLocation(state, action: PayloadAction) { - state.liveLocation = action.payload; - }, - addOrder(state, action: PayloadAction) { - state.orders.unshift(action.payload); - }, - }, -}); - -export const { placeOrder, updateOrderStatus, setDeliveryAgent, updateLiveLocation, addOrder } = orderSlice.actions; -export default orderSlice.reducer; diff --git a/app/store/rootReducer.ts b/app/store/rootReducer.ts new file mode 100644 index 0000000..2af31bc --- /dev/null +++ b/app/store/rootReducer.ts @@ -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; +export default rootReducer; diff --git a/app/store/store.ts b/app/store/store.ts new file mode 100644 index 0000000..e4065d9 --- /dev/null +++ b/app/store/store.ts @@ -0,0 +1,53 @@ +import { useDispatch, useSelector, TypedUseSelectorHook } from 'react-redux'; +import AsyncStorage from '@react-native-async-storage/async-storage'; +import { + createMigrate, + FLUSH, + PAUSE, + PERSIST, + persistReducer, + persistStore, + PURGE, + REGISTER, + REHYDRATE, +} from 'redux-persist'; +import { ThunkAction } from 'redux-thunk'; +import { Action, configureStore } from '@reduxjs/toolkit'; +import { migrations, persistVersion } from './migration'; +import rootReducer, { RootState } from './rootReducer'; +import { injectStore } from '../services'; + +const persistConfig = { + key: 'root', + version: persistVersion, + storage: AsyncStorage, + blacklist: [], + migrate: createMigrate(migrations, { debug: __DEV__ }), +}; + +const persistedReducer = persistReducer(persistConfig, rootReducer); + +export const store = configureStore({ + reducer: persistedReducer, + devTools: __DEV__, + middleware: getDefaultMiddleware => + getDefaultMiddleware({ + immutableCheck: false, + serializableCheck: { + ignoredActions: [FLUSH, REHYDRATE, PAUSE, PERSIST, PURGE, REGISTER], + }, + }), +}); + +injectStore(store); + +export const persistor = persistStore(store); + +export type AppDispatch = typeof store.dispatch; + +export const useAppDispatch = (): AppDispatch => useDispatch(); +export const useAppSelector: TypedUseSelectorHook = useSelector; + +export type AppThunk = ThunkAction>; + +export default store; diff --git a/package.json b/package.json index 21c9129..320ac4b 100644 --- a/package.json +++ b/package.json @@ -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", diff --git a/yarn.lock b/yarn.lock index 99c2d71..7c945fe 100644 --- a/yarn.lock +++ b/yarn.lock @@ -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"