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 }) => (
- {}} />
+ 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"