feat(app): implement core application structure with authentication, delivery flows, map services, and state management
This commit is contained in:
parent
8f6e162e5e
commit
44f6050e60
@ -1,6 +1,8 @@
|
|||||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
|
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
|
|
||||||
<uses-permission android:name="android.permission.INTERNET" />
|
<uses-permission android:name="android.permission.INTERNET" />
|
||||||
|
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
|
||||||
|
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" />
|
||||||
|
|
||||||
<application
|
<application
|
||||||
android:name=".MainApplication"
|
android:name=".MainApplication"
|
||||||
@ -23,5 +25,8 @@
|
|||||||
<category android:name="android.intent.category.LAUNCHER" />
|
<category android:name="android.intent.category.LAUNCHER" />
|
||||||
</intent-filter>
|
</intent-filter>
|
||||||
</activity>
|
</activity>
|
||||||
|
<meta-data
|
||||||
|
android:name="com.google.android.geo.API_KEY"
|
||||||
|
android:value="AIzaSyDKaKVyQCmSpHMXcMkNCccSXpILpuvwsVM" />
|
||||||
</application>
|
</application>
|
||||||
</manifest>
|
</manifest>
|
||||||
|
|||||||
11
app/App.tsx
11
app/App.tsx
@ -1,22 +1,29 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { StatusBar, useColorScheme } from 'react-native';
|
import { StatusBar, useColorScheme } from 'react-native';
|
||||||
import { SafeAreaProvider } from 'react-native-safe-area-context';
|
import {
|
||||||
|
initialWindowMetrics,
|
||||||
|
SafeAreaProvider,
|
||||||
|
useSafeAreaInsets,
|
||||||
|
} from 'react-native-safe-area-context';
|
||||||
import { NavigationContainer } from '@react-navigation/native';
|
import { NavigationContainer } from '@react-navigation/native';
|
||||||
import { Provider } from 'react-redux';
|
import { Provider } from 'react-redux';
|
||||||
import { store } from './store';
|
import { persistor, store } from './store';
|
||||||
import { RootNavigator } from './navigation/rootNavigator';
|
import { RootNavigator } from './navigation/rootNavigator';
|
||||||
|
import { PersistGate } from 'redux-persist/integration/react';
|
||||||
|
|
||||||
function App() {
|
function App() {
|
||||||
const isDarkMode = useColorScheme() === 'dark';
|
const isDarkMode = useColorScheme() === 'dark';
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Provider store={store}>
|
<Provider store={store}>
|
||||||
|
<PersistGate loading={null} persistor={persistor}>
|
||||||
<SafeAreaProvider>
|
<SafeAreaProvider>
|
||||||
<StatusBar barStyle={isDarkMode ? 'light-content' : 'dark-content'} />
|
<StatusBar barStyle={isDarkMode ? 'light-content' : 'dark-content'} />
|
||||||
<NavigationContainer>
|
<NavigationContainer>
|
||||||
<RootNavigator />
|
<RootNavigator />
|
||||||
</NavigationContainer>
|
</NavigationContainer>
|
||||||
</SafeAreaProvider>
|
</SafeAreaProvider>
|
||||||
|
</PersistGate>
|
||||||
</Provider>
|
</Provider>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
42
app/api/authApi.ts
Normal file
42
app/api/authApi.ts
Normal file
@ -0,0 +1,42 @@
|
|||||||
|
import { apiClient } from '../services/apiClient';
|
||||||
|
import { User } from '../interfaces';
|
||||||
|
|
||||||
|
// ─── Types ───────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
export interface LoginResponse {
|
||||||
|
success: boolean;
|
||||||
|
message: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface VerifyOtpResponse {
|
||||||
|
success: boolean;
|
||||||
|
user: User;
|
||||||
|
accessToken: string;
|
||||||
|
refreshToken: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface UpdateProfileResponse {
|
||||||
|
success: boolean;
|
||||||
|
user: User;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface LogoutResponse {
|
||||||
|
success: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ─── Endpoints ───────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
export const loginApi = (mobileNumber: string) =>
|
||||||
|
apiClient.post<LoginResponse>('/auth/login', { mobileNumber });
|
||||||
|
|
||||||
|
export const verifyOtpApi = (mobileNumber: string, otp: string) =>
|
||||||
|
apiClient.post<VerifyOtpResponse>('/auth/verify-otp', { mobileNumber, otp });
|
||||||
|
|
||||||
|
export const updateProfileApi = (data: {
|
||||||
|
name: string;
|
||||||
|
email: string;
|
||||||
|
locationLabels: string[];
|
||||||
|
}) => apiClient.put<UpdateProfileResponse>('/auth/profile', data);
|
||||||
|
|
||||||
|
export const logoutApi = () =>
|
||||||
|
apiClient.post<LogoutResponse>('/auth/logout', {});
|
||||||
29
app/api/deliveryApi.ts
Normal file
29
app/api/deliveryApi.ts
Normal file
@ -0,0 +1,29 @@
|
|||||||
|
import { apiClient } from '../services/apiClient';
|
||||||
|
import { Provider, CatalogItem, Order, DeliveryAgent, Location } from '../interfaces';
|
||||||
|
|
||||||
|
// ─── Types ───────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
export interface PlaceOrderResponse {
|
||||||
|
success: boolean;
|
||||||
|
order: Order;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ─── Endpoints ───────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
export const getProvidersApi = () =>
|
||||||
|
apiClient.get<Provider[]>('/providers');
|
||||||
|
|
||||||
|
export const getProviderCatalogApi = (providerId: string) =>
|
||||||
|
apiClient.get<CatalogItem[]>(`/providers/${providerId}/catalog`);
|
||||||
|
|
||||||
|
export const searchProvidersApi = (query: string) =>
|
||||||
|
apiClient.get<Provider[]>(`/search?q=${query}`);
|
||||||
|
|
||||||
|
export const placeOrderApi = (orderData: Partial<Order>) =>
|
||||||
|
apiClient.post<PlaceOrderResponse>('/orders', orderData);
|
||||||
|
|
||||||
|
export const getDeliveryAgentApi = () =>
|
||||||
|
apiClient.get<DeliveryAgent>('/delivery/agent');
|
||||||
|
|
||||||
|
export const getLiveLocationApi = () =>
|
||||||
|
apiClient.get<Location>('/delivery/live-location');
|
||||||
2
app/api/index.ts
Normal file
2
app/api/index.ts
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
export * from './authApi';
|
||||||
|
export * from './deliveryApi';
|
||||||
@ -1,9 +1,15 @@
|
|||||||
import { TextInputProps } from 'react-native';
|
import { TextInputProps, ViewStyle, StyleProp } from 'react-native';
|
||||||
|
|
||||||
export interface CustomInputProps extends TextInputProps {
|
export interface CustomInputProps extends TextInputProps {
|
||||||
label: string;
|
label?: string;
|
||||||
isPassword?: boolean;
|
isPassword?: boolean;
|
||||||
rightActionText?: string;
|
rightActionText?: string;
|
||||||
onRightActionPress?: () => void;
|
onRightActionPress?: () => void;
|
||||||
error?: string;
|
error?: string;
|
||||||
|
style?: StyleProp<ViewStyle>;
|
||||||
|
/**
|
||||||
|
* Optional element rendered at the start of the input, before the
|
||||||
|
* TextInput itself — e.g. a country-code chip ("+91") or an icon.
|
||||||
|
*/
|
||||||
|
leftElement?: React.ReactNode;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,7 +1,8 @@
|
|||||||
import { StyleSheet } from 'react-native';
|
import { StyleSheet } from 'react-native';
|
||||||
import { typography } from '@theme';
|
import { typography } from '@theme';
|
||||||
|
|
||||||
export const getStyles = (colors: any) => StyleSheet.create({
|
export const getStyles = (colors: any) =>
|
||||||
|
StyleSheet.create({
|
||||||
container: {
|
container: {
|
||||||
marginBottom: 20,
|
marginBottom: 20,
|
||||||
width: '100%',
|
width: '100%',
|
||||||
@ -32,6 +33,12 @@ export const getStyles = (colors: any) => StyleSheet.create({
|
|||||||
paddingHorizontal: 16,
|
paddingHorizontal: 16,
|
||||||
height: 56,
|
height: 56,
|
||||||
},
|
},
|
||||||
|
leftElementWrap: {
|
||||||
|
marginRight: 10,
|
||||||
|
paddingRight: 10,
|
||||||
|
borderRightWidth: 1,
|
||||||
|
borderRightColor: colors.border,
|
||||||
|
},
|
||||||
input: {
|
input: {
|
||||||
flex: 1,
|
flex: 1,
|
||||||
height: '100%',
|
height: '100%',
|
||||||
@ -46,4 +53,4 @@ export const getStyles = (colors: any) => StyleSheet.create({
|
|||||||
marginTop: 4,
|
marginTop: 4,
|
||||||
marginLeft: 4,
|
marginLeft: 4,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|||||||
@ -1,7 +1,7 @@
|
|||||||
import React, { useState } from 'react';
|
import React, { useState } from 'react';
|
||||||
import { View, Text, TextInput, TouchableOpacity } from 'react-native';
|
import { View, Text, TextInput, TouchableOpacity } from 'react-native';
|
||||||
import { CustomInputProps } from './customInput.props';
|
import { CustomInputProps } from './CustomInput.props';
|
||||||
import { getStyles } from './customInput.styles';
|
import { getStyles } from './CustomInput.styles';
|
||||||
import { useAppTheme } from '@theme';
|
import { useAppTheme } from '@theme';
|
||||||
|
|
||||||
export const CustomInput: React.FC<CustomInputProps> = ({
|
export const CustomInput: React.FC<CustomInputProps> = ({
|
||||||
@ -12,6 +12,7 @@ export const CustomInput: React.FC<CustomInputProps> = ({
|
|||||||
error,
|
error,
|
||||||
secureTextEntry,
|
secureTextEntry,
|
||||||
style,
|
style,
|
||||||
|
leftElement,
|
||||||
...rest
|
...rest
|
||||||
}) => {
|
}) => {
|
||||||
const { colors } = useAppTheme();
|
const { colors } = useAppTheme();
|
||||||
@ -25,7 +26,16 @@ export const CustomInput: React.FC<CustomInputProps> = ({
|
|||||||
<Text style={styles.label}>{label}</Text>
|
<Text style={styles.label}>{label}</Text>
|
||||||
</View>
|
</View>
|
||||||
)}
|
)}
|
||||||
<View style={[styles.inputContainer, error ? { borderColor: colors.error } : null, style]}>
|
<View
|
||||||
|
style={[
|
||||||
|
styles.inputContainer,
|
||||||
|
error ? { borderColor: colors.error } : null,
|
||||||
|
style,
|
||||||
|
]}
|
||||||
|
>
|
||||||
|
{leftElement && (
|
||||||
|
<View style={styles.leftElementWrap}>{leftElement}</View>
|
||||||
|
)}
|
||||||
<TextInput
|
<TextInput
|
||||||
style={styles.input}
|
style={styles.input}
|
||||||
placeholderTextColor={colors.placeholder}
|
placeholderTextColor={colors.placeholder}
|
||||||
@ -36,8 +46,13 @@ export const CustomInput: React.FC<CustomInputProps> = ({
|
|||||||
/>
|
/>
|
||||||
<View style={{ flexDirection: 'row', alignItems: 'center', gap: 12 }}>
|
<View style={{ flexDirection: 'row', alignItems: 'center', gap: 12 }}>
|
||||||
{isPassword && (
|
{isPassword && (
|
||||||
<TouchableOpacity onPress={() => setIsSecure(!isSecure)} activeOpacity={0.7}>
|
<TouchableOpacity
|
||||||
<Text style={styles.rightAction}>{isSecure ? 'Show' : 'Hide'}</Text>
|
onPress={() => setIsSecure(!isSecure)}
|
||||||
|
activeOpacity={0.7}
|
||||||
|
>
|
||||||
|
<Text style={styles.rightAction}>
|
||||||
|
{isSecure ? 'Show' : 'Hide'}
|
||||||
|
</Text>
|
||||||
</TouchableOpacity>
|
</TouchableOpacity>
|
||||||
)}
|
)}
|
||||||
{rightActionText && onRightActionPress && (
|
{rightActionText && onRightActionPress && (
|
||||||
|
|||||||
@ -1,2 +1,2 @@
|
|||||||
export * from './customInput';
|
export * from './CustomInput';
|
||||||
export * from './customInput.props';
|
export * from './CustomInput.props';
|
||||||
|
|||||||
@ -1,7 +1,7 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { TouchableOpacity, Text, ActivityIndicator } from 'react-native';
|
import { TouchableOpacity, Text, ActivityIndicator } from 'react-native';
|
||||||
import { PrimaryButtonProps } from './primaryButton.props';
|
import { PrimaryButtonProps } from './PrimaryButton.props';
|
||||||
import { styles } from './primaryButton.styles';
|
import { styles } from './PrimaryButton.styles';
|
||||||
import { colors } from '@theme';
|
import { colors } from '@theme';
|
||||||
|
|
||||||
export const PrimaryButton: React.FC<PrimaryButtonProps> = ({
|
export const PrimaryButton: React.FC<PrimaryButtonProps> = ({
|
||||||
|
|||||||
@ -1,2 +1,2 @@
|
|||||||
export * from './primaryButton';
|
export * from './PrimaryButton';
|
||||||
export * from './primaryButton.props';
|
export * from './PrimaryButton.props';
|
||||||
|
|||||||
@ -1,7 +1,7 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { TouchableOpacity, Text } from 'react-native';
|
import { TouchableOpacity, Text } from 'react-native';
|
||||||
import { SocialButtonProps } from './socialButton.props';
|
import { SocialButtonProps } from './socialButton.props';
|
||||||
import { getStyles } from './socialButton.styles';
|
import { getStyles } from './SocialButton.styles';
|
||||||
import { useAppTheme } from '@theme';
|
import { useAppTheme } from '@theme';
|
||||||
import Svg, { Path } from 'react-native-svg';
|
import Svg, { Path } from 'react-native-svg';
|
||||||
|
|
||||||
@ -26,7 +26,7 @@ const GoogleIcon = ({ style }: { style: any }) => (
|
|||||||
</Svg>
|
</Svg>
|
||||||
);
|
);
|
||||||
|
|
||||||
const AppleIcon = ({ style, color }: { style: any, color: string }) => (
|
const AppleIcon = ({ style, color }: { style: any; color: string }) => (
|
||||||
<Svg width={24} height={24} viewBox="0 0 24 24" style={style}>
|
<Svg width={24} height={24} viewBox="0 0 24 24" style={style}>
|
||||||
<Path
|
<Path
|
||||||
fill={color}
|
fill={color}
|
||||||
|
|||||||
@ -1,2 +1,2 @@
|
|||||||
export * from './socialButton';
|
export * from './SocialButton';
|
||||||
export * from './socialButton.props';
|
export * from './SocialButton.props';
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
export * from './customInput';
|
export * from './CustomInput';
|
||||||
export * from './primaryButton';
|
export * from './PrimaryButton';
|
||||||
export * from './socialButton';
|
export * from './SocialButton';
|
||||||
export * from './header';
|
export * from './header';
|
||||||
export * from './searchBar';
|
export * from './searchBar';
|
||||||
export * from './providerCard';
|
export * from './providerCard';
|
||||||
|
|||||||
@ -1,9 +1,9 @@
|
|||||||
export interface ProviderCardProps {
|
export interface ProviderCardProps {
|
||||||
imageUrl: string;
|
imageUrl?: string;
|
||||||
name: string;
|
name: string;
|
||||||
rating: number;
|
rating: number;
|
||||||
deliveryTime: string;
|
deliveryTime: string;
|
||||||
tag: string;
|
tag: string;
|
||||||
discountText?: string;
|
discountText?: string;
|
||||||
onPress?: () => void;
|
onPress: () => void;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,69 +1,143 @@
|
|||||||
import { StyleSheet } from 'react-native';
|
import { StyleSheet, Platform } from 'react-native';
|
||||||
import { typography } from '@theme';
|
import { typography } from '@theme';
|
||||||
|
|
||||||
export const getStyles = (colors: any) => StyleSheet.create({
|
export const getStyles = (colors: any) =>
|
||||||
|
StyleSheet.create({
|
||||||
container: {
|
container: {
|
||||||
flexDirection: 'row',
|
|
||||||
backgroundColor: colors.cardBg,
|
backgroundColor: colors.cardBg,
|
||||||
borderRadius: 12,
|
borderRadius: 18,
|
||||||
padding: 12,
|
|
||||||
marginHorizontal: 16,
|
marginHorizontal: 16,
|
||||||
marginVertical: 6,
|
marginVertical: 8,
|
||||||
borderWidth: 1,
|
overflow: 'hidden',
|
||||||
borderColor: colors.border,
|
...Platform.select({
|
||||||
|
ios: {
|
||||||
|
shadowColor: '#000',
|
||||||
|
shadowOffset: { width: 0, height: 3 },
|
||||||
|
shadowOpacity: 0.06,
|
||||||
|
shadowRadius: 10,
|
||||||
|
},
|
||||||
|
android: { elevation: 2 },
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
|
||||||
|
// ---------- Image ----------
|
||||||
|
imageWrap: {
|
||||||
|
width: '100%',
|
||||||
|
height: 150,
|
||||||
|
position: 'relative',
|
||||||
|
backgroundColor: colors.border,
|
||||||
},
|
},
|
||||||
image: {
|
image: {
|
||||||
width: 80,
|
width: '100%',
|
||||||
height: 80,
|
height: '100%',
|
||||||
borderRadius: 8,
|
|
||||||
backgroundColor: colors.border,
|
|
||||||
justifyContent: 'center',
|
|
||||||
alignItems: 'center',
|
|
||||||
},
|
},
|
||||||
imagePlaceholder: {
|
imagePlaceholder: {
|
||||||
fontSize: 32,
|
width: '100%',
|
||||||
},
|
height: '100%',
|
||||||
info: {
|
alignItems: 'center',
|
||||||
flex: 1,
|
|
||||||
marginLeft: 12,
|
|
||||||
justifyContent: 'center',
|
justifyContent: 'center',
|
||||||
|
backgroundColor: colors.border,
|
||||||
|
},
|
||||||
|
imagePlaceholderEmoji: {
|
||||||
|
fontSize: 34,
|
||||||
|
},
|
||||||
|
|
||||||
|
discountRibbon: {
|
||||||
|
position: 'absolute',
|
||||||
|
top: 12,
|
||||||
|
left: 0,
|
||||||
|
backgroundColor: colors.primary,
|
||||||
|
paddingVertical: 4,
|
||||||
|
paddingHorizontal: 10,
|
||||||
|
borderTopRightRadius: 8,
|
||||||
|
borderBottomRightRadius: 8,
|
||||||
|
},
|
||||||
|
discountRibbonText: {
|
||||||
|
color: '#FFFFFF',
|
||||||
|
fontSize: typography.fontSize.xs,
|
||||||
|
fontWeight: typography.fontWeight.bold,
|
||||||
|
},
|
||||||
|
|
||||||
|
favoriteButton: {
|
||||||
|
position: 'absolute',
|
||||||
|
top: 10,
|
||||||
|
right: 10,
|
||||||
|
width: 32,
|
||||||
|
height: 32,
|
||||||
|
borderRadius: 16,
|
||||||
|
backgroundColor: 'rgba(255,255,255,0.92)',
|
||||||
|
alignItems: 'center',
|
||||||
|
justifyContent: 'center',
|
||||||
|
},
|
||||||
|
favoriteIcon: {
|
||||||
|
fontSize: 15,
|
||||||
|
},
|
||||||
|
|
||||||
|
// ---------- Body ----------
|
||||||
|
body: {
|
||||||
|
padding: 14,
|
||||||
|
},
|
||||||
|
topRow: {
|
||||||
|
flexDirection: 'row',
|
||||||
|
justifyContent: 'space-between',
|
||||||
|
alignItems: 'flex-start',
|
||||||
},
|
},
|
||||||
name: {
|
name: {
|
||||||
fontSize: typography.fontSize.md,
|
fontSize: typography.fontSize.md,
|
||||||
fontWeight: typography.fontWeight.semibold,
|
fontWeight: typography.fontWeight.bold,
|
||||||
color: colors.text,
|
color: colors.text,
|
||||||
marginBottom: 4,
|
flex: 1,
|
||||||
},
|
|
||||||
row: {
|
|
||||||
flexDirection: 'row',
|
|
||||||
alignItems: 'center',
|
|
||||||
marginBottom: 2,
|
|
||||||
},
|
|
||||||
rating: {
|
|
||||||
fontSize: typography.fontSize.sm,
|
|
||||||
color: colors.textSecondary,
|
|
||||||
marginRight: 8,
|
marginRight: 8,
|
||||||
},
|
},
|
||||||
deliveryTime: {
|
ratingPill: {
|
||||||
|
flexDirection: 'row',
|
||||||
|
alignItems: 'center',
|
||||||
|
backgroundColor: colors.primaryMuted ?? '#E9F7EF',
|
||||||
|
borderRadius: 8,
|
||||||
|
paddingHorizontal: 6,
|
||||||
|
paddingVertical: 3,
|
||||||
|
},
|
||||||
|
ratingStar: {
|
||||||
|
fontSize: 11,
|
||||||
|
marginRight: 3,
|
||||||
|
},
|
||||||
|
ratingText: {
|
||||||
|
fontSize: typography.fontSize.xs,
|
||||||
|
fontWeight: typography.fontWeight.bold,
|
||||||
|
color: colors.primary,
|
||||||
|
},
|
||||||
|
|
||||||
|
metaRow: {
|
||||||
|
flexDirection: 'row',
|
||||||
|
alignItems: 'center',
|
||||||
|
marginTop: 8,
|
||||||
|
},
|
||||||
|
metaText: {
|
||||||
fontSize: typography.fontSize.sm,
|
fontSize: typography.fontSize.sm,
|
||||||
color: colors.textSecondary,
|
color: colors.textSecondary,
|
||||||
},
|
},
|
||||||
tag: {
|
metaDivider: {
|
||||||
|
width: 3,
|
||||||
|
height: 3,
|
||||||
|
borderRadius: 1.5,
|
||||||
|
backgroundColor: colors.textSecondary,
|
||||||
|
marginHorizontal: 8,
|
||||||
|
opacity: 0.5,
|
||||||
|
},
|
||||||
|
|
||||||
|
tagPill: {
|
||||||
|
alignSelf: 'flex-start',
|
||||||
|
backgroundColor: colors.background,
|
||||||
|
borderRadius: 6,
|
||||||
|
paddingHorizontal: 8,
|
||||||
|
paddingVertical: 3,
|
||||||
|
marginTop: 10,
|
||||||
|
borderWidth: 1,
|
||||||
|
borderColor: colors.border,
|
||||||
|
},
|
||||||
|
tagPillText: {
|
||||||
fontSize: typography.fontSize.xs,
|
fontSize: typography.fontSize.xs,
|
||||||
color: colors.primary,
|
color: colors.textSecondary,
|
||||||
fontWeight: typography.fontWeight.medium,
|
fontWeight: typography.fontWeight.medium,
|
||||||
},
|
},
|
||||||
discountBadge: {
|
});
|
||||||
alignSelf: 'flex-start',
|
|
||||||
backgroundColor: '#FFE7E7',
|
|
||||||
paddingHorizontal: 8,
|
|
||||||
paddingVertical: 2,
|
|
||||||
borderRadius: 4,
|
|
||||||
marginTop: 4,
|
|
||||||
},
|
|
||||||
discountText: {
|
|
||||||
fontSize: typography.fontSize.xs,
|
|
||||||
color: '#D32F2F',
|
|
||||||
fontWeight: typography.fontWeight.semibold,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|||||||
@ -1,5 +1,5 @@
|
|||||||
import React from 'react';
|
import React, { useState } from 'react';
|
||||||
import { View, Text, TouchableOpacity } from 'react-native';
|
import { View, Text, Image, TouchableOpacity } from 'react-native';
|
||||||
import { ProviderCardProps } from './providerCard.props';
|
import { ProviderCardProps } from './providerCard.props';
|
||||||
import { getStyles } from './providerCard.styles';
|
import { getStyles } from './providerCard.styles';
|
||||||
import { useAppTheme } from '@theme';
|
import { useAppTheme } from '@theme';
|
||||||
@ -15,28 +15,62 @@ export const ProviderCard: React.FC<ProviderCardProps> = ({
|
|||||||
}) => {
|
}) => {
|
||||||
const { colors } = useAppTheme();
|
const { colors } = useAppTheme();
|
||||||
const styles = getStyles(colors);
|
const styles = getStyles(colors);
|
||||||
|
const [isFavorite, setIsFavorite] = useState(false);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<TouchableOpacity
|
<TouchableOpacity
|
||||||
style={styles.container}
|
style={styles.container}
|
||||||
onPress={onPress}
|
onPress={onPress}
|
||||||
activeOpacity={0.8}
|
activeOpacity={0.85}
|
||||||
>
|
>
|
||||||
<View style={styles.image}>
|
<View style={styles.imageWrap}>
|
||||||
<Text style={styles.imagePlaceholder}>{imageUrl || '🍽️'}</Text>
|
{imageUrl ? (
|
||||||
</View>
|
<Image
|
||||||
<View style={styles.info}>
|
source={{ uri: imageUrl }}
|
||||||
<Text style={styles.name} numberOfLines={1}>{name}</Text>
|
style={styles.image}
|
||||||
<View style={styles.row}>
|
resizeMode="cover"
|
||||||
<Text style={styles.rating}>⭐ {rating.toFixed(1)}</Text>
|
/>
|
||||||
<Text style={styles.deliveryTime}>🕐 {deliveryTime}</Text>
|
) : (
|
||||||
</View>
|
<View style={styles.imagePlaceholder}>
|
||||||
<Text style={styles.tag}>{tag}</Text>
|
<Text style={styles.imagePlaceholderEmoji}>🍽️</Text>
|
||||||
{discountText && (
|
|
||||||
<View style={styles.discountBadge}>
|
|
||||||
<Text style={styles.discountText}>{discountText}</Text>
|
|
||||||
</View>
|
</View>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
{!!discountText && (
|
||||||
|
<View style={styles.discountRibbon}>
|
||||||
|
<Text style={styles.discountRibbonText}>{discountText}</Text>
|
||||||
|
</View>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<TouchableOpacity
|
||||||
|
style={styles.favoriteButton}
|
||||||
|
activeOpacity={0.7}
|
||||||
|
onPress={() => setIsFavorite(prev => !prev)}
|
||||||
|
>
|
||||||
|
<Text style={styles.favoriteIcon}>{isFavorite ? '❤️' : '🤍'}</Text>
|
||||||
|
</TouchableOpacity>
|
||||||
|
</View>
|
||||||
|
|
||||||
|
<View style={styles.body}>
|
||||||
|
<View style={styles.topRow}>
|
||||||
|
<Text style={styles.name} numberOfLines={1}>
|
||||||
|
{name}
|
||||||
|
</Text>
|
||||||
|
<View style={styles.ratingPill}>
|
||||||
|
<Text style={styles.ratingStar}>⭐</Text>
|
||||||
|
<Text style={styles.ratingText}>{rating.toFixed(1)}</Text>
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
|
||||||
|
<View style={styles.metaRow}>
|
||||||
|
<Text style={styles.metaText}>🕐 {deliveryTime}</Text>
|
||||||
|
<View style={styles.metaDivider} />
|
||||||
|
<Text style={styles.metaText}>{tag}</Text>
|
||||||
|
</View>
|
||||||
|
|
||||||
|
<View style={styles.tagPill}>
|
||||||
|
<Text style={styles.tagPillText}>Free delivery over ₹199</Text>
|
||||||
|
</View>
|
||||||
</View>
|
</View>
|
||||||
</TouchableOpacity>
|
</TouchableOpacity>
|
||||||
);
|
);
|
||||||
|
|||||||
@ -1,20 +1,89 @@
|
|||||||
import { StyleSheet } from 'react-native';
|
import { StyleSheet, Dimensions, Platform } from 'react-native';
|
||||||
import { typography } from '@theme';
|
import { typography } from '@theme';
|
||||||
|
|
||||||
export const getStyles = (colors: any) => StyleSheet.create({
|
const { width } = Dimensions.get('window');
|
||||||
|
|
||||||
|
export const getStyles = (colors: any) =>
|
||||||
|
StyleSheet.create({
|
||||||
container: {
|
container: {
|
||||||
flex: 1,
|
flex: 1,
|
||||||
backgroundColor: colors.background,
|
backgroundColor: colors.primary,
|
||||||
paddingHorizontal: 24,
|
|
||||||
},
|
},
|
||||||
scrollContainer: {
|
scrollContainer: {
|
||||||
flexGrow: 1,
|
flexGrow: 1,
|
||||||
|
},
|
||||||
|
|
||||||
|
// ---------- Hero ----------
|
||||||
|
hero: {
|
||||||
|
backgroundColor: colors.primary,
|
||||||
|
paddingTop: Platform.OS === 'ios' ? 70 : 48,
|
||||||
|
paddingBottom: 56,
|
||||||
|
alignItems: 'center',
|
||||||
|
overflow: 'hidden',
|
||||||
|
},
|
||||||
|
heroDecoCircleLg: {
|
||||||
|
position: 'absolute',
|
||||||
|
width: 220,
|
||||||
|
height: 220,
|
||||||
|
borderRadius: 110,
|
||||||
|
backgroundColor: 'rgba(255,255,255,0.06)',
|
||||||
|
top: -100,
|
||||||
|
right: -60,
|
||||||
|
},
|
||||||
|
heroDecoCircleSm: {
|
||||||
|
position: 'absolute',
|
||||||
|
width: 120,
|
||||||
|
height: 120,
|
||||||
|
borderRadius: 60,
|
||||||
|
backgroundColor: 'rgba(255,255,255,0.08)',
|
||||||
|
top: 30,
|
||||||
|
left: -50,
|
||||||
|
},
|
||||||
|
logoBadge: {
|
||||||
|
width: 68,
|
||||||
|
height: 68,
|
||||||
|
borderRadius: 20,
|
||||||
|
backgroundColor: 'rgba(255,255,255,0.95)',
|
||||||
|
alignItems: 'center',
|
||||||
justifyContent: 'center',
|
justifyContent: 'center',
|
||||||
|
marginBottom: 16,
|
||||||
|
...Platform.select({
|
||||||
|
ios: {
|
||||||
|
shadowColor: '#000',
|
||||||
|
shadowOffset: { width: 0, height: 4 },
|
||||||
|
shadowOpacity: 0.15,
|
||||||
|
shadowRadius: 10,
|
||||||
|
},
|
||||||
|
android: { elevation: 4 },
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
logoEmoji: {
|
||||||
|
fontSize: 32,
|
||||||
|
},
|
||||||
|
brandName: {
|
||||||
|
fontSize: typography.fontSize.xl,
|
||||||
|
fontWeight: typography.fontWeight.bold,
|
||||||
|
color: '#FFFFFF',
|
||||||
|
marginBottom: 4,
|
||||||
|
},
|
||||||
|
brandTagline: {
|
||||||
|
fontSize: typography.fontSize.sm,
|
||||||
|
color: 'rgba(255,255,255,0.85)',
|
||||||
|
},
|
||||||
|
|
||||||
|
// ---------- Form card ----------
|
||||||
|
formCard: {
|
||||||
|
flex: 1,
|
||||||
|
backgroundColor: colors.background,
|
||||||
|
borderTopLeftRadius: 32,
|
||||||
|
borderTopRightRadius: 32,
|
||||||
|
marginTop: -28,
|
||||||
|
paddingTop: 32,
|
||||||
|
paddingHorizontal: 24,
|
||||||
paddingBottom: 24,
|
paddingBottom: 24,
|
||||||
},
|
},
|
||||||
headerContainer: {
|
headerContainer: {
|
||||||
marginTop: 40,
|
marginBottom: 28,
|
||||||
marginBottom: 32,
|
|
||||||
},
|
},
|
||||||
title: {
|
title: {
|
||||||
fontSize: typography.fontSize.xxl,
|
fontSize: typography.fontSize.xxl,
|
||||||
@ -29,4 +98,45 @@ export const getStyles = (colors: any) => StyleSheet.create({
|
|||||||
formContainer: {
|
formContainer: {
|
||||||
width: '100%',
|
width: '100%',
|
||||||
},
|
},
|
||||||
});
|
|
||||||
|
// Phone prefix chip rendered via CustomInput's leftElement
|
||||||
|
prefixChip: {
|
||||||
|
flexDirection: 'row',
|
||||||
|
alignItems: 'center',
|
||||||
|
},
|
||||||
|
prefixFlag: {
|
||||||
|
fontSize: 16,
|
||||||
|
marginRight: 6,
|
||||||
|
},
|
||||||
|
prefixText: {
|
||||||
|
fontSize: typography.fontSize.md,
|
||||||
|
fontWeight: typography.fontWeight.semibold,
|
||||||
|
color: colors.text,
|
||||||
|
},
|
||||||
|
|
||||||
|
helperText: {
|
||||||
|
fontSize: typography.fontSize.xs,
|
||||||
|
color: colors.textSecondary,
|
||||||
|
marginTop: -12,
|
||||||
|
marginBottom: 20,
|
||||||
|
marginLeft: 4,
|
||||||
|
},
|
||||||
|
|
||||||
|
// ---------- Footer ----------
|
||||||
|
footerSpacer: {
|
||||||
|
flex: 1,
|
||||||
|
minHeight: 24,
|
||||||
|
},
|
||||||
|
termsText: {
|
||||||
|
fontSize: typography.fontSize.xs,
|
||||||
|
color: colors.textSecondary,
|
||||||
|
textAlign: 'center',
|
||||||
|
lineHeight: 18,
|
||||||
|
marginTop: 20,
|
||||||
|
paddingHorizontal: 8,
|
||||||
|
},
|
||||||
|
termsLink: {
|
||||||
|
color: colors.primary,
|
||||||
|
fontWeight: typography.fontWeight.semibold,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|||||||
@ -1,4 +1,4 @@
|
|||||||
import React, { useState } from 'react';
|
import React from 'react';
|
||||||
import {
|
import {
|
||||||
View,
|
View,
|
||||||
Text,
|
Text,
|
||||||
@ -6,39 +6,17 @@ import {
|
|||||||
Platform,
|
Platform,
|
||||||
ScrollView,
|
ScrollView,
|
||||||
} from 'react-native';
|
} from 'react-native';
|
||||||
import { useNavigation } from '@react-navigation/native';
|
|
||||||
import { StackNavigationProp } from '@react-navigation/stack';
|
|
||||||
import { getStyles } from './loginScreen.styles';
|
import { getStyles } from './loginScreen.styles';
|
||||||
import { CustomInput, PrimaryButton } from '@components';
|
import { CustomInput, PrimaryButton } from '@components';
|
||||||
import { useAppTheme } from '@theme';
|
import { useAppTheme } from '@theme';
|
||||||
import { useAppDispatch } from '../../../hooks/useAppDispatch';
|
import { useLoginScreen } from '../../../hooks';
|
||||||
import { loginWithPhone } from '../../../store/authSlice';
|
|
||||||
import { AuthStackParamList } from '../../../navigation/authStack';
|
|
||||||
|
|
||||||
type LoginNavProp = StackNavigationProp<AuthStackParamList, 'LoginScreen'>;
|
|
||||||
|
|
||||||
export const LoginScreen: React.FC = () => {
|
export const LoginScreen: React.FC = () => {
|
||||||
const { colors } = useAppTheme();
|
const { colors } = useAppTheme();
|
||||||
const styles = getStyles(colors);
|
const styles = getStyles(colors);
|
||||||
const navigation = useNavigation<LoginNavProp>();
|
|
||||||
const dispatch = useAppDispatch();
|
|
||||||
|
|
||||||
const [mobileNumber, setMobileNumber] = useState('');
|
const { mobileNumber, error, onChangeMobileNumber, handleLogin } =
|
||||||
const [error, setError] = useState<string | undefined>(undefined);
|
useLoginScreen();
|
||||||
|
|
||||||
const handleLogin = () => {
|
|
||||||
if (!mobileNumber) {
|
|
||||||
setError('Mobile number is required');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (mobileNumber.length < 10) {
|
|
||||||
setError('Please enter a valid mobile number');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
setError(undefined);
|
|
||||||
dispatch(loginWithPhone(mobileNumber));
|
|
||||||
navigation.navigate('OtpScreen', { mobileNumber });
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<KeyboardAvoidingView
|
<KeyboardAvoidingView
|
||||||
@ -48,7 +26,21 @@ export const LoginScreen: React.FC = () => {
|
|||||||
<ScrollView
|
<ScrollView
|
||||||
contentContainerStyle={styles.scrollContainer}
|
contentContainerStyle={styles.scrollContainer}
|
||||||
showsVerticalScrollIndicator={false}
|
showsVerticalScrollIndicator={false}
|
||||||
|
bounces={false}
|
||||||
>
|
>
|
||||||
|
{/* Hero / branding */}
|
||||||
|
<View style={styles.hero}>
|
||||||
|
<View style={styles.heroDecoCircleLg} />
|
||||||
|
<View style={styles.heroDecoCircleSm} />
|
||||||
|
<View style={styles.logoBadge}>
|
||||||
|
<Text style={styles.logoEmoji}>🛵</Text>
|
||||||
|
</View>
|
||||||
|
<Text style={styles.brandName}>QuickCart</Text>
|
||||||
|
<Text style={styles.brandTagline}>Delivered fast, every time</Text>
|
||||||
|
</View>
|
||||||
|
|
||||||
|
{/* Form card */}
|
||||||
|
<View style={styles.formCard}>
|
||||||
<View style={styles.headerContainer}>
|
<View style={styles.headerContainer}>
|
||||||
<Text style={styles.title}>Welcome back</Text>
|
<Text style={styles.title}>Welcome back</Text>
|
||||||
<Text style={styles.subtitle}>Login to continue</Text>
|
<Text style={styles.subtitle}>Login to continue</Text>
|
||||||
@ -57,22 +49,40 @@ export const LoginScreen: React.FC = () => {
|
|||||||
<View style={styles.formContainer}>
|
<View style={styles.formContainer}>
|
||||||
<CustomInput
|
<CustomInput
|
||||||
label="Mobile number"
|
label="Mobile number"
|
||||||
placeholder="+91 98765 43210"
|
placeholder="98765 43210"
|
||||||
keyboardType="phone-pad"
|
keyboardType="phone-pad"
|
||||||
|
maxLength={10}
|
||||||
value={mobileNumber}
|
value={mobileNumber}
|
||||||
onChangeText={(text) => {
|
onChangeText={onChangeMobileNumber}
|
||||||
setMobileNumber(text);
|
|
||||||
if (error) setError(undefined);
|
|
||||||
}}
|
|
||||||
error={error}
|
error={error}
|
||||||
|
leftElement={
|
||||||
|
<View style={styles.prefixChip}>
|
||||||
|
<Text style={styles.prefixFlag}>🇮🇳</Text>
|
||||||
|
<Text style={styles.prefixText}>+91</Text>
|
||||||
|
</View>
|
||||||
|
}
|
||||||
/>
|
/>
|
||||||
|
{!error && (
|
||||||
|
<Text style={styles.helperText}>
|
||||||
|
We'll send a one-time password to verify this number
|
||||||
|
</Text>
|
||||||
|
)}
|
||||||
|
|
||||||
<PrimaryButton
|
<PrimaryButton
|
||||||
title="Login"
|
title="Get OTP"
|
||||||
onPress={handleLogin}
|
onPress={handleLogin}
|
||||||
style={{ marginTop: 12 }}
|
style={{ marginTop: 12 }}
|
||||||
/>
|
/>
|
||||||
</View>
|
</View>
|
||||||
|
|
||||||
|
<View style={styles.footerSpacer} />
|
||||||
|
|
||||||
|
<Text style={styles.termsText}>
|
||||||
|
By continuing, you agree to our{' '}
|
||||||
|
<Text style={styles.termsLink}>Terms of Service</Text> and{' '}
|
||||||
|
<Text style={styles.termsLink}>Privacy Policy</Text>
|
||||||
|
</Text>
|
||||||
|
</View>
|
||||||
</ScrollView>
|
</ScrollView>
|
||||||
</KeyboardAvoidingView>
|
</KeyboardAvoidingView>
|
||||||
);
|
);
|
||||||
|
|||||||
@ -1,7 +1,8 @@
|
|||||||
import { StyleSheet } from 'react-native';
|
import { StyleSheet, Platform } from 'react-native';
|
||||||
import { typography } from '@theme';
|
import { typography } from '@theme';
|
||||||
|
|
||||||
export const getStyles = (colors: any) => StyleSheet.create({
|
export const getStyles = (colors: any) =>
|
||||||
|
StyleSheet.create({
|
||||||
container: {
|
container: {
|
||||||
flex: 1,
|
flex: 1,
|
||||||
backgroundColor: colors.background,
|
backgroundColor: colors.background,
|
||||||
@ -9,60 +10,215 @@ export const getStyles = (colors: any) => StyleSheet.create({
|
|||||||
content: {
|
content: {
|
||||||
paddingBottom: 40,
|
paddingBottom: 40,
|
||||||
},
|
},
|
||||||
|
|
||||||
|
// ---------- Profile card ----------
|
||||||
profileCard: {
|
profileCard: {
|
||||||
alignItems: 'center',
|
alignItems: 'center',
|
||||||
paddingVertical: 32,
|
backgroundColor: colors.cardBg,
|
||||||
borderBottomWidth: 1,
|
|
||||||
borderBottomColor: colors.border,
|
|
||||||
marginHorizontal: 16,
|
marginHorizontal: 16,
|
||||||
|
marginTop: 16,
|
||||||
|
borderRadius: 20,
|
||||||
|
paddingVertical: 28,
|
||||||
|
paddingHorizontal: 20,
|
||||||
|
...Platform.select({
|
||||||
|
ios: {
|
||||||
|
shadowColor: '#000',
|
||||||
|
shadowOffset: { width: 0, height: 3 },
|
||||||
|
shadowOpacity: 0.06,
|
||||||
|
shadowRadius: 10,
|
||||||
|
},
|
||||||
|
android: { elevation: 2 },
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
avatarWrap: {
|
||||||
|
position: 'relative',
|
||||||
|
marginBottom: 14,
|
||||||
},
|
},
|
||||||
avatar: {
|
avatar: {
|
||||||
width: 72,
|
width: 84,
|
||||||
height: 72,
|
height: 84,
|
||||||
borderRadius: 36,
|
borderRadius: 42,
|
||||||
backgroundColor: colors.primary,
|
backgroundColor: colors.primary,
|
||||||
justifyContent: 'center',
|
justifyContent: 'center',
|
||||||
alignItems: 'center',
|
alignItems: 'center',
|
||||||
marginBottom: 12,
|
borderWidth: 3,
|
||||||
|
borderColor: colors.primaryMuted ?? '#E9F7EF',
|
||||||
},
|
},
|
||||||
avatarText: {
|
avatarText: {
|
||||||
fontSize: 28,
|
fontSize: 32,
|
||||||
fontWeight: typography.fontWeight.bold,
|
fontWeight: typography.fontWeight.bold,
|
||||||
color: '#FFFFFF',
|
color: '#FFFFFF',
|
||||||
},
|
},
|
||||||
|
editAvatarBadge: {
|
||||||
|
position: 'absolute',
|
||||||
|
bottom: -2,
|
||||||
|
right: -2,
|
||||||
|
width: 28,
|
||||||
|
height: 28,
|
||||||
|
borderRadius: 14,
|
||||||
|
backgroundColor: colors.background,
|
||||||
|
alignItems: 'center',
|
||||||
|
justifyContent: 'center',
|
||||||
|
borderWidth: 2,
|
||||||
|
borderColor: colors.cardBg,
|
||||||
|
},
|
||||||
|
editAvatarIcon: {
|
||||||
|
fontSize: 12,
|
||||||
|
},
|
||||||
profileName: {
|
profileName: {
|
||||||
fontSize: typography.fontSize.xl,
|
fontSize: typography.fontSize.xl,
|
||||||
fontWeight: typography.fontWeight.bold,
|
fontWeight: typography.fontWeight.bold,
|
||||||
color: colors.text,
|
color: colors.text,
|
||||||
marginBottom: 4,
|
marginBottom: 4,
|
||||||
},
|
},
|
||||||
|
profilePhoneRow: {
|
||||||
|
flexDirection: 'row',
|
||||||
|
alignItems: 'center',
|
||||||
|
marginBottom: 16,
|
||||||
|
},
|
||||||
|
profilePhoneIcon: {
|
||||||
|
fontSize: 13,
|
||||||
|
marginRight: 5,
|
||||||
|
},
|
||||||
profilePhone: {
|
profilePhone: {
|
||||||
fontSize: typography.fontSize.md,
|
fontSize: typography.fontSize.md,
|
||||||
color: colors.textSecondary,
|
color: colors.textSecondary,
|
||||||
},
|
},
|
||||||
|
editProfileButton: {
|
||||||
|
borderWidth: 1.5,
|
||||||
|
borderColor: colors.primary,
|
||||||
|
borderRadius: 20,
|
||||||
|
paddingHorizontal: 20,
|
||||||
|
paddingVertical: 8,
|
||||||
|
},
|
||||||
|
editProfileButtonText: {
|
||||||
|
fontSize: typography.fontSize.sm,
|
||||||
|
fontWeight: typography.fontWeight.semibold,
|
||||||
|
color: colors.primary,
|
||||||
|
},
|
||||||
|
|
||||||
|
// ---------- Stats row ----------
|
||||||
|
statsRow: {
|
||||||
|
flexDirection: 'row',
|
||||||
|
marginHorizontal: 16,
|
||||||
|
marginTop: 14,
|
||||||
|
backgroundColor: colors.cardBg,
|
||||||
|
borderRadius: 16,
|
||||||
|
paddingVertical: 16,
|
||||||
|
...Platform.select({
|
||||||
|
ios: {
|
||||||
|
shadowColor: '#000',
|
||||||
|
shadowOffset: { width: 0, height: 2 },
|
||||||
|
shadowOpacity: 0.05,
|
||||||
|
shadowRadius: 8,
|
||||||
|
},
|
||||||
|
android: { elevation: 1 },
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
statItem: {
|
||||||
|
flex: 1,
|
||||||
|
alignItems: 'center',
|
||||||
|
},
|
||||||
|
statDivider: {
|
||||||
|
width: 1,
|
||||||
|
backgroundColor: colors.border,
|
||||||
|
},
|
||||||
|
statValue: {
|
||||||
|
fontSize: typography.fontSize.lg,
|
||||||
|
fontWeight: typography.fontWeight.bold,
|
||||||
|
color: colors.text,
|
||||||
|
},
|
||||||
|
statLabel: {
|
||||||
|
fontSize: typography.fontSize.xs,
|
||||||
|
color: colors.textSecondary,
|
||||||
|
marginTop: 2,
|
||||||
|
},
|
||||||
|
|
||||||
|
// ---------- Menu sections ----------
|
||||||
menuSection: {
|
menuSection: {
|
||||||
marginTop: 16,
|
marginTop: 24,
|
||||||
paddingHorizontal: 16,
|
paddingHorizontal: 16,
|
||||||
},
|
},
|
||||||
|
sectionLabel: {
|
||||||
|
fontSize: typography.fontSize.xs,
|
||||||
|
fontWeight: typography.fontWeight.semibold,
|
||||||
|
color: colors.textSecondary,
|
||||||
|
letterSpacing: 0.8,
|
||||||
|
textTransform: 'uppercase',
|
||||||
|
marginBottom: 10,
|
||||||
|
marginLeft: 4,
|
||||||
|
},
|
||||||
|
sectionCard: {
|
||||||
|
backgroundColor: colors.cardBg,
|
||||||
|
borderRadius: 16,
|
||||||
|
overflow: 'hidden',
|
||||||
|
...Platform.select({
|
||||||
|
ios: {
|
||||||
|
shadowColor: '#000',
|
||||||
|
shadowOffset: { width: 0, height: 2 },
|
||||||
|
shadowOpacity: 0.04,
|
||||||
|
shadowRadius: 8,
|
||||||
|
},
|
||||||
|
android: { elevation: 1 },
|
||||||
|
}),
|
||||||
|
},
|
||||||
menuItem: {
|
menuItem: {
|
||||||
flexDirection: 'row',
|
flexDirection: 'row',
|
||||||
alignItems: 'center',
|
alignItems: 'center',
|
||||||
paddingVertical: 16,
|
paddingVertical: 14,
|
||||||
|
paddingHorizontal: 14,
|
||||||
|
},
|
||||||
|
menuItemDivider: {
|
||||||
borderBottomWidth: 1,
|
borderBottomWidth: 1,
|
||||||
borderBottomColor: colors.border,
|
borderBottomColor: colors.border,
|
||||||
},
|
},
|
||||||
menuIcon: {
|
menuIconBadge: {
|
||||||
fontSize: 20,
|
width: 38,
|
||||||
|
height: 38,
|
||||||
|
borderRadius: 12,
|
||||||
|
alignItems: 'center',
|
||||||
|
justifyContent: 'center',
|
||||||
marginRight: 14,
|
marginRight: 14,
|
||||||
},
|
},
|
||||||
menuLabel: {
|
menuIcon: {
|
||||||
|
fontSize: 17,
|
||||||
|
},
|
||||||
|
menuTextWrap: {
|
||||||
flex: 1,
|
flex: 1,
|
||||||
|
},
|
||||||
|
menuLabel: {
|
||||||
fontSize: typography.fontSize.md,
|
fontSize: typography.fontSize.md,
|
||||||
fontWeight: typography.fontWeight.medium,
|
fontWeight: typography.fontWeight.medium,
|
||||||
color: colors.text,
|
color: colors.text,
|
||||||
},
|
},
|
||||||
|
menuSubLabel: {
|
||||||
|
fontSize: typography.fontSize.xs,
|
||||||
|
color: colors.textSecondary,
|
||||||
|
marginTop: 1,
|
||||||
|
},
|
||||||
|
menuArrowBadge: {
|
||||||
|
width: 24,
|
||||||
|
height: 24,
|
||||||
|
borderRadius: 12,
|
||||||
|
backgroundColor: colors.background,
|
||||||
|
alignItems: 'center',
|
||||||
|
justifyContent: 'center',
|
||||||
|
},
|
||||||
menuArrow: {
|
menuArrow: {
|
||||||
fontSize: 18,
|
fontSize: 13,
|
||||||
color: colors.textSecondary,
|
color: colors.textSecondary,
|
||||||
},
|
},
|
||||||
});
|
|
||||||
|
// ---------- Logout ----------
|
||||||
|
logoutSection: {
|
||||||
|
paddingHorizontal: 16,
|
||||||
|
marginTop: 28,
|
||||||
|
},
|
||||||
|
versionText: {
|
||||||
|
textAlign: 'center',
|
||||||
|
fontSize: typography.fontSize.xs,
|
||||||
|
color: colors.textSecondary,
|
||||||
|
marginTop: 18,
|
||||||
|
opacity: 0.7,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|||||||
@ -1,59 +1,197 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { View, Text, TouchableOpacity, ScrollView } from 'react-native';
|
import { View, Text, TouchableOpacity, ScrollView } from 'react-native';
|
||||||
|
import {
|
||||||
|
useNavigation,
|
||||||
|
CompositeNavigationProp,
|
||||||
|
} from '@react-navigation/native';
|
||||||
|
import { BottomTabNavigationProp } from '@react-navigation/bottom-tabs';
|
||||||
|
import { StackNavigationProp } from '@react-navigation/stack';
|
||||||
import { getStyles } from './accountScreen.styles';
|
import { getStyles } from './accountScreen.styles';
|
||||||
import { Header, PrimaryButton } from '@components';
|
import { Header, PrimaryButton } from '@components';
|
||||||
import { useAppTheme } from '@theme';
|
import { useAppTheme } from '@theme';
|
||||||
import { useAppDispatch, useAppSelector } from '../../../hooks/useAppDispatch';
|
import { useAppDispatch, useAppSelector } from '../../../hooks/useAppDispatch';
|
||||||
import { logout } from '../../../store/authSlice';
|
import { logout } from '../../../store/commonreducers/auth';
|
||||||
|
import { AppStackParamList } from '../../../navigation/appStack';
|
||||||
|
import { MainTabParamList } from '../../../navigation/mainTabNavigator';
|
||||||
|
|
||||||
const MENU_ITEMS = [
|
type AccountNavProp = CompositeNavigationProp<
|
||||||
{ icon: '📍', label: 'My Addresses' },
|
BottomTabNavigationProp<MainTabParamList, 'AccountScreen'>,
|
||||||
{ icon: '💳', label: 'Payment Methods' },
|
StackNavigationProp<AppStackParamList>
|
||||||
{ icon: '🔔', label: 'Notifications' },
|
>;
|
||||||
{ icon: '❓', label: 'Help & Support' },
|
|
||||||
{ icon: '📋', label: 'Terms & Conditions' },
|
interface MenuItemData {
|
||||||
{ icon: '🔒', label: 'Privacy Policy' },
|
icon: string;
|
||||||
|
label: string;
|
||||||
|
subLabel: string;
|
||||||
|
tint: string;
|
||||||
|
onPress?: (navigation: AccountNavProp) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface MenuSectionData {
|
||||||
|
title: string;
|
||||||
|
items: MenuItemData[];
|
||||||
|
}
|
||||||
|
|
||||||
|
const MENU_SECTIONS: MenuSectionData[] = [
|
||||||
|
{
|
||||||
|
title: 'Account',
|
||||||
|
items: [
|
||||||
|
{
|
||||||
|
icon: '📍',
|
||||||
|
label: 'My Addresses',
|
||||||
|
subLabel: 'Manage delivery addresses',
|
||||||
|
tint: '#FDECEA',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
icon: '💳',
|
||||||
|
label: 'Payment Methods',
|
||||||
|
subLabel: 'Cards, UPI & wallets',
|
||||||
|
tint: '#FFF4E5',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
icon: '🔔',
|
||||||
|
label: 'Notifications',
|
||||||
|
subLabel: 'Alerts, offers & updates',
|
||||||
|
tint: '#FFF9DB',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'Support & Legal',
|
||||||
|
items: [
|
||||||
|
{
|
||||||
|
icon: '❓',
|
||||||
|
label: 'Help & Support',
|
||||||
|
subLabel: 'FAQs and contact us',
|
||||||
|
tint: '#EAF4FF',
|
||||||
|
onPress: navigation => navigation.navigate('HelpSupportScreen'),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
icon: '📋',
|
||||||
|
label: 'Terms & Conditions',
|
||||||
|
subLabel: 'Our terms of service',
|
||||||
|
tint: '#F1F0FF',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
icon: '🔒',
|
||||||
|
label: 'Privacy Policy',
|
||||||
|
subLabel: 'How we handle your data',
|
||||||
|
tint: '#E9F7EF',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
export const AccountScreen: React.FC = () => {
|
export const AccountScreen: React.FC = () => {
|
||||||
const { colors } = useAppTheme();
|
const { colors } = useAppTheme();
|
||||||
const styles = getStyles(colors);
|
const styles = getStyles(colors);
|
||||||
const dispatch = useAppDispatch();
|
const dispatch = useAppDispatch();
|
||||||
const user = useAppSelector((state) => state.auth.user);
|
const navigation = useNavigation<AccountNavProp>();
|
||||||
|
const user = useAppSelector(state => state.auth.user);
|
||||||
|
|
||||||
|
// TODO: wire these to real selectors once order/wallet state is available.
|
||||||
|
// const ordersCount = user?.ordersCount ?? 0;
|
||||||
|
// const savedAddressesCount = user?.addressesCount ?? 0;
|
||||||
|
// const walletBalance = user?.walletBalance ?? 0;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<View style={styles.container}>
|
<View style={styles.container}>
|
||||||
<Header title="Account" />
|
<Header title="Account" />
|
||||||
<ScrollView contentContainerStyle={styles.content}>
|
<ScrollView
|
||||||
|
contentContainerStyle={styles.content}
|
||||||
|
showsVerticalScrollIndicator={false}
|
||||||
|
>
|
||||||
|
{/* Profile card */}
|
||||||
<View style={styles.profileCard}>
|
<View style={styles.profileCard}>
|
||||||
|
<View style={styles.avatarWrap}>
|
||||||
<View style={styles.avatar}>
|
<View style={styles.avatar}>
|
||||||
<Text style={styles.avatarText}>
|
<Text style={styles.avatarText}>
|
||||||
{user?.name?.charAt(0)?.toUpperCase() || 'U'}
|
{user?.name?.charAt(0)?.toUpperCase() || 'U'}
|
||||||
</Text>
|
</Text>
|
||||||
</View>
|
</View>
|
||||||
<Text style={styles.profileName}>{user?.name || 'User'}</Text>
|
|
||||||
<Text style={styles.profilePhone}>{user?.mobileNumber || '+91 98765 43210'}</Text>
|
|
||||||
</View>
|
|
||||||
|
|
||||||
<View style={styles.menuSection}>
|
|
||||||
{MENU_ITEMS.map((item, index) => (
|
|
||||||
<TouchableOpacity
|
<TouchableOpacity
|
||||||
key={index}
|
style={styles.editAvatarBadge}
|
||||||
style={styles.menuItem}
|
|
||||||
activeOpacity={0.7}
|
activeOpacity={0.7}
|
||||||
>
|
>
|
||||||
<Text style={styles.menuIcon}>{item.icon}</Text>
|
<Text style={styles.editAvatarIcon}>✏️</Text>
|
||||||
<Text style={styles.menuLabel}>{item.label}</Text>
|
|
||||||
<Text style={styles.menuArrow}>{'>'}</Text>
|
|
||||||
</TouchableOpacity>
|
</TouchableOpacity>
|
||||||
))}
|
|
||||||
</View>
|
</View>
|
||||||
|
|
||||||
<PrimaryButton
|
<Text style={styles.profileName}>{user?.name || 'User'}</Text>
|
||||||
title="Logout"
|
<View style={styles.profilePhoneRow}>
|
||||||
onPress={() => dispatch(logout())}
|
<Text style={styles.profilePhoneIcon}>📞</Text>
|
||||||
style={{ marginTop: 24 }}
|
<Text style={styles.profilePhone}>
|
||||||
/>
|
{user?.mobileNumber || '+91 98765 43210'}
|
||||||
|
</Text>
|
||||||
|
</View>
|
||||||
|
|
||||||
|
<TouchableOpacity
|
||||||
|
style={styles.editProfileButton}
|
||||||
|
activeOpacity={0.75}
|
||||||
|
>
|
||||||
|
<Text style={styles.editProfileButtonText}>Edit Profile</Text>
|
||||||
|
</TouchableOpacity>
|
||||||
|
</View>
|
||||||
|
|
||||||
|
{/* Quick stats */}
|
||||||
|
{/* <View style={styles.statsRow}>
|
||||||
|
<View style={styles.statItem}>
|
||||||
|
<Text style={styles.statValue}>{ordersCount}</Text>
|
||||||
|
<Text style={styles.statLabel}>Orders</Text>
|
||||||
|
</View>
|
||||||
|
<View style={styles.statDivider} />
|
||||||
|
<View style={styles.statItem}>
|
||||||
|
<Text style={styles.statValue}>{savedAddressesCount}</Text>
|
||||||
|
<Text style={styles.statLabel}>Addresses</Text>
|
||||||
|
</View>
|
||||||
|
<View style={styles.statDivider} />
|
||||||
|
<View style={styles.statItem}>
|
||||||
|
<Text style={styles.statValue}>₹{walletBalance}</Text>
|
||||||
|
<Text style={styles.statLabel}>Wallet</Text>
|
||||||
|
</View>
|
||||||
|
</View> */}
|
||||||
|
|
||||||
|
{/* Menu sections */}
|
||||||
|
{MENU_SECTIONS.map(section => (
|
||||||
|
<View key={section.title} style={styles.menuSection}>
|
||||||
|
<Text style={styles.sectionLabel}>{section.title}</Text>
|
||||||
|
<View style={styles.sectionCard}>
|
||||||
|
{section.items.map((item, index) => {
|
||||||
|
const isLast = index === section.items.length - 1;
|
||||||
|
return (
|
||||||
|
<TouchableOpacity
|
||||||
|
key={item.label}
|
||||||
|
style={[styles.menuItem, !isLast && styles.menuItemDivider]}
|
||||||
|
activeOpacity={0.7}
|
||||||
|
onPress={() => item.onPress?.(navigation)}
|
||||||
|
>
|
||||||
|
<View
|
||||||
|
style={[
|
||||||
|
styles.menuIconBadge,
|
||||||
|
{ backgroundColor: item.tint },
|
||||||
|
]}
|
||||||
|
>
|
||||||
|
<Text style={styles.menuIcon}>{item.icon}</Text>
|
||||||
|
</View>
|
||||||
|
<View style={styles.menuTextWrap}>
|
||||||
|
<Text style={styles.menuLabel}>{item.label}</Text>
|
||||||
|
<Text style={styles.menuSubLabel}>{item.subLabel}</Text>
|
||||||
|
</View>
|
||||||
|
<View style={styles.menuArrowBadge}>
|
||||||
|
<Text style={styles.menuArrow}>{'>'}</Text>
|
||||||
|
</View>
|
||||||
|
</TouchableOpacity>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
))}
|
||||||
|
|
||||||
|
{/* Logout */}
|
||||||
|
<View style={styles.logoutSection}>
|
||||||
|
<PrimaryButton title="Logout" onPress={() => dispatch(logout())} />
|
||||||
|
<Text style={styles.versionText}>App version 1.0.0</Text>
|
||||||
|
</View>
|
||||||
</ScrollView>
|
</ScrollView>
|
||||||
</View>
|
</View>
|
||||||
);
|
);
|
||||||
|
|||||||
@ -1,72 +1,232 @@
|
|||||||
import { StyleSheet } from 'react-native';
|
import { StyleSheet, Platform } from 'react-native';
|
||||||
import { typography } from '@theme';
|
import { typography } from '@theme';
|
||||||
|
|
||||||
export const getStyles = (colors: any) => StyleSheet.create({
|
export const getStyles = (colors: any) =>
|
||||||
|
StyleSheet.create({
|
||||||
container: {
|
container: {
|
||||||
flex: 1,
|
flex: 1,
|
||||||
backgroundColor: colors.background,
|
backgroundColor: colors.background,
|
||||||
},
|
},
|
||||||
list: {
|
list: {
|
||||||
paddingBottom: 100,
|
paddingBottom: 140,
|
||||||
|
},
|
||||||
|
|
||||||
|
// ---------- ETA banner ----------
|
||||||
|
etaBanner: {
|
||||||
|
flexDirection: 'row',
|
||||||
|
alignItems: 'center',
|
||||||
|
backgroundColor: colors.primaryMuted ?? '#E9F7EF',
|
||||||
|
marginHorizontal: 16,
|
||||||
|
marginTop: 12,
|
||||||
|
marginBottom: 4,
|
||||||
|
borderRadius: 12,
|
||||||
|
paddingVertical: 10,
|
||||||
|
paddingHorizontal: 14,
|
||||||
|
},
|
||||||
|
etaIcon: {
|
||||||
|
fontSize: 16,
|
||||||
|
marginRight: 8,
|
||||||
|
},
|
||||||
|
etaText: {
|
||||||
|
fontSize: typography.fontSize.sm,
|
||||||
|
fontWeight: typography.fontWeight.semibold,
|
||||||
|
color: colors.primary,
|
||||||
|
},
|
||||||
|
|
||||||
|
// ---------- Item cards ----------
|
||||||
|
itemsCard: {
|
||||||
|
backgroundColor: colors.cardBg,
|
||||||
|
marginHorizontal: 16,
|
||||||
|
marginTop: 14,
|
||||||
|
borderRadius: 16,
|
||||||
|
overflow: 'hidden',
|
||||||
|
...Platform.select({
|
||||||
|
ios: {
|
||||||
|
shadowColor: '#000',
|
||||||
|
shadowOffset: { width: 0, height: 2 },
|
||||||
|
shadowOpacity: 0.05,
|
||||||
|
shadowRadius: 8,
|
||||||
|
},
|
||||||
|
android: { elevation: 1 },
|
||||||
|
}),
|
||||||
},
|
},
|
||||||
cartItem: {
|
cartItem: {
|
||||||
flexDirection: 'row',
|
flexDirection: 'row',
|
||||||
alignItems: 'center',
|
alignItems: 'center',
|
||||||
justifyContent: 'space-between',
|
|
||||||
paddingVertical: 14,
|
paddingVertical: 14,
|
||||||
paddingHorizontal: 16,
|
paddingHorizontal: 14,
|
||||||
|
},
|
||||||
|
cartItemDivider: {
|
||||||
borderBottomWidth: 1,
|
borderBottomWidth: 1,
|
||||||
borderBottomColor: colors.border,
|
borderBottomColor: colors.border,
|
||||||
},
|
},
|
||||||
|
itemThumb: {
|
||||||
|
width: 52,
|
||||||
|
height: 52,
|
||||||
|
borderRadius: 10,
|
||||||
|
backgroundColor: colors.inputBg,
|
||||||
|
alignItems: 'center',
|
||||||
|
justifyContent: 'center',
|
||||||
|
marginRight: 12,
|
||||||
|
},
|
||||||
|
itemThumbEmoji: {
|
||||||
|
fontSize: 22,
|
||||||
|
},
|
||||||
itemInfo: {
|
itemInfo: {
|
||||||
flex: 1,
|
flex: 1,
|
||||||
|
marginRight: 10,
|
||||||
},
|
},
|
||||||
itemTitle: {
|
itemTitle: {
|
||||||
fontSize: typography.fontSize.md,
|
fontSize: typography.fontSize.md,
|
||||||
fontWeight: typography.fontWeight.medium,
|
fontWeight: typography.fontWeight.semibold,
|
||||||
color: colors.text,
|
color: colors.text,
|
||||||
marginBottom: 4,
|
marginBottom: 4,
|
||||||
},
|
},
|
||||||
itemPrice: {
|
itemPrice: {
|
||||||
fontSize: typography.fontSize.md,
|
fontSize: typography.fontSize.sm,
|
||||||
|
fontWeight: typography.fontWeight.bold,
|
||||||
|
color: colors.textSecondary,
|
||||||
|
},
|
||||||
|
|
||||||
|
// ---------- Add more items ----------
|
||||||
|
addMoreRow: {
|
||||||
|
flexDirection: 'row',
|
||||||
|
alignItems: 'center',
|
||||||
|
justifyContent: 'center',
|
||||||
|
paddingVertical: 12,
|
||||||
|
marginHorizontal: 16,
|
||||||
|
marginTop: 4,
|
||||||
|
borderWidth: 1.5,
|
||||||
|
borderStyle: 'dashed',
|
||||||
|
borderColor: colors.border,
|
||||||
|
borderRadius: 12,
|
||||||
|
},
|
||||||
|
addMoreIcon: {
|
||||||
|
fontSize: 14,
|
||||||
|
marginRight: 6,
|
||||||
|
color: colors.primary,
|
||||||
|
},
|
||||||
|
addMoreText: {
|
||||||
|
fontSize: typography.fontSize.sm,
|
||||||
|
fontWeight: typography.fontWeight.semibold,
|
||||||
|
color: colors.primary,
|
||||||
|
},
|
||||||
|
|
||||||
|
// ---------- Footer ----------
|
||||||
|
footer: {
|
||||||
|
paddingHorizontal: 16,
|
||||||
|
paddingTop: 20,
|
||||||
|
},
|
||||||
|
|
||||||
|
// Coupon
|
||||||
|
sectionTitle: {
|
||||||
|
fontSize: typography.fontSize.sm,
|
||||||
fontWeight: typography.fontWeight.bold,
|
fontWeight: typography.fontWeight.bold,
|
||||||
color: colors.text,
|
color: colors.text,
|
||||||
|
marginBottom: 10,
|
||||||
},
|
},
|
||||||
footer: {
|
couponCard: {
|
||||||
padding: 16,
|
backgroundColor: colors.cardBg,
|
||||||
|
borderRadius: 14,
|
||||||
|
padding: 6,
|
||||||
|
marginBottom: 20,
|
||||||
|
...Platform.select({
|
||||||
|
ios: {
|
||||||
|
shadowColor: '#000',
|
||||||
|
shadowOffset: { width: 0, height: 2 },
|
||||||
|
shadowOpacity: 0.05,
|
||||||
|
shadowRadius: 8,
|
||||||
|
},
|
||||||
|
android: { elevation: 1 },
|
||||||
|
}),
|
||||||
},
|
},
|
||||||
couponRow: {
|
couponRow: {
|
||||||
flexDirection: 'row',
|
flexDirection: 'row',
|
||||||
alignItems: 'center',
|
alignItems: 'center',
|
||||||
marginBottom: 20,
|
},
|
||||||
|
couponIcon: {
|
||||||
|
fontSize: 18,
|
||||||
|
marginLeft: 10,
|
||||||
|
marginRight: 4,
|
||||||
},
|
},
|
||||||
couponInput: {
|
couponInput: {
|
||||||
flex: 1,
|
flex: 1,
|
||||||
height: 48,
|
height: 44,
|
||||||
borderWidth: 1,
|
paddingHorizontal: 8,
|
||||||
borderColor: colors.border,
|
|
||||||
borderRadius: 12,
|
|
||||||
paddingHorizontal: 16,
|
|
||||||
fontSize: typography.fontSize.md,
|
fontSize: typography.fontSize.md,
|
||||||
color: colors.text,
|
color: colors.text,
|
||||||
backgroundColor: colors.inputBg,
|
|
||||||
marginRight: 8,
|
|
||||||
},
|
},
|
||||||
applyButton: {
|
applyButton: {
|
||||||
paddingHorizontal: 20,
|
paddingHorizontal: 18,
|
||||||
paddingVertical: 14,
|
paddingVertical: 11,
|
||||||
borderRadius: 12,
|
borderRadius: 10,
|
||||||
backgroundColor: colors.primary,
|
backgroundColor: colors.primary,
|
||||||
|
marginRight: 4,
|
||||||
},
|
},
|
||||||
applyButtonText: {
|
applyButtonText: {
|
||||||
color: '#FFFFFF',
|
color: '#FFFFFF',
|
||||||
fontWeight: typography.fontWeight.semibold,
|
fontWeight: typography.fontWeight.semibold,
|
||||||
fontSize: typography.fontSize.sm,
|
fontSize: typography.fontSize.sm,
|
||||||
},
|
},
|
||||||
|
couponAppliedRow: {
|
||||||
|
flexDirection: 'row',
|
||||||
|
alignItems: 'center',
|
||||||
|
justifyContent: 'space-between',
|
||||||
|
paddingVertical: 10,
|
||||||
|
paddingHorizontal: 12,
|
||||||
|
},
|
||||||
|
couponAppliedLeft: {
|
||||||
|
flexDirection: 'row',
|
||||||
|
alignItems: 'center',
|
||||||
|
flex: 1,
|
||||||
|
},
|
||||||
|
couponCheckBadge: {
|
||||||
|
width: 28,
|
||||||
|
height: 28,
|
||||||
|
borderRadius: 14,
|
||||||
|
backgroundColor: colors.primaryMuted ?? '#E9F7EF',
|
||||||
|
alignItems: 'center',
|
||||||
|
justifyContent: 'center',
|
||||||
|
marginRight: 10,
|
||||||
|
},
|
||||||
|
couponCheckIcon: {
|
||||||
|
fontSize: 13,
|
||||||
|
},
|
||||||
|
couponAppliedCode: {
|
||||||
|
fontSize: typography.fontSize.sm,
|
||||||
|
fontWeight: typography.fontWeight.bold,
|
||||||
|
color: colors.text,
|
||||||
|
},
|
||||||
|
couponAppliedSub: {
|
||||||
|
fontSize: typography.fontSize.xs,
|
||||||
|
color: colors.primary,
|
||||||
|
marginTop: 1,
|
||||||
|
},
|
||||||
|
couponRemoveText: {
|
||||||
|
fontSize: typography.fontSize.sm,
|
||||||
|
fontWeight: typography.fontWeight.semibold,
|
||||||
|
color: '#D32F2F',
|
||||||
|
},
|
||||||
|
|
||||||
|
// Bill details
|
||||||
|
billCard: {
|
||||||
|
backgroundColor: colors.cardBg,
|
||||||
|
borderRadius: 14,
|
||||||
|
padding: 16,
|
||||||
|
...Platform.select({
|
||||||
|
ios: {
|
||||||
|
shadowColor: '#000',
|
||||||
|
shadowOffset: { width: 0, height: 2 },
|
||||||
|
shadowOpacity: 0.05,
|
||||||
|
shadowRadius: 8,
|
||||||
|
},
|
||||||
|
android: { elevation: 1 },
|
||||||
|
}),
|
||||||
|
},
|
||||||
feeRow: {
|
feeRow: {
|
||||||
flexDirection: 'row',
|
flexDirection: 'row',
|
||||||
justifyContent: 'space-between',
|
justifyContent: 'space-between',
|
||||||
marginBottom: 8,
|
marginBottom: 10,
|
||||||
},
|
},
|
||||||
feeLabel: {
|
feeLabel: {
|
||||||
fontSize: typography.fontSize.sm,
|
fontSize: typography.fontSize.sm,
|
||||||
@ -82,6 +242,7 @@ export const getStyles = (colors: any) => StyleSheet.create({
|
|||||||
borderTopColor: colors.border,
|
borderTopColor: colors.border,
|
||||||
paddingTop: 12,
|
paddingTop: 12,
|
||||||
marginTop: 4,
|
marginTop: 4,
|
||||||
|
marginBottom: 0,
|
||||||
},
|
},
|
||||||
totalLabel: {
|
totalLabel: {
|
||||||
fontSize: typography.fontSize.md,
|
fontSize: typography.fontSize.md,
|
||||||
@ -93,17 +254,97 @@ export const getStyles = (colors: any) => StyleSheet.create({
|
|||||||
fontWeight: typography.fontWeight.bold,
|
fontWeight: typography.fontWeight.bold,
|
||||||
color: colors.text,
|
color: colors.text,
|
||||||
},
|
},
|
||||||
bottomPanel: {
|
savingsBanner: {
|
||||||
flexDirection: 'row',
|
flexDirection: 'row',
|
||||||
alignItems: 'center',
|
alignItems: 'center',
|
||||||
padding: 16,
|
backgroundColor: colors.primaryMuted ?? '#E9F7EF',
|
||||||
borderTopWidth: 1,
|
borderRadius: 10,
|
||||||
borderTopColor: colors.border,
|
paddingVertical: 8,
|
||||||
backgroundColor: colors.background,
|
paddingHorizontal: 12,
|
||||||
|
marginTop: 14,
|
||||||
|
},
|
||||||
|
savingsIcon: {
|
||||||
|
fontSize: 14,
|
||||||
|
marginRight: 6,
|
||||||
|
},
|
||||||
|
savingsText: {
|
||||||
|
fontSize: typography.fontSize.xs,
|
||||||
|
fontWeight: typography.fontWeight.semibold,
|
||||||
|
color: colors.primary,
|
||||||
|
},
|
||||||
|
|
||||||
|
// ---------- Bottom panel ----------
|
||||||
|
bottomPanel: {
|
||||||
|
position: 'absolute',
|
||||||
|
left: 0,
|
||||||
|
right: 0,
|
||||||
|
bottom: 0,
|
||||||
|
flexDirection: 'row',
|
||||||
|
alignItems: 'center',
|
||||||
|
justifyContent: 'space-between',
|
||||||
|
paddingHorizontal: 16,
|
||||||
|
paddingTop: 12,
|
||||||
|
paddingBottom: Platform.OS === 'ios' ? 28 : 16,
|
||||||
|
backgroundColor: colors.cardBg,
|
||||||
|
borderTopLeftRadius: 20,
|
||||||
|
borderTopRightRadius: 20,
|
||||||
|
...Platform.select({
|
||||||
|
ios: {
|
||||||
|
shadowColor: '#000',
|
||||||
|
shadowOffset: { width: 0, height: -4 },
|
||||||
|
shadowOpacity: 0.08,
|
||||||
|
shadowRadius: 12,
|
||||||
|
},
|
||||||
|
android: { elevation: 10 },
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
bottomTotalWrap: {},
|
||||||
|
bottomTotalLabel: {
|
||||||
|
fontSize: typography.fontSize.xs,
|
||||||
|
color: colors.textSecondary,
|
||||||
},
|
},
|
||||||
bottomTotal: {
|
bottomTotal: {
|
||||||
fontSize: typography.fontSize.lg,
|
fontSize: typography.fontSize.lg,
|
||||||
fontWeight: typography.fontWeight.bold,
|
fontWeight: typography.fontWeight.bold,
|
||||||
color: colors.text,
|
color: colors.text,
|
||||||
},
|
},
|
||||||
});
|
checkoutButton: {
|
||||||
|
flex: 1,
|
||||||
|
marginLeft: 16,
|
||||||
|
},
|
||||||
|
|
||||||
|
// ---------- Empty state ----------
|
||||||
|
emptyWrap: {
|
||||||
|
flex: 1,
|
||||||
|
alignItems: 'center',
|
||||||
|
justifyContent: 'center',
|
||||||
|
paddingHorizontal: 32,
|
||||||
|
},
|
||||||
|
emptyEmoji: {
|
||||||
|
fontSize: 56,
|
||||||
|
marginBottom: 16,
|
||||||
|
},
|
||||||
|
emptyTitle: {
|
||||||
|
fontSize: typography.fontSize.lg,
|
||||||
|
fontWeight: typography.fontWeight.bold,
|
||||||
|
color: colors.text,
|
||||||
|
marginBottom: 6,
|
||||||
|
},
|
||||||
|
emptySubtitle: {
|
||||||
|
fontSize: typography.fontSize.sm,
|
||||||
|
color: colors.textSecondary,
|
||||||
|
textAlign: 'center',
|
||||||
|
marginBottom: 24,
|
||||||
|
},
|
||||||
|
emptyButton: {
|
||||||
|
backgroundColor: colors.primary,
|
||||||
|
paddingHorizontal: 28,
|
||||||
|
paddingVertical: 14,
|
||||||
|
borderRadius: 12,
|
||||||
|
},
|
||||||
|
emptyButtonText: {
|
||||||
|
color: '#FFFFFF',
|
||||||
|
fontWeight: typography.fontWeight.bold,
|
||||||
|
fontSize: typography.fontSize.md,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|||||||
@ -2,34 +2,96 @@ import React, { useState } from 'react';
|
|||||||
import {
|
import {
|
||||||
View,
|
View,
|
||||||
Text,
|
Text,
|
||||||
FlatList,
|
ScrollView,
|
||||||
TextInput,
|
TextInput,
|
||||||
TouchableOpacity,
|
TouchableOpacity,
|
||||||
} from 'react-native';
|
} from 'react-native';
|
||||||
|
import { useNavigation } from '@react-navigation/native';
|
||||||
|
import { StackNavigationProp } from '@react-navigation/stack';
|
||||||
import { getStyles } from './cartScreen.styles';
|
import { getStyles } from './cartScreen.styles';
|
||||||
import { Header, QuantitySelector, PrimaryButton } from '@components';
|
import { Header, QuantitySelector, PrimaryButton } from '@components';
|
||||||
import { useAppTheme } from '@theme';
|
import { useAppTheme } from '@theme';
|
||||||
import { useAppDispatch, useAppSelector } from '../../../hooks/useAppDispatch';
|
import { useAppDispatch, useAppSelector } from '../../../hooks/useAppDispatch';
|
||||||
import { removeItem, applyCoupon, removeCoupon, selectCartTotal } from '../../../store/cartSlice';
|
import {
|
||||||
|
removeItem,
|
||||||
|
applyCoupon,
|
||||||
|
removeCoupon,
|
||||||
|
selectCartTotal,
|
||||||
|
} from '../../../store/commonreducers/cart';
|
||||||
|
import { AppStackParamList } from '../../../navigation/appStack';
|
||||||
|
|
||||||
|
type CartScreenNavProp = StackNavigationProp<AppStackParamList, 'CartScreen'>;
|
||||||
|
|
||||||
export const CartScreen: React.FC = () => {
|
export const CartScreen: React.FC = () => {
|
||||||
const { colors } = useAppTheme();
|
const { colors } = useAppTheme();
|
||||||
const styles = getStyles(colors);
|
const styles = getStyles(colors);
|
||||||
const dispatch = useAppDispatch();
|
const dispatch = useAppDispatch();
|
||||||
const { items, couponCode } = useAppSelector((state) => state.cart);
|
const navigation = useNavigation<CartScreenNavProp>();
|
||||||
|
const { items, couponCode } = useAppSelector(state => state.cart);
|
||||||
const totals = useAppSelector(selectCartTotal);
|
const totals = useAppSelector(selectCartTotal);
|
||||||
const [couponInput, setCouponInput] = useState('');
|
const [couponInput, setCouponInput] = useState('');
|
||||||
|
|
||||||
|
const handleCouponPress = () => {
|
||||||
|
if (couponCode) {
|
||||||
|
dispatch(removeCoupon());
|
||||||
|
setCouponInput('');
|
||||||
|
} else if (couponInput) {
|
||||||
|
dispatch(applyCoupon(couponInput));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
if (items.length === 0) {
|
||||||
return (
|
return (
|
||||||
<View style={styles.container}>
|
<View style={styles.container}>
|
||||||
<Header title="Cart" onBack={() => {}} />
|
<Header title="Cart" onBack={() => navigation.goBack()} />
|
||||||
<FlatList
|
<View style={styles.emptyWrap}>
|
||||||
data={items}
|
<Text style={styles.emptyEmoji}>🛒</Text>
|
||||||
keyExtractor={(item) => item.id}
|
<Text style={styles.emptyTitle}>Your cart is empty</Text>
|
||||||
renderItem={({ item }) => (
|
<Text style={styles.emptySubtitle}>
|
||||||
<View style={styles.cartItem}>
|
Looks like you haven't added anything yet. Browse providers and find
|
||||||
|
something you'll love.
|
||||||
|
</Text>
|
||||||
|
<TouchableOpacity
|
||||||
|
style={styles.emptyButton}
|
||||||
|
activeOpacity={0.85}
|
||||||
|
onPress={() => navigation.goBack()}
|
||||||
|
>
|
||||||
|
<Text style={styles.emptyButtonText}>Start Ordering</Text>
|
||||||
|
</TouchableOpacity>
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<View style={styles.container}>
|
||||||
|
<Header title="Cart" onBack={() => navigation.goBack()} />
|
||||||
|
|
||||||
|
<ScrollView
|
||||||
|
contentContainerStyle={styles.list}
|
||||||
|
showsVerticalScrollIndicator={false}
|
||||||
|
>
|
||||||
|
<View style={styles.etaBanner}>
|
||||||
|
<Text style={styles.etaIcon}>🛵</Text>
|
||||||
|
<Text style={styles.etaText}>Delivery in 20–25 mins</Text>
|
||||||
|
</View>
|
||||||
|
|
||||||
|
<View style={styles.itemsCard}>
|
||||||
|
{items.map((item, index) => (
|
||||||
|
<View
|
||||||
|
key={item.id}
|
||||||
|
style={[
|
||||||
|
styles.cartItem,
|
||||||
|
index < items.length - 1 && styles.cartItemDivider,
|
||||||
|
]}
|
||||||
|
>
|
||||||
|
<View style={styles.itemThumb}>
|
||||||
|
<Text style={styles.itemThumbEmoji}>🍕</Text>
|
||||||
|
</View>
|
||||||
<View style={styles.itemInfo}>
|
<View style={styles.itemInfo}>
|
||||||
<Text style={styles.itemTitle}>{item.item.title}</Text>
|
<Text style={styles.itemTitle} numberOfLines={2}>
|
||||||
|
{item.item.title}
|
||||||
|
</Text>
|
||||||
<Text style={styles.itemPrice}>₹{item.item.price}</Text>
|
<Text style={styles.itemPrice}>₹{item.item.price}</Text>
|
||||||
</View>
|
</View>
|
||||||
<QuantitySelector
|
<QuantitySelector
|
||||||
@ -38,35 +100,64 @@ export const CartScreen: React.FC = () => {
|
|||||||
onDecrement={() => dispatch(removeItem(item.item.id))}
|
onDecrement={() => dispatch(removeItem(item.item.id))}
|
||||||
/>
|
/>
|
||||||
</View>
|
</View>
|
||||||
)}
|
))}
|
||||||
ListFooterComponent={
|
</View>
|
||||||
|
|
||||||
|
<TouchableOpacity
|
||||||
|
style={styles.addMoreRow}
|
||||||
|
activeOpacity={0.7}
|
||||||
|
onPress={() => navigation.goBack()}
|
||||||
|
>
|
||||||
|
<Text style={styles.addMoreIcon}>+</Text>
|
||||||
|
<Text style={styles.addMoreText}>Add more items</Text>
|
||||||
|
</TouchableOpacity>
|
||||||
|
|
||||||
<View style={styles.footer}>
|
<View style={styles.footer}>
|
||||||
|
<Text style={styles.sectionTitle}>Apply Coupon</Text>
|
||||||
|
<View style={styles.couponCard}>
|
||||||
|
{couponCode ? (
|
||||||
|
<View style={styles.couponAppliedRow}>
|
||||||
|
<View style={styles.couponAppliedLeft}>
|
||||||
|
<View style={styles.couponCheckBadge}>
|
||||||
|
<Text style={styles.couponCheckIcon}>✓</Text>
|
||||||
|
</View>
|
||||||
|
<View>
|
||||||
|
<Text style={styles.couponAppliedCode}>{couponCode}</Text>
|
||||||
|
<Text style={styles.couponAppliedSub}>Coupon applied</Text>
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
<TouchableOpacity
|
||||||
|
onPress={handleCouponPress}
|
||||||
|
activeOpacity={0.7}
|
||||||
|
>
|
||||||
|
<Text style={styles.couponRemoveText}>Remove</Text>
|
||||||
|
</TouchableOpacity>
|
||||||
|
</View>
|
||||||
|
) : (
|
||||||
<View style={styles.couponRow}>
|
<View style={styles.couponRow}>
|
||||||
|
<Text style={styles.couponIcon}>🏷️</Text>
|
||||||
<TextInput
|
<TextInput
|
||||||
style={styles.couponInput}
|
style={styles.couponInput}
|
||||||
placeholder="Enter coupon code"
|
placeholder="Enter coupon code"
|
||||||
placeholderTextColor={colors.placeholder}
|
placeholderTextColor={colors.placeholder}
|
||||||
value={couponInput}
|
value={couponInput}
|
||||||
onChangeText={setCouponInput}
|
onChangeText={setCouponInput}
|
||||||
|
autoCapitalize="characters"
|
||||||
/>
|
/>
|
||||||
<TouchableOpacity
|
<TouchableOpacity
|
||||||
style={styles.applyButton}
|
style={styles.applyButton}
|
||||||
onPress={() => {
|
onPress={handleCouponPress}
|
||||||
if (couponCode) {
|
|
||||||
dispatch(removeCoupon());
|
|
||||||
setCouponInput('');
|
|
||||||
} else if (couponInput) {
|
|
||||||
dispatch(applyCoupon(couponInput));
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
activeOpacity={0.7}
|
activeOpacity={0.7}
|
||||||
|
disabled={!couponInput}
|
||||||
>
|
>
|
||||||
<Text style={styles.applyButtonText}>
|
<Text style={styles.applyButtonText}>Apply</Text>
|
||||||
{couponCode ? 'Remove' : 'Apply'}
|
|
||||||
</Text>
|
|
||||||
</TouchableOpacity>
|
</TouchableOpacity>
|
||||||
</View>
|
</View>
|
||||||
|
)}
|
||||||
|
</View>
|
||||||
|
|
||||||
|
<Text style={styles.sectionTitle}>Bill Details</Text>
|
||||||
|
<View style={styles.billCard}>
|
||||||
<View style={styles.feeRow}>
|
<View style={styles.feeRow}>
|
||||||
<Text style={styles.feeLabel}>Subtotal</Text>
|
<Text style={styles.feeLabel}>Subtotal</Text>
|
||||||
<Text style={styles.feeValue}>₹{totals.subtotal}</Text>
|
<Text style={styles.feeValue}>₹{totals.subtotal}</Text>
|
||||||
@ -93,16 +184,28 @@ export const CartScreen: React.FC = () => {
|
|||||||
<Text style={styles.totalLabel}>Total</Text>
|
<Text style={styles.totalLabel}>Total</Text>
|
||||||
<Text style={styles.totalValue}>₹{totals.total}</Text>
|
<Text style={styles.totalValue}>₹{totals.total}</Text>
|
||||||
</View>
|
</View>
|
||||||
|
|
||||||
|
{totals.discount > 0 && (
|
||||||
|
<View style={styles.savingsBanner}>
|
||||||
|
<Text style={styles.savingsIcon}>🎉</Text>
|
||||||
|
<Text style={styles.savingsText}>
|
||||||
|
You're saving ₹{totals.discount} on this order
|
||||||
|
</Text>
|
||||||
</View>
|
</View>
|
||||||
}
|
)}
|
||||||
contentContainerStyle={styles.list}
|
</View>
|
||||||
/>
|
</View>
|
||||||
|
</ScrollView>
|
||||||
|
|
||||||
<View style={styles.bottomPanel}>
|
<View style={styles.bottomPanel}>
|
||||||
<Text style={styles.bottomTotal}>Total: ₹{totals.total}</Text>
|
<View style={styles.bottomTotalWrap}>
|
||||||
|
<Text style={styles.bottomTotalLabel}>Total</Text>
|
||||||
|
<Text style={styles.bottomTotal}>₹{totals.total}</Text>
|
||||||
|
</View>
|
||||||
<PrimaryButton
|
<PrimaryButton
|
||||||
title="View Bill"
|
title="Proceed to Checkout"
|
||||||
onPress={() => {}}
|
onPress={() => navigation.navigate('CheckoutAddressScreen')}
|
||||||
style={{ flex: 1, marginLeft: 12 }}
|
style={styles.checkoutButton}
|
||||||
/>
|
/>
|
||||||
</View>
|
</View>
|
||||||
</View>
|
</View>
|
||||||
|
|||||||
@ -5,9 +5,14 @@ import {
|
|||||||
TouchableOpacity,
|
TouchableOpacity,
|
||||||
ScrollView,
|
ScrollView,
|
||||||
} from 'react-native';
|
} from 'react-native';
|
||||||
|
import { useNavigation } from '@react-navigation/native';
|
||||||
|
import { StackNavigationProp } from '@react-navigation/stack';
|
||||||
import { getStyles } from './checkoutAddressScreen.styles';
|
import { getStyles } from './checkoutAddressScreen.styles';
|
||||||
import { Header, StepProgress, PrimaryButton } from '@components';
|
import { Header, StepProgress, PrimaryButton } from '@components';
|
||||||
import { useAppTheme } from '@theme';
|
import { useAppTheme } from '@theme';
|
||||||
|
import { AppStackParamList } from '../../../navigation/appStack';
|
||||||
|
|
||||||
|
type CheckoutAddressNavProp = StackNavigationProp<AppStackParamList, 'CheckoutAddressScreen'>;
|
||||||
|
|
||||||
const CHECKOUT_STEPS = [
|
const CHECKOUT_STEPS = [
|
||||||
{ key: 'address', label: 'Address' },
|
{ key: 'address', label: 'Address' },
|
||||||
@ -18,11 +23,12 @@ const CHECKOUT_STEPS = [
|
|||||||
export const CheckoutAddressScreen: React.FC = () => {
|
export const CheckoutAddressScreen: React.FC = () => {
|
||||||
const { colors } = useAppTheme();
|
const { colors } = useAppTheme();
|
||||||
const styles = getStyles(colors);
|
const styles = getStyles(colors);
|
||||||
|
const navigation = useNavigation<CheckoutAddressNavProp>();
|
||||||
const [deliveryOption, setDeliveryOption] = useState<'standard' | 'express'>('standard');
|
const [deliveryOption, setDeliveryOption] = useState<'standard' | 'express'>('standard');
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<View style={styles.container}>
|
<View style={styles.container}>
|
||||||
<Header title="Checkout" onBack={() => {}} />
|
<Header title="Checkout" onBack={() => navigation.goBack()} />
|
||||||
<StepProgress steps={CHECKOUT_STEPS} activeStep={0} />
|
<StepProgress steps={CHECKOUT_STEPS} activeStep={0} />
|
||||||
<ScrollView contentContainerStyle={styles.content}>
|
<ScrollView contentContainerStyle={styles.content}>
|
||||||
<View style={styles.addressCard}>
|
<View style={styles.addressCard}>
|
||||||
@ -69,7 +75,7 @@ export const CheckoutAddressScreen: React.FC = () => {
|
|||||||
</TouchableOpacity>
|
</TouchableOpacity>
|
||||||
</ScrollView>
|
</ScrollView>
|
||||||
<View style={styles.footer}>
|
<View style={styles.footer}>
|
||||||
<PrimaryButton title="Continue to Payment" onPress={() => {}} />
|
<PrimaryButton title="Continue to Payment" onPress={() => navigation.navigate('CheckoutPaymentScreen')} />
|
||||||
</View>
|
</View>
|
||||||
</View>
|
</View>
|
||||||
);
|
);
|
||||||
|
|||||||
@ -1,8 +1,13 @@
|
|||||||
import React, { useState } from 'react';
|
import React, { useState } from 'react';
|
||||||
import { View, Text, ScrollView } from 'react-native';
|
import { View, Text, ScrollView } from 'react-native';
|
||||||
|
import { useNavigation } from '@react-navigation/native';
|
||||||
|
import { StackNavigationProp } from '@react-navigation/stack';
|
||||||
import { getStyles } from './checkoutPaymentScreen.styles';
|
import { getStyles } from './checkoutPaymentScreen.styles';
|
||||||
import { Header, StepProgress, PaymentOption, PrimaryButton } from '@components';
|
import { Header, StepProgress, PaymentOption, PrimaryButton } from '@components';
|
||||||
import { useAppTheme } from '@theme';
|
import { useAppTheme } from '@theme';
|
||||||
|
import { AppStackParamList } from '../../../navigation/appStack';
|
||||||
|
|
||||||
|
type CheckoutPaymentNavProp = StackNavigationProp<AppStackParamList, 'CheckoutPaymentScreen'>;
|
||||||
|
|
||||||
const CHECKOUT_STEPS = [
|
const CHECKOUT_STEPS = [
|
||||||
{ key: 'address', label: 'Address' },
|
{ key: 'address', label: 'Address' },
|
||||||
@ -20,11 +25,12 @@ const PAYMENT_METHODS = [
|
|||||||
export const CheckoutPaymentScreen: React.FC = () => {
|
export const CheckoutPaymentScreen: React.FC = () => {
|
||||||
const { colors } = useAppTheme();
|
const { colors } = useAppTheme();
|
||||||
const styles = getStyles(colors);
|
const styles = getStyles(colors);
|
||||||
|
const navigation = useNavigation<CheckoutPaymentNavProp>();
|
||||||
const [selectedMethod, setSelectedMethod] = useState('upi');
|
const [selectedMethod, setSelectedMethod] = useState('upi');
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<View style={styles.container}>
|
<View style={styles.container}>
|
||||||
<Header title="Checkout" onBack={() => {}} />
|
<Header title="Checkout" onBack={() => navigation.goBack()} />
|
||||||
<StepProgress steps={CHECKOUT_STEPS} activeStep={1} />
|
<StepProgress steps={CHECKOUT_STEPS} activeStep={1} />
|
||||||
<ScrollView contentContainerStyle={styles.content}>
|
<ScrollView contentContainerStyle={styles.content}>
|
||||||
<Text style={styles.sectionTitle}>Select Payment Method</Text>
|
<Text style={styles.sectionTitle}>Select Payment Method</Text>
|
||||||
@ -39,7 +45,7 @@ export const CheckoutPaymentScreen: React.FC = () => {
|
|||||||
))}
|
))}
|
||||||
</ScrollView>
|
</ScrollView>
|
||||||
<View style={styles.footer}>
|
<View style={styles.footer}>
|
||||||
<PrimaryButton title="Place Order" onPress={() => {}} />
|
<PrimaryButton title="Place Order" onPress={() => navigation.navigate('OrderConfirmedScreen', { orderId: 'ORD-' + Math.floor(Math.random() * 900000 + 100000) })} />
|
||||||
</View>
|
</View>
|
||||||
</View>
|
</View>
|
||||||
);
|
);
|
||||||
|
|||||||
@ -13,7 +13,7 @@ import { getStyles } from './completeProfileScreen.styles';
|
|||||||
import { CustomInput, PrimaryButton } from '@components';
|
import { CustomInput, PrimaryButton } from '@components';
|
||||||
import { useAppTheme } from '@theme';
|
import { useAppTheme } from '@theme';
|
||||||
import { useAppDispatch } from '../../../hooks/useAppDispatch';
|
import { useAppDispatch } from '../../../hooks/useAppDispatch';
|
||||||
import { completeProfile } from '../../../store/authSlice';
|
import { completeProfile } from '../../../store/commonreducers/auth';
|
||||||
import { AuthStackParamList } from '../../../navigation/authStack';
|
import { AuthStackParamList } from '../../../navigation/authStack';
|
||||||
|
|
||||||
type NavProp = StackNavigationProp<AuthStackParamList, 'CompleteProfileScreen'>;
|
type NavProp = StackNavigationProp<AuthStackParamList, 'CompleteProfileScreen'>;
|
||||||
|
|||||||
@ -1,8 +1,13 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { View, Text, ScrollView, TouchableOpacity } from 'react-native';
|
import { View, Text, ScrollView, TouchableOpacity } from 'react-native';
|
||||||
|
import { useNavigation } from '@react-navigation/native';
|
||||||
|
import { StackNavigationProp } from '@react-navigation/stack';
|
||||||
import { getStyles } from './helpSupportScreen.styles';
|
import { getStyles } from './helpSupportScreen.styles';
|
||||||
import { Header } from '@components';
|
import { Header } from '@components';
|
||||||
import { useAppTheme } from '@theme';
|
import { useAppTheme } from '@theme';
|
||||||
|
import { AppStackParamList } from '../../../navigation/appStack';
|
||||||
|
|
||||||
|
type HelpSupportNavProp = StackNavigationProp<AppStackParamList, 'HelpSupportScreen'>;
|
||||||
|
|
||||||
const FAQS = [
|
const FAQS = [
|
||||||
{ q: 'How do I track my order?', a: 'Go to Orders tab and tap on your active order to see tracking.' },
|
{ q: 'How do I track my order?', a: 'Go to Orders tab and tap on your active order to see tracking.' },
|
||||||
@ -14,10 +19,11 @@ const FAQS = [
|
|||||||
export const HelpSupportScreen: React.FC = () => {
|
export const HelpSupportScreen: React.FC = () => {
|
||||||
const { colors } = useAppTheme();
|
const { colors } = useAppTheme();
|
||||||
const styles = getStyles(colors);
|
const styles = getStyles(colors);
|
||||||
|
const navigation = useNavigation<HelpSupportNavProp>();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<View style={styles.container}>
|
<View style={styles.container}>
|
||||||
<Header title="Help & Support" />
|
<Header title="Help & Support" onBack={() => navigation.goBack()} />
|
||||||
<ScrollView contentContainerStyle={styles.content}>
|
<ScrollView contentContainerStyle={styles.content}>
|
||||||
<Text style={styles.sectionTitle}>Frequently Asked Questions</Text>
|
<Text style={styles.sectionTitle}>Frequently Asked Questions</Text>
|
||||||
{FAQS.map((faq, index) => (
|
{FAQS.map((faq, index) => (
|
||||||
|
|||||||
@ -1,20 +1,50 @@
|
|||||||
import { StyleSheet } from 'react-native';
|
import { StyleSheet, Dimensions, Platform } from 'react-native';
|
||||||
import { typography } from '@theme';
|
import { typography } from '@theme';
|
||||||
|
|
||||||
export const getStyles = (colors: any) => StyleSheet.create({
|
const { width } = Dimensions.get('window');
|
||||||
|
const BANNER_WIDTH = width - 32;
|
||||||
|
|
||||||
|
export const getStyles = (colors: any) =>
|
||||||
|
StyleSheet.create({
|
||||||
container: {
|
container: {
|
||||||
flex: 1,
|
flex: 1,
|
||||||
backgroundColor: colors.background,
|
backgroundColor: colors.background,
|
||||||
},
|
},
|
||||||
addressBar: {
|
|
||||||
|
// ---------- Top bar ----------
|
||||||
|
topBar: {
|
||||||
|
flexDirection: 'row',
|
||||||
|
alignItems: 'center',
|
||||||
|
justifyContent: 'space-between',
|
||||||
paddingHorizontal: 16,
|
paddingHorizontal: 16,
|
||||||
paddingTop: 12,
|
paddingTop: 14,
|
||||||
paddingBottom: 4,
|
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: {
|
addressLabel: {
|
||||||
fontSize: typography.fontSize.xs,
|
fontSize: typography.fontSize.xs,
|
||||||
color: colors.textSecondary,
|
color: colors.textSecondary,
|
||||||
marginBottom: 2,
|
marginBottom: 1,
|
||||||
|
letterSpacing: 0.2,
|
||||||
},
|
},
|
||||||
addressRow: {
|
addressRow: {
|
||||||
flexDirection: 'row',
|
flexDirection: 'row',
|
||||||
@ -22,43 +52,337 @@ export const getStyles = (colors: any) => StyleSheet.create({
|
|||||||
},
|
},
|
||||||
addressText: {
|
addressText: {
|
||||||
fontSize: typography.fontSize.md,
|
fontSize: typography.fontSize.md,
|
||||||
fontWeight: typography.fontWeight.semibold,
|
fontWeight: typography.fontWeight.bold,
|
||||||
color: colors.text,
|
color: colors.text,
|
||||||
flex: 1,
|
maxWidth: width * 0.55,
|
||||||
},
|
},
|
||||||
dropdownArrow: {
|
dropdownArrow: {
|
||||||
fontSize: 12,
|
fontSize: 11,
|
||||||
color: colors.textSecondary,
|
color: colors.textSecondary,
|
||||||
marginLeft: 4,
|
marginLeft: 6,
|
||||||
},
|
},
|
||||||
banner: {
|
avatarButton: {
|
||||||
backgroundColor: '#05824C',
|
width: 40,
|
||||||
marginHorizontal: 16,
|
height: 40,
|
||||||
borderRadius: 12,
|
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,
|
padding: 20,
|
||||||
marginVertical: 8,
|
overflow: 'hidden',
|
||||||
|
justifyContent: 'space-between',
|
||||||
|
...Platform.select({
|
||||||
|
ios: {
|
||||||
|
shadowColor: '#000',
|
||||||
|
shadowOffset: { width: 0, height: 6 },
|
||||||
|
shadowOpacity: 0.15,
|
||||||
|
shadowRadius: 12,
|
||||||
|
},
|
||||||
|
android: { elevation: 5 },
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
bannerDecoCircleLg: {
|
||||||
|
position: 'absolute',
|
||||||
|
width: 140,
|
||||||
|
height: 140,
|
||||||
|
borderRadius: 70,
|
||||||
|
backgroundColor: 'rgba(255,255,255,0.10)',
|
||||||
|
top: -50,
|
||||||
|
right: -30,
|
||||||
|
},
|
||||||
|
bannerDecoCircleSm: {
|
||||||
|
position: 'absolute',
|
||||||
|
width: 80,
|
||||||
|
height: 80,
|
||||||
|
borderRadius: 40,
|
||||||
|
backgroundColor: 'rgba(255,255,255,0.08)',
|
||||||
|
bottom: -30,
|
||||||
|
right: 40,
|
||||||
|
},
|
||||||
|
bannerEyebrow: {
|
||||||
|
fontSize: typography.fontSize.xs,
|
||||||
|
fontWeight: typography.fontWeight.semibold,
|
||||||
|
color: 'rgba(255,255,255,0.85)',
|
||||||
|
letterSpacing: 0.6,
|
||||||
|
textTransform: 'uppercase',
|
||||||
},
|
},
|
||||||
bannerText: {
|
bannerText: {
|
||||||
fontSize: typography.fontSize.xxl,
|
fontSize: 26,
|
||||||
fontWeight: typography.fontWeight.bold,
|
fontWeight: typography.fontWeight.bold,
|
||||||
color: '#FFFFFF',
|
color: '#FFFFFF',
|
||||||
|
marginTop: 2,
|
||||||
},
|
},
|
||||||
bannerSubtext: {
|
bannerSubtext: {
|
||||||
fontSize: typography.fontSize.sm,
|
fontSize: typography.fontSize.sm,
|
||||||
color: '#FFFFFF',
|
color: 'rgba(255,255,255,0.9)',
|
||||||
opacity: 0.9,
|
marginTop: 2,
|
||||||
|
},
|
||||||
|
bannerCta: {
|
||||||
|
alignSelf: 'flex-start',
|
||||||
|
backgroundColor: 'rgba(255,255,255,0.95)',
|
||||||
|
paddingHorizontal: 14,
|
||||||
|
paddingVertical: 7,
|
||||||
|
borderRadius: 20,
|
||||||
marginTop: 4,
|
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: {
|
chipRow: {
|
||||||
paddingHorizontal: 16,
|
paddingHorizontal: 16,
|
||||||
paddingVertical: 12,
|
paddingTop: 4,
|
||||||
|
},
|
||||||
|
categoryItem: {
|
||||||
|
alignItems: 'center',
|
||||||
|
marginRight: 18,
|
||||||
|
width: 64,
|
||||||
|
},
|
||||||
|
categoryCircle: {
|
||||||
|
width: 60,
|
||||||
|
height: 60,
|
||||||
|
borderRadius: 18,
|
||||||
|
backgroundColor: colors.surface ?? '#F5F6F8',
|
||||||
|
alignItems: 'center',
|
||||||
|
justifyContent: 'center',
|
||||||
|
marginBottom: 6,
|
||||||
|
borderWidth: 1.5,
|
||||||
|
borderColor: 'transparent',
|
||||||
|
},
|
||||||
|
categoryCircleSelected: {
|
||||||
|
backgroundColor: colors.primaryMuted ?? '#E9F7EF',
|
||||||
|
borderColor: colors.primary ?? '#05824C',
|
||||||
|
},
|
||||||
|
categoryIcon: {
|
||||||
|
fontSize: 24,
|
||||||
|
},
|
||||||
|
categoryLabel: {
|
||||||
|
fontSize: typography.fontSize.xs,
|
||||||
|
color: colors.textSecondary,
|
||||||
|
fontWeight: typography.fontWeight.medium,
|
||||||
|
textAlign: 'center',
|
||||||
|
},
|
||||||
|
categoryLabelSelected: {
|
||||||
|
color: colors.text,
|
||||||
|
fontWeight: typography.fontWeight.bold,
|
||||||
|
},
|
||||||
|
|
||||||
|
// ---------- Section header ----------
|
||||||
|
sectionHeaderRow: {
|
||||||
|
flexDirection: 'row',
|
||||||
|
alignItems: 'center',
|
||||||
|
justifyContent: 'space-between',
|
||||||
|
paddingHorizontal: 16,
|
||||||
|
marginTop: 26,
|
||||||
|
marginBottom: 12,
|
||||||
},
|
},
|
||||||
sectionTitle: {
|
sectionTitle: {
|
||||||
fontSize: typography.fontSize.lg,
|
fontSize: typography.fontSize.lg,
|
||||||
fontWeight: typography.fontWeight.bold,
|
fontWeight: typography.fontWeight.bold,
|
||||||
color: colors.text,
|
color: colors.text,
|
||||||
paddingHorizontal: 16,
|
|
||||||
marginBottom: 8,
|
|
||||||
marginTop: 4,
|
|
||||||
},
|
},
|
||||||
});
|
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,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|||||||
@ -1,30 +1,74 @@
|
|||||||
import React, { useState, useEffect, useCallback } from 'react';
|
import React, { useState, useEffect, useCallback, useRef } from 'react';
|
||||||
import {
|
import {
|
||||||
View,
|
View,
|
||||||
Text,
|
Text,
|
||||||
ScrollView,
|
ScrollView,
|
||||||
TouchableOpacity,
|
TouchableOpacity,
|
||||||
FlatList,
|
FlatList,
|
||||||
|
Image,
|
||||||
|
NativeSyntheticEvent,
|
||||||
|
NativeScrollEvent,
|
||||||
|
Dimensions,
|
||||||
} from 'react-native';
|
} from 'react-native';
|
||||||
import { useNavigation } from '@react-navigation/native';
|
import {
|
||||||
|
useNavigation,
|
||||||
|
CompositeNavigationProp,
|
||||||
|
} from '@react-navigation/native';
|
||||||
import { BottomTabNavigationProp } from '@react-navigation/bottom-tabs';
|
import { BottomTabNavigationProp } from '@react-navigation/bottom-tabs';
|
||||||
|
import { StackNavigationProp } from '@react-navigation/stack';
|
||||||
import { getStyles } from './homeScreen.styles';
|
import { getStyles } from './homeScreen.styles';
|
||||||
import { SearchBar, ProviderCard, CategoryChip } from '@components';
|
import { ProviderCard, SearchBar } from '@components';
|
||||||
import { useAppTheme } from '@theme';
|
import { useAppTheme } from '@theme';
|
||||||
import { deliveryService } from '../../../services/deliveryService';
|
import { getProvidersApi } from '../../../api/deliveryApi';
|
||||||
import { Provider } from '../../../interfaces';
|
import { Provider } from '../../../interfaces';
|
||||||
import { AppStackParamList } from '../../../navigation/appStack';
|
import { AppStackParamList } from '../../../navigation/appStack';
|
||||||
|
import { MainTabParamList } from '../../../navigation/mainTabNavigator';
|
||||||
|
|
||||||
type NavProp = BottomTabNavigationProp<AppStackParamList, 'HomeScreen'>;
|
type NavProp = CompositeNavigationProp<
|
||||||
|
BottomTabNavigationProp<MainTabParamList, 'HomeScreen'>,
|
||||||
|
StackNavigationProp<AppStackParamList>
|
||||||
|
>;
|
||||||
|
|
||||||
|
const { width } = Dimensions.get('window');
|
||||||
|
const BANNER_STEP = width - 32 + 12; // card width + margin
|
||||||
|
|
||||||
const CATEGORIES = [
|
const CATEGORIES = [
|
||||||
{ key: 'all', icon: '🏠', label: 'All' },
|
{ key: 'all', icon: '🏠', label: 'All' },
|
||||||
{ key: 'Food', icon: '🍔', label: 'Food' },
|
{ key: 'Food', icon: '🍔', label: 'Food' },
|
||||||
{ key: 'Groceries', icon: '🛒', label: 'Groceries' },
|
{ key: 'Groceries', icon: '🛒', label: 'Grocery' },
|
||||||
{ key: 'Pharmacy', icon: '💊', label: 'Pharmacy' },
|
{ key: 'Pharmacy', icon: '💊', label: 'Pharmacy' },
|
||||||
|
{ key: 'Meat', icon: '🥩', label: 'Meat' },
|
||||||
|
{ key: 'Flowers', icon: '💐', label: 'Flowers' },
|
||||||
{ key: 'More', icon: '📦', label: 'More' },
|
{ key: 'More', icon: '📦', label: 'More' },
|
||||||
];
|
];
|
||||||
|
|
||||||
|
const PROMOS = [
|
||||||
|
{
|
||||||
|
key: 'promo1',
|
||||||
|
colors: ['#05824C', '#0AA25E'],
|
||||||
|
eyebrow: 'Limited time',
|
||||||
|
title: '50% OFF',
|
||||||
|
subtitle: 'On your first order, up to ₹150',
|
||||||
|
cta: 'Order now',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'promo2',
|
||||||
|
colors: ['#7B3FF2', '#9B6BF5'],
|
||||||
|
eyebrow: 'Free delivery',
|
||||||
|
title: '₹0 Delivery',
|
||||||
|
subtitle: 'On orders above ₹299 today',
|
||||||
|
cta: 'Explore',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'promo3',
|
||||||
|
colors: ['#E85D3D', '#F0805F'],
|
||||||
|
eyebrow: 'Pharmacy',
|
||||||
|
title: 'Meds in 20 min',
|
||||||
|
subtitle: 'Genuine medicines, fast delivery',
|
||||||
|
cta: 'Shop now',
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
export const HomeScreen: React.FC = () => {
|
export const HomeScreen: React.FC = () => {
|
||||||
const { colors } = useAppTheme();
|
const { colors } = useAppTheme();
|
||||||
const styles = getStyles(colors);
|
const styles = getStyles(colors);
|
||||||
@ -32,62 +76,175 @@ export const HomeScreen: React.FC = () => {
|
|||||||
|
|
||||||
const [providers, setProviders] = useState<Provider[]>([]);
|
const [providers, setProviders] = useState<Provider[]>([]);
|
||||||
const [selectedCategory, setSelectedCategory] = useState('all');
|
const [selectedCategory, setSelectedCategory] = useState('all');
|
||||||
|
const [activeBanner, setActiveBanner] = useState(0);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
deliveryService.getProviders().then(setProviders);
|
getProvidersApi().then(setProviders);
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const filteredProviders = selectedCategory === 'all'
|
const filteredProviders =
|
||||||
|
selectedCategory === 'all'
|
||||||
? providers
|
? providers
|
||||||
: providers.filter((p) => p.tag === selectedCategory);
|
: providers.filter(p => p.tag === selectedCategory);
|
||||||
|
|
||||||
const handleSearchFocus = useCallback(() => {
|
const handleSearchFocus = useCallback(() => {
|
||||||
navigation.navigate('SearchScreen');
|
navigation.navigate('SearchScreen');
|
||||||
}, [navigation]);
|
}, [navigation]);
|
||||||
|
|
||||||
|
const handleBannerScroll = useCallback(
|
||||||
|
(e: NativeSyntheticEvent<NativeScrollEvent>) => {
|
||||||
|
const index = Math.round(e.nativeEvent.contentOffset.x / BANNER_STEP);
|
||||||
|
setActiveBanner(index);
|
||||||
|
},
|
||||||
|
[],
|
||||||
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ScrollView style={styles.container} showsVerticalScrollIndicator={false}>
|
<ScrollView style={styles.container} showsVerticalScrollIndicator={false}>
|
||||||
<View style={styles.addressBar}>
|
{/* Top bar */}
|
||||||
|
<View style={styles.topBar}>
|
||||||
|
<TouchableOpacity style={styles.addressBar} activeOpacity={0.7}>
|
||||||
|
<View style={styles.pinBadge}>
|
||||||
|
<Text style={styles.pinEmoji}>📍</Text>
|
||||||
|
</View>
|
||||||
|
<View style={styles.addressTextWrap}>
|
||||||
<Text style={styles.addressLabel}>Deliver to</Text>
|
<Text style={styles.addressLabel}>Deliver to</Text>
|
||||||
<TouchableOpacity style={styles.addressRow} activeOpacity={0.7}>
|
<View style={styles.addressRow}>
|
||||||
<Text style={styles.addressText}>Koramangala 4th Block, B... </Text>
|
<Text style={styles.addressText} numberOfLines={1}>
|
||||||
|
Koramangala 4th Block
|
||||||
|
</Text>
|
||||||
<Text style={styles.dropdownArrow}>▼</Text>
|
<Text style={styles.dropdownArrow}>▼</Text>
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
</TouchableOpacity>
|
||||||
|
|
||||||
|
<TouchableOpacity style={styles.avatarButton} activeOpacity={0.7}>
|
||||||
|
<Text style={styles.avatarEmoji}>🔔</Text>
|
||||||
|
<View style={styles.notifDot} />
|
||||||
</TouchableOpacity>
|
</TouchableOpacity>
|
||||||
</View>
|
</View>
|
||||||
|
|
||||||
|
{/* Search */}
|
||||||
|
<View style={styles.searchWrap}>
|
||||||
<SearchBar
|
<SearchBar
|
||||||
value=""
|
value=""
|
||||||
onChangeText={() => {}}
|
onChangeText={() => {}}
|
||||||
placeholder="Search providers or items..."
|
placeholder="Search providers or items..."
|
||||||
onFocus={handleSearchFocus}
|
onFocus={handleSearchFocus}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<View style={styles.banner}>
|
|
||||||
<Text style={styles.bannerText}>50% OFF</Text>
|
|
||||||
<Text style={styles.bannerSubtext}>on your first order</Text>
|
|
||||||
</View>
|
</View>
|
||||||
|
|
||||||
|
{/* Promo carousel */}
|
||||||
|
<FlatList
|
||||||
|
data={PROMOS}
|
||||||
|
horizontal
|
||||||
|
keyExtractor={item => item.key}
|
||||||
|
showsHorizontalScrollIndicator={false}
|
||||||
|
contentContainerStyle={styles.bannerListContent}
|
||||||
|
snapToInterval={BANNER_STEP}
|
||||||
|
decelerationRate="fast"
|
||||||
|
onScroll={handleBannerScroll}
|
||||||
|
scrollEventThrottle={16}
|
||||||
|
renderItem={({ item }) => (
|
||||||
|
<View
|
||||||
|
style={[styles.bannerCard, { backgroundColor: item.colors[0] }]}
|
||||||
|
>
|
||||||
|
<View style={styles.bannerDecoCircleLg} />
|
||||||
|
<View style={styles.bannerDecoCircleSm} />
|
||||||
|
<View>
|
||||||
|
<Text style={styles.bannerEyebrow}>{item.eyebrow}</Text>
|
||||||
|
<Text style={styles.bannerText}>{item.title}</Text>
|
||||||
|
<Text style={styles.bannerSubtext}>{item.subtitle}</Text>
|
||||||
|
</View>
|
||||||
|
<View style={styles.bannerCta}>
|
||||||
|
<Text style={styles.bannerCtaText}>{item.cta}</Text>
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
<View style={styles.dotsRow}>
|
||||||
|
{PROMOS.map((_, i) => (
|
||||||
|
<View
|
||||||
|
key={i}
|
||||||
|
style={[styles.dot, i === activeBanner && styles.dotActive]}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</View>
|
||||||
|
|
||||||
|
{/* Categories */}
|
||||||
|
<View style={styles.categorySection}>
|
||||||
<FlatList
|
<FlatList
|
||||||
horizontal
|
horizontal
|
||||||
data={CATEGORIES}
|
data={CATEGORIES}
|
||||||
keyExtractor={(item) => item.key}
|
keyExtractor={item => item.key}
|
||||||
showsHorizontalScrollIndicator={false}
|
showsHorizontalScrollIndicator={false}
|
||||||
contentContainerStyle={styles.chipRow}
|
contentContainerStyle={styles.chipRow}
|
||||||
renderItem={({ item }) => (
|
renderItem={({ item }) => {
|
||||||
<CategoryChip
|
const isSelected = selectedCategory === item.key;
|
||||||
icon={item.icon}
|
return (
|
||||||
label={item.label}
|
<TouchableOpacity
|
||||||
isSelected={selectedCategory === item.key}
|
style={styles.categoryItem}
|
||||||
onPress={() => setSelectedCategory(item.key)}
|
activeOpacity={0.75}
|
||||||
/>
|
onPress={() => {
|
||||||
)}
|
setSelectedCategory(item.key);
|
||||||
/>
|
if (item.key !== 'all') {
|
||||||
|
navigation.navigate('ProviderListScreen', {
|
||||||
<Text style={styles.sectionTitle}>
|
category: item.key,
|
||||||
{selectedCategory === 'all' ? 'Popular Providers' : selectedCategory}
|
});
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<View
|
||||||
|
style={[
|
||||||
|
styles.categoryCircle,
|
||||||
|
isSelected && styles.categoryCircleSelected,
|
||||||
|
]}
|
||||||
|
>
|
||||||
|
<Text style={styles.categoryIcon}>{item.icon}</Text>
|
||||||
|
</View>
|
||||||
|
<Text
|
||||||
|
style={[
|
||||||
|
styles.categoryLabel,
|
||||||
|
isSelected && styles.categoryLabelSelected,
|
||||||
|
]}
|
||||||
|
numberOfLines={1}
|
||||||
|
>
|
||||||
|
{item.label}
|
||||||
</Text>
|
</Text>
|
||||||
|
</TouchableOpacity>
|
||||||
|
);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</View>
|
||||||
|
|
||||||
{filteredProviders.map((provider) => (
|
{/* Section header */}
|
||||||
|
<View style={styles.sectionHeaderRow}>
|
||||||
|
<View>
|
||||||
|
<Text style={styles.sectionTitle}>
|
||||||
|
{selectedCategory === 'all' ? 'Popular near you' : selectedCategory}
|
||||||
|
</Text>
|
||||||
|
<Text style={styles.sectionSubtitle}>
|
||||||
|
{filteredProviders.length} place
|
||||||
|
{filteredProviders.length === 1 ? '' : 's'} delivering to you
|
||||||
|
</Text>
|
||||||
|
</View>
|
||||||
|
<TouchableOpacity activeOpacity={0.7}>
|
||||||
|
<Text style={styles.seeAllText}>See all</Text>
|
||||||
|
</TouchableOpacity>
|
||||||
|
</View>
|
||||||
|
|
||||||
|
{/* Provider list */}
|
||||||
|
<View style={styles.providerList}>
|
||||||
|
{filteredProviders.length === 0 && (
|
||||||
|
<View style={styles.emptyWrap}>
|
||||||
|
<Text style={styles.emptyEmoji}>🍽️</Text>
|
||||||
|
<Text style={styles.emptyText}>
|
||||||
|
No providers in this category yet
|
||||||
|
</Text>
|
||||||
|
</View>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{filteredProviders.map(provider => (
|
||||||
<ProviderCard
|
<ProviderCard
|
||||||
key={provider.id}
|
key={provider.id}
|
||||||
imageUrl={provider.imageUrl}
|
imageUrl={provider.imageUrl}
|
||||||
@ -96,9 +253,14 @@ export const HomeScreen: React.FC = () => {
|
|||||||
deliveryTime={provider.deliveryTime}
|
deliveryTime={provider.deliveryTime}
|
||||||
tag={provider.tag}
|
tag={provider.tag}
|
||||||
discountText={provider.discountText}
|
discountText={provider.discountText}
|
||||||
onPress={() => navigation.navigate('SearchScreen')}
|
onPress={() =>
|
||||||
|
navigation.navigate('ProviderDetailsScreen', {
|
||||||
|
providerId: provider.id,
|
||||||
|
})
|
||||||
|
}
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
|
</View>
|
||||||
|
|
||||||
<View style={{ height: 24 }} />
|
<View style={{ height: 24 }} />
|
||||||
</ScrollView>
|
</ScrollView>
|
||||||
|
|||||||
@ -1,4 +1,4 @@
|
|||||||
export * from './loginScreen';
|
export * from './LoginScreen';
|
||||||
export * from './otpScreen';
|
export * from './otpScreen';
|
||||||
export * from './setLocationScreen';
|
export * from './setLocationScreen';
|
||||||
export * from './completeProfileScreen';
|
export * from './completeProfileScreen';
|
||||||
|
|||||||
@ -1,27 +1,41 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { View, Text } from 'react-native';
|
import { View, Text } from 'react-native';
|
||||||
|
import { useNavigation, useRoute, RouteProp } from '@react-navigation/native';
|
||||||
|
import { StackNavigationProp } from '@react-navigation/stack';
|
||||||
import { getStyles } from './liveTrackingScreen.styles';
|
import { getStyles } from './liveTrackingScreen.styles';
|
||||||
import { Header, PrimaryButton } from '@components';
|
import { Header, PrimaryButton } from '@components';
|
||||||
import { useAppTheme } from '@theme';
|
import { useAppTheme } from '@theme';
|
||||||
|
import { AppStackParamList } from '../../../navigation/appStack';
|
||||||
|
|
||||||
|
type LiveTrackingNavProp = StackNavigationProp<AppStackParamList, 'LiveTrackingScreen'>;
|
||||||
|
type LiveTrackingRouteProp = RouteProp<AppStackParamList, 'LiveTrackingScreen'>;
|
||||||
|
|
||||||
export const LiveTrackingScreen: React.FC = () => {
|
export const LiveTrackingScreen: React.FC = () => {
|
||||||
const { colors } = useAppTheme();
|
const { colors } = useAppTheme();
|
||||||
const styles = getStyles(colors);
|
const styles = getStyles(colors);
|
||||||
|
const navigation = useNavigation<LiveTrackingNavProp>();
|
||||||
|
const route = useRoute<LiveTrackingRouteProp>();
|
||||||
|
const orderId = route.params?.orderId || 'ORD-123456';
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<View style={styles.container}>
|
<View style={styles.container}>
|
||||||
<Header title="Live Tracking" onBack={() => {}} />
|
<Header title="Live Tracking" onBack={() => navigation.goBack()} />
|
||||||
<View style={styles.mapPlaceholder}>
|
<View style={styles.mapPlaceholder}>
|
||||||
<Text style={styles.mapIcon}>🗺️</Text>
|
<Text style={styles.mapIcon}>🗺️</Text>
|
||||||
<Text style={styles.mapText}>Live Map</Text>
|
<Text style={styles.mapText}>Live Map</Text>
|
||||||
<Text style={styles.mapSubtext}>
|
<Text style={styles.mapSubtext}>
|
||||||
Delivery partner location tracking
|
Delivery partner location tracking for {orderId}
|
||||||
</Text>
|
</Text>
|
||||||
<View style={styles.blinkingBadge}>
|
<View style={styles.blinkingBadge}>
|
||||||
<Text style={styles.blinkingText}>● Live</Text>
|
<Text style={styles.blinkingText}>● Live</Text>
|
||||||
</View>
|
</View>
|
||||||
</View>
|
</View>
|
||||||
<View style={styles.agentSheet}>
|
<View style={styles.agentSheet}>
|
||||||
|
<PrimaryButton
|
||||||
|
title="Simulate Order Delivered"
|
||||||
|
onPress={() => navigation.navigate('OrderDeliveredScreen', { orderId })}
|
||||||
|
style={{ marginBottom: 16 }}
|
||||||
|
/>
|
||||||
<Text style={styles.agentName}>Rahul Sharma</Text>
|
<Text style={styles.agentName}>Rahul Sharma</Text>
|
||||||
<Text style={styles.agentRating}>⭐ 4.8</Text>
|
<Text style={styles.agentRating}>⭐ 4.8</Text>
|
||||||
<Text style={styles.agentVehicle}>KA-01-AB-1234 • Honda Activa</Text>
|
<Text style={styles.agentVehicle}>KA-01-AB-1234 • Honda Activa</Text>
|
||||||
|
|||||||
@ -5,20 +5,31 @@ import {
|
|||||||
FlatList,
|
FlatList,
|
||||||
TouchableOpacity,
|
TouchableOpacity,
|
||||||
} from 'react-native';
|
} from 'react-native';
|
||||||
|
import { useNavigation, CompositeNavigationProp } from '@react-navigation/native';
|
||||||
|
import { BottomTabNavigationProp } from '@react-navigation/bottom-tabs';
|
||||||
|
import { StackNavigationProp } from '@react-navigation/stack';
|
||||||
import { getStyles } from './myOrdersScreen.styles';
|
import { getStyles } from './myOrdersScreen.styles';
|
||||||
import { Header, OrderHistoryCard } from '@components';
|
import { Header, OrderHistoryCard } from '@components';
|
||||||
import { useAppTheme } from '@theme';
|
import { useAppTheme } from '@theme';
|
||||||
|
import { AppStackParamList } from '../../../navigation/appStack';
|
||||||
|
import { MainTabParamList } from '../../../navigation/mainTabNavigator';
|
||||||
|
|
||||||
|
type MyOrdersNavProp = CompositeNavigationProp<
|
||||||
|
BottomTabNavigationProp<MainTabParamList, 'MyOrdersScreen'>,
|
||||||
|
StackNavigationProp<AppStackParamList>
|
||||||
|
>;
|
||||||
|
|
||||||
const TABS = ['All', 'Ongoing', 'Completed'];
|
const TABS = ['All', 'Ongoing', 'Completed'];
|
||||||
const mockOrders = [
|
const mockOrders = [
|
||||||
{ id: '1', providerName: 'Pizza Planet', providerImage: '', orderDate: '29 Jun 2026', status: 'Delivered', total: 599 },
|
{ id: 'ORD-591283', providerName: 'Pizza Planet', providerImage: '', orderDate: '29 Jun 2026', status: 'Delivered', total: 599 },
|
||||||
{ id: '2', providerName: 'Fresh Mart', providerImage: '', orderDate: '28 Jun 2026', status: 'Ongoing', total: 349 },
|
{ id: 'ORD-771239', providerName: 'Fresh Mart', providerImage: '', orderDate: '28 Jun 2026', status: 'Ongoing', total: 349 },
|
||||||
{ id: '3', providerName: 'Burger Barn', providerImage: '', orderDate: '27 Jun 2026', status: 'Delivered', total: 449 },
|
{ id: 'ORD-108239', providerName: 'Burger Barn', providerImage: '', orderDate: '27 Jun 2026', status: 'Delivered', total: 449 },
|
||||||
];
|
];
|
||||||
|
|
||||||
export const MyOrdersScreen: React.FC = () => {
|
export const MyOrdersScreen: React.FC = () => {
|
||||||
const { colors } = useAppTheme();
|
const { colors } = useAppTheme();
|
||||||
const styles = getStyles(colors);
|
const styles = getStyles(colors);
|
||||||
|
const navigation = useNavigation<MyOrdersNavProp>();
|
||||||
const [activeTab, setActiveTab] = useState('All');
|
const [activeTab, setActiveTab] = useState('All');
|
||||||
|
|
||||||
const filteredOrders = activeTab === 'All'
|
const filteredOrders = activeTab === 'All'
|
||||||
@ -62,8 +73,19 @@ export const MyOrdersScreen: React.FC = () => {
|
|||||||
orderDate={item.orderDate}
|
orderDate={item.orderDate}
|
||||||
status={item.status}
|
status={item.status}
|
||||||
total={item.total}
|
total={item.total}
|
||||||
onReorder={() => {}}
|
onReorder={() => {
|
||||||
onDetails={() => {}}
|
navigation.navigate('ProviderDetailsScreen', {
|
||||||
|
providerId: 'p1',
|
||||||
|
providerName: item.providerName,
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
onDetails={() => {
|
||||||
|
if (item.status === 'Ongoing') {
|
||||||
|
navigation.navigate('OrderTrackingScreen', { orderId: item.id });
|
||||||
|
} else {
|
||||||
|
navigation.navigate('OrderDeliveredScreen', { orderId: item.id });
|
||||||
|
}
|
||||||
|
}}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
contentContainerStyle={styles.list}
|
contentContainerStyle={styles.list}
|
||||||
|
|||||||
@ -4,7 +4,7 @@ import { getStyles } from './onboardingCompleteScreen.styles';
|
|||||||
import { PrimaryButton } from '@components';
|
import { PrimaryButton } from '@components';
|
||||||
import { useAppTheme } from '@theme';
|
import { useAppTheme } from '@theme';
|
||||||
import { useAppDispatch } from '../../../hooks/useAppDispatch';
|
import { useAppDispatch } from '../../../hooks/useAppDispatch';
|
||||||
import { completeOnboarding } from '../../../store/authSlice';
|
import { completeOnboarding } from '../../../store/commonreducers/auth';
|
||||||
|
|
||||||
export const OnboardingCompleteScreen: React.FC = () => {
|
export const OnboardingCompleteScreen: React.FC = () => {
|
||||||
const { colors } = useAppTheme();
|
const { colors } = useAppTheme();
|
||||||
|
|||||||
@ -1,16 +1,25 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { View, Text } from 'react-native';
|
import { View, Text } from 'react-native';
|
||||||
|
import { useNavigation, useRoute, RouteProp } from '@react-navigation/native';
|
||||||
|
import { StackNavigationProp } from '@react-navigation/stack';
|
||||||
import { getStyles } from './orderConfirmedScreen.styles';
|
import { getStyles } from './orderConfirmedScreen.styles';
|
||||||
import { Header, PrimaryButton } from '@components';
|
import { Header, PrimaryButton } from '@components';
|
||||||
import { useAppTheme } from '@theme';
|
import { useAppTheme } from '@theme';
|
||||||
|
import { AppStackParamList } from '../../../navigation/appStack';
|
||||||
|
|
||||||
|
type OrderConfirmedNavProp = StackNavigationProp<AppStackParamList, 'OrderConfirmedScreen'>;
|
||||||
|
type OrderConfirmedRouteProp = RouteProp<AppStackParamList, 'OrderConfirmedScreen'>;
|
||||||
|
|
||||||
export const OrderConfirmedScreen: React.FC = () => {
|
export const OrderConfirmedScreen: React.FC = () => {
|
||||||
const { colors } = useAppTheme();
|
const { colors } = useAppTheme();
|
||||||
const styles = getStyles(colors);
|
const styles = getStyles(colors);
|
||||||
|
const navigation = useNavigation<OrderConfirmedNavProp>();
|
||||||
|
const route = useRoute<OrderConfirmedRouteProp>();
|
||||||
|
const orderId = route.params?.orderId || 'ORD-123456';
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<View style={styles.container}>
|
<View style={styles.container}>
|
||||||
<Header title="Order Confirmed" onBack={() => {}} />
|
<Header title="Order Confirmed" onBack={() => navigation.navigate('MainTabs')} />
|
||||||
<View style={styles.content}>
|
<View style={styles.content}>
|
||||||
<Text style={styles.icon}>✅</Text>
|
<Text style={styles.icon}>✅</Text>
|
||||||
<Text style={styles.title}>Order Confirmed!</Text>
|
<Text style={styles.title}>Order Confirmed!</Text>
|
||||||
@ -18,12 +27,12 @@ export const OrderConfirmedScreen: React.FC = () => {
|
|||||||
Your order has been placed successfully
|
Your order has been placed successfully
|
||||||
</Text>
|
</Text>
|
||||||
<View style={styles.orderCard}>
|
<View style={styles.orderCard}>
|
||||||
<Text style={styles.orderId}>Order ID: ORD-123456</Text>
|
<Text style={styles.orderId}>Order ID: {orderId}</Text>
|
||||||
<Text style={styles.orderEta}>Estimated delivery: 25-30 min</Text>
|
<Text style={styles.orderEta}>Estimated delivery: 25-30 min</Text>
|
||||||
</View>
|
</View>
|
||||||
</View>
|
</View>
|
||||||
<View style={styles.footer}>
|
<View style={styles.footer}>
|
||||||
<PrimaryButton title="View Tracking" onPress={() => {}} />
|
<PrimaryButton title="View Tracking" onPress={() => navigation.navigate('OrderTrackingScreen', { orderId })} />
|
||||||
</View>
|
</View>
|
||||||
</View>
|
</View>
|
||||||
);
|
);
|
||||||
|
|||||||
@ -1,17 +1,23 @@
|
|||||||
import React, { useState } from 'react';
|
import React, { useState } from 'react';
|
||||||
import { View, Text } from 'react-native';
|
import { View, Text } from 'react-native';
|
||||||
|
import { useNavigation } from '@react-navigation/native';
|
||||||
|
import { StackNavigationProp } from '@react-navigation/stack';
|
||||||
import { getStyles } from './orderDeliveredScreen.styles';
|
import { getStyles } from './orderDeliveredScreen.styles';
|
||||||
import { Header, RatingStars, PrimaryButton } from '@components';
|
import { Header, RatingStars, PrimaryButton } from '@components';
|
||||||
import { useAppTheme } from '@theme';
|
import { useAppTheme } from '@theme';
|
||||||
|
import { AppStackParamList } from '../../../navigation/appStack';
|
||||||
|
|
||||||
|
type OrderDeliveredNavProp = StackNavigationProp<AppStackParamList, 'OrderDeliveredScreen'>;
|
||||||
|
|
||||||
export const OrderDeliveredScreen: React.FC = () => {
|
export const OrderDeliveredScreen: React.FC = () => {
|
||||||
const { colors } = useAppTheme();
|
const { colors } = useAppTheme();
|
||||||
const styles = getStyles(colors);
|
const styles = getStyles(colors);
|
||||||
|
const navigation = useNavigation<OrderDeliveredNavProp>();
|
||||||
const [rating, setRating] = useState(0);
|
const [rating, setRating] = useState(0);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<View style={styles.container}>
|
<View style={styles.container}>
|
||||||
<Header title="Order Delivered" onBack={() => {}} />
|
<Header title="Order Delivered" onBack={() => navigation.navigate('MainTabs')} />
|
||||||
<View style={styles.content}>
|
<View style={styles.content}>
|
||||||
<Text style={styles.icon}>🎉</Text>
|
<Text style={styles.icon}>🎉</Text>
|
||||||
<Text style={styles.title}>Your order has been delivered!</Text>
|
<Text style={styles.title}>Your order has been delivered!</Text>
|
||||||
@ -21,7 +27,7 @@ export const OrderDeliveredScreen: React.FC = () => {
|
|||||||
|
|
||||||
<PrimaryButton
|
<PrimaryButton
|
||||||
title="Rate & Tip"
|
title="Rate & Tip"
|
||||||
onPress={() => {}}
|
onPress={() => navigation.navigate('MainTabs')}
|
||||||
disabled={rating === 0}
|
disabled={rating === 0}
|
||||||
style={{ marginTop: 32 }}
|
style={{ marginTop: 32 }}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@ -1,8 +1,14 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { View, Text } from 'react-native';
|
import { View, Text } from 'react-native';
|
||||||
|
import { useNavigation, useRoute, RouteProp } from '@react-navigation/native';
|
||||||
|
import { StackNavigationProp } from '@react-navigation/stack';
|
||||||
import { getStyles } from './orderTrackingScreen.styles';
|
import { getStyles } from './orderTrackingScreen.styles';
|
||||||
import { Header, PrimaryButton } from '@components';
|
import { Header, PrimaryButton } from '@components';
|
||||||
import { useAppTheme } from '@theme';
|
import { useAppTheme } from '@theme';
|
||||||
|
import { AppStackParamList } from '../../../navigation/appStack';
|
||||||
|
|
||||||
|
type OrderTrackingNavProp = StackNavigationProp<AppStackParamList, 'OrderTrackingScreen'>;
|
||||||
|
type OrderTrackingRouteProp = RouteProp<AppStackParamList, 'OrderTrackingScreen'>;
|
||||||
|
|
||||||
const MILESTONES = [
|
const MILESTONES = [
|
||||||
{ key: 'placed', label: 'Order Placed', time: '12:30 PM' },
|
{ key: 'placed', label: 'Order Placed', time: '12:30 PM' },
|
||||||
@ -14,10 +20,13 @@ const MILESTONES = [
|
|||||||
export const OrderTrackingScreen: React.FC = () => {
|
export const OrderTrackingScreen: React.FC = () => {
|
||||||
const { colors } = useAppTheme();
|
const { colors } = useAppTheme();
|
||||||
const styles = getStyles(colors);
|
const styles = getStyles(colors);
|
||||||
|
const navigation = useNavigation<OrderTrackingNavProp>();
|
||||||
|
const route = useRoute<OrderTrackingRouteProp>();
|
||||||
|
const orderId = route.params?.orderId || 'ORD-123456';
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<View style={styles.container}>
|
<View style={styles.container}>
|
||||||
<Header title="Track Order" onBack={() => {}} />
|
<Header title={`Track Order (${orderId})`} onBack={() => navigation.goBack()} />
|
||||||
<View style={styles.content}>
|
<View style={styles.content}>
|
||||||
<View style={styles.timeline}>
|
<View style={styles.timeline}>
|
||||||
{MILESTONES.map((milestone, index) => (
|
{MILESTONES.map((milestone, index) => (
|
||||||
@ -54,9 +63,14 @@ export const OrderTrackingScreen: React.FC = () => {
|
|||||||
))}
|
))}
|
||||||
</View>
|
</View>
|
||||||
<View style={styles.supportCard}>
|
<View style={styles.supportCard}>
|
||||||
|
<PrimaryButton
|
||||||
|
title="View Live Map"
|
||||||
|
onPress={() => navigation.navigate('LiveTrackingScreen', { orderId })}
|
||||||
|
style={{ marginBottom: 16 }}
|
||||||
|
/>
|
||||||
<Text style={styles.supportTitle}>Need help?</Text>
|
<Text style={styles.supportTitle}>Need help?</Text>
|
||||||
<Text style={styles.supportSubtext}>Contact support for assistance</Text>
|
<Text style={styles.supportSubtext}>Contact support for assistance</Text>
|
||||||
<PrimaryButton title="Contact Support" onPress={() => {}} />
|
<PrimaryButton title="Contact Support" onPress={() => navigation.navigate('HelpSupportScreen')} />
|
||||||
</View>
|
</View>
|
||||||
</View>
|
</View>
|
||||||
</View>
|
</View>
|
||||||
|
|||||||
@ -12,7 +12,7 @@ import { getStyles } from './otpScreen.styles';
|
|||||||
import { PrimaryButton } from '@components';
|
import { PrimaryButton } from '@components';
|
||||||
import { useAppTheme } from '@theme';
|
import { useAppTheme } from '@theme';
|
||||||
import { useAppDispatch } from '../../../hooks/useAppDispatch';
|
import { useAppDispatch } from '../../../hooks/useAppDispatch';
|
||||||
import { verifyOtp } from '../../../store/authSlice';
|
import { verifyOtp } from '../../../store/commonreducers/auth';
|
||||||
import { AuthStackParamList } from '../../../navigation/authStack';
|
import { AuthStackParamList } from '../../../navigation/authStack';
|
||||||
|
|
||||||
type OtpNavProp = StackNavigationProp<AuthStackParamList, 'OtpScreen'>;
|
type OtpNavProp = StackNavigationProp<AuthStackParamList, 'OtpScreen'>;
|
||||||
|
|||||||
@ -1,7 +1,11 @@
|
|||||||
import { StyleSheet } from 'react-native';
|
import { StyleSheet, Dimensions, Platform } from 'react-native';
|
||||||
import { typography } from '@theme';
|
import { typography } from '@theme';
|
||||||
|
|
||||||
export const getStyles = (colors: any) => StyleSheet.create({
|
const { width } = Dimensions.get('window');
|
||||||
|
const HERO_HEIGHT = 220;
|
||||||
|
|
||||||
|
export const getStyles = (colors: any) =>
|
||||||
|
StyleSheet.create({
|
||||||
container: {
|
container: {
|
||||||
flex: 1,
|
flex: 1,
|
||||||
backgroundColor: colors.background,
|
backgroundColor: colors.background,
|
||||||
@ -9,87 +13,333 @@ export const getStyles = (colors: any) => StyleSheet.create({
|
|||||||
content: {
|
content: {
|
||||||
flex: 1,
|
flex: 1,
|
||||||
},
|
},
|
||||||
hero: {
|
contentBody: {
|
||||||
alignItems: 'center',
|
paddingBottom: 32,
|
||||||
paddingVertical: 24,
|
},
|
||||||
borderBottomWidth: 1,
|
|
||||||
borderBottomColor: colors.border,
|
// ---------- Hero ----------
|
||||||
|
heroWrap: {
|
||||||
|
width,
|
||||||
|
height: HERO_HEIGHT,
|
||||||
|
backgroundColor: colors.inputBg,
|
||||||
},
|
},
|
||||||
heroImage: {
|
heroImage: {
|
||||||
width: 100,
|
width: '100%',
|
||||||
height: 100,
|
height: '100%',
|
||||||
borderRadius: 50,
|
|
||||||
backgroundColor: colors.inputBg,
|
|
||||||
justifyContent: 'center',
|
|
||||||
alignItems: 'center',
|
|
||||||
marginBottom: 12,
|
|
||||||
},
|
},
|
||||||
heroEmoji: {
|
heroFallback: {
|
||||||
fontSize: 44,
|
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: {
|
heroName: {
|
||||||
fontSize: typography.fontSize.xl,
|
fontSize: typography.fontSize.xl,
|
||||||
fontWeight: typography.fontWeight.bold,
|
fontWeight: typography.fontWeight.bold,
|
||||||
color: colors.text,
|
color: colors.text,
|
||||||
marginBottom: 8,
|
flex: 1,
|
||||||
|
marginRight: 12,
|
||||||
},
|
},
|
||||||
heroStats: {
|
ratingBadge: {
|
||||||
flexDirection: 'row',
|
flexDirection: 'row',
|
||||||
gap: 16,
|
alignItems: 'center',
|
||||||
|
backgroundColor: colors.primary,
|
||||||
|
borderRadius: 8,
|
||||||
|
paddingHorizontal: 8,
|
||||||
|
paddingVertical: 5,
|
||||||
},
|
},
|
||||||
heroStat: {
|
ratingBadgeText: {
|
||||||
|
fontSize: typography.fontSize.sm,
|
||||||
|
fontWeight: typography.fontWeight.bold,
|
||||||
|
color: '#FFFFFF',
|
||||||
|
marginLeft: 3,
|
||||||
|
},
|
||||||
|
cuisineText: {
|
||||||
|
fontSize: typography.fontSize.sm,
|
||||||
|
color: colors.textSecondary,
|
||||||
|
marginTop: 4,
|
||||||
|
},
|
||||||
|
|
||||||
|
metaRow: {
|
||||||
|
flexDirection: 'row',
|
||||||
|
alignItems: 'center',
|
||||||
|
marginTop: 12,
|
||||||
|
},
|
||||||
|
metaItem: {
|
||||||
|
flexDirection: 'row',
|
||||||
|
alignItems: 'center',
|
||||||
|
},
|
||||||
|
metaIcon: {
|
||||||
|
fontSize: 13,
|
||||||
|
marginRight: 4,
|
||||||
|
},
|
||||||
|
metaText: {
|
||||||
|
fontSize: typography.fontSize.sm,
|
||||||
|
color: colors.text,
|
||||||
|
fontWeight: typography.fontWeight.medium,
|
||||||
|
},
|
||||||
|
metaDivider: {
|
||||||
|
width: 3,
|
||||||
|
height: 3,
|
||||||
|
borderRadius: 1.5,
|
||||||
|
backgroundColor: colors.textSecondary,
|
||||||
|
marginHorizontal: 10,
|
||||||
|
opacity: 0.5,
|
||||||
|
},
|
||||||
|
|
||||||
|
statusRow: {
|
||||||
|
flexDirection: 'row',
|
||||||
|
alignItems: 'center',
|
||||||
|
marginTop: 10,
|
||||||
|
},
|
||||||
|
statusDot: {
|
||||||
|
width: 7,
|
||||||
|
height: 7,
|
||||||
|
borderRadius: 3.5,
|
||||||
|
backgroundColor: colors.primary,
|
||||||
|
marginRight: 6,
|
||||||
|
},
|
||||||
|
statusText: {
|
||||||
|
fontSize: typography.fontSize.xs,
|
||||||
|
color: colors.primary,
|
||||||
|
fontWeight: typography.fontWeight.semibold,
|
||||||
|
},
|
||||||
|
statusTextMuted: {
|
||||||
|
fontSize: typography.fontSize.xs,
|
||||||
|
color: colors.textSecondary,
|
||||||
|
},
|
||||||
|
|
||||||
|
// ---------- Offers ----------
|
||||||
|
offersSection: {
|
||||||
|
marginTop: 18,
|
||||||
|
},
|
||||||
|
offersScrollContent: {
|
||||||
|
paddingHorizontal: 20,
|
||||||
|
},
|
||||||
|
offerChip: {
|
||||||
|
flexDirection: 'row',
|
||||||
|
alignItems: 'center',
|
||||||
|
borderWidth: 1,
|
||||||
|
borderStyle: 'dashed',
|
||||||
|
borderColor: colors.primary,
|
||||||
|
backgroundColor: colors.primaryMuted ?? '#E9F7EF',
|
||||||
|
borderRadius: 10,
|
||||||
|
paddingVertical: 8,
|
||||||
|
paddingHorizontal: 12,
|
||||||
|
marginRight: 10,
|
||||||
|
},
|
||||||
|
offerIcon: {
|
||||||
|
fontSize: 15,
|
||||||
|
marginRight: 6,
|
||||||
|
},
|
||||||
|
offerText: {
|
||||||
|
fontSize: typography.fontSize.xs,
|
||||||
|
fontWeight: typography.fontWeight.semibold,
|
||||||
|
color: colors.text,
|
||||||
|
},
|
||||||
|
|
||||||
|
// ---------- Divider ----------
|
||||||
|
sectionDivider: {
|
||||||
|
height: 8,
|
||||||
|
backgroundColor: colors.inputBg,
|
||||||
|
marginTop: 18,
|
||||||
|
},
|
||||||
|
|
||||||
|
// ---------- Menu search ----------
|
||||||
|
menuHeaderRow: {
|
||||||
|
flexDirection: 'row',
|
||||||
|
alignItems: 'center',
|
||||||
|
justifyContent: 'space-between',
|
||||||
|
paddingHorizontal: 20,
|
||||||
|
paddingTop: 18,
|
||||||
|
paddingBottom: 4,
|
||||||
|
},
|
||||||
|
menuTitle: {
|
||||||
|
fontSize: typography.fontSize.lg,
|
||||||
|
fontWeight: typography.fontWeight.bold,
|
||||||
|
color: colors.text,
|
||||||
|
},
|
||||||
|
menuCount: {
|
||||||
fontSize: typography.fontSize.sm,
|
fontSize: typography.fontSize.sm,
|
||||||
color: colors.textSecondary,
|
color: colors.textSecondary,
|
||||||
},
|
},
|
||||||
|
|
||||||
|
// ---------- Category tabs ----------
|
||||||
categoryRow: {
|
categoryRow: {
|
||||||
paddingVertical: 12,
|
paddingVertical: 14,
|
||||||
paddingHorizontal: 16,
|
paddingHorizontal: 20,
|
||||||
flexGrow: 0,
|
flexGrow: 0,
|
||||||
},
|
},
|
||||||
categoryTab: {
|
categoryTab: {
|
||||||
paddingHorizontal: 20,
|
paddingHorizontal: 18,
|
||||||
paddingVertical: 8,
|
paddingVertical: 9,
|
||||||
borderRadius: 20,
|
borderRadius: 22,
|
||||||
borderWidth: 1,
|
borderWidth: 1.5,
|
||||||
borderColor: colors.border,
|
borderColor: colors.border,
|
||||||
marginRight: 8,
|
marginRight: 10,
|
||||||
|
backgroundColor: colors.background,
|
||||||
},
|
},
|
||||||
categoryTabActive: {
|
categoryTabActive: {
|
||||||
borderColor: colors.primary,
|
borderColor: colors.primary,
|
||||||
backgroundColor: '#E8F5E9',
|
backgroundColor: colors.primary,
|
||||||
},
|
},
|
||||||
categoryTabText: {
|
categoryTabText: {
|
||||||
fontSize: typography.fontSize.sm,
|
fontSize: typography.fontSize.sm,
|
||||||
color: colors.textSecondary,
|
color: colors.textSecondary,
|
||||||
|
fontWeight: typography.fontWeight.medium,
|
||||||
},
|
},
|
||||||
categoryTabTextActive: {
|
categoryTabTextActive: {
|
||||||
color: colors.primary,
|
color: '#FFFFFF',
|
||||||
fontWeight: typography.fontWeight.semibold,
|
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: {
|
cartStrip: {
|
||||||
flexDirection: 'row',
|
flexDirection: 'row',
|
||||||
alignItems: 'center',
|
alignItems: 'center',
|
||||||
justifyContent: 'space-between',
|
justifyContent: 'space-between',
|
||||||
backgroundColor: colors.primary,
|
backgroundColor: colors.primary,
|
||||||
paddingHorizontal: 20,
|
paddingHorizontal: 18,
|
||||||
paddingVertical: 14,
|
paddingVertical: 14,
|
||||||
borderTopLeftRadius: 16,
|
borderRadius: 16,
|
||||||
borderTopRightRadius: 16,
|
...Platform.select({
|
||||||
|
ios: {
|
||||||
|
shadowColor: '#000',
|
||||||
|
shadowOffset: { width: 0, height: 6 },
|
||||||
|
shadowOpacity: 0.25,
|
||||||
|
shadowRadius: 12,
|
||||||
},
|
},
|
||||||
cartStripText: {
|
android: { elevation: 8 },
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
cartStripLeft: {
|
||||||
|
flexDirection: 'row',
|
||||||
|
alignItems: 'center',
|
||||||
|
},
|
||||||
|
cartBagIcon: {
|
||||||
|
fontSize: 18,
|
||||||
|
marginRight: 8,
|
||||||
|
},
|
||||||
|
cartStripTextWrap: {},
|
||||||
|
cartStripCount: {
|
||||||
|
fontSize: typography.fontSize.xs,
|
||||||
|
color: 'rgba(255,255,255,0.85)',
|
||||||
|
},
|
||||||
|
cartStripPrice: {
|
||||||
fontSize: typography.fontSize.md,
|
fontSize: typography.fontSize.md,
|
||||||
fontWeight: typography.fontWeight.semibold,
|
fontWeight: typography.fontWeight.bold,
|
||||||
color: '#FFFFFF',
|
color: '#FFFFFF',
|
||||||
},
|
},
|
||||||
viewCartButton: {
|
viewCartButton: {
|
||||||
|
flexDirection: 'row',
|
||||||
|
alignItems: 'center',
|
||||||
backgroundColor: '#FFFFFF',
|
backgroundColor: '#FFFFFF',
|
||||||
paddingHorizontal: 16,
|
paddingHorizontal: 16,
|
||||||
paddingVertical: 8,
|
paddingVertical: 9,
|
||||||
borderRadius: 8,
|
borderRadius: 10,
|
||||||
},
|
},
|
||||||
viewCartText: {
|
viewCartText: {
|
||||||
fontSize: typography.fontSize.sm,
|
fontSize: typography.fontSize.sm,
|
||||||
fontWeight: typography.fontWeight.semibold,
|
fontWeight: typography.fontWeight.bold,
|
||||||
|
color: colors.primary,
|
||||||
|
marginRight: 4,
|
||||||
|
},
|
||||||
|
viewCartArrow: {
|
||||||
|
fontSize: 13,
|
||||||
color: colors.primary,
|
color: colors.primary,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|||||||
@ -1,38 +1,289 @@
|
|||||||
import React, { useState, useEffect } from 'react';
|
import React, { useEffect, useMemo, useState } from 'react';
|
||||||
import {
|
import {
|
||||||
View,
|
View,
|
||||||
Text,
|
Text,
|
||||||
TouchableOpacity,
|
TouchableOpacity,
|
||||||
ScrollView,
|
ScrollView,
|
||||||
|
Image,
|
||||||
|
ImageBackground,
|
||||||
} from 'react-native';
|
} from 'react-native';
|
||||||
|
import { useNavigation, useRoute, RouteProp } from '@react-navigation/native';
|
||||||
|
import { StackNavigationProp } from '@react-navigation/stack';
|
||||||
import { getStyles } from './providerDetailsScreen.styles';
|
import { getStyles } from './providerDetailsScreen.styles';
|
||||||
import { Header, CatalogItemRow } from '@components';
|
import { CatalogItemRow } from '@components';
|
||||||
import { useAppTheme } from '@theme';
|
import { useAppTheme } from '@theme';
|
||||||
import { useAppDispatch, useAppSelector } from '../../../hooks/useAppDispatch';
|
import { useAppDispatch, useAppSelector } from '../../../hooks/useAppDispatch';
|
||||||
import { addItem } from '../../../store/cartSlice';
|
import { addItem } from '../../../store/commonreducers/cart';
|
||||||
import { deliveryService } from '../../../services/deliveryService';
|
import { getProviderCatalogApi, getProvidersApi } from '../../../api/deliveryApi';
|
||||||
import { CatalogItem } from '../../../interfaces';
|
import { CatalogItem, Provider } from '../../../interfaces';
|
||||||
|
import { AppStackParamList } from '../../../navigation/appStack';
|
||||||
|
|
||||||
const CATEGORIES = ['Pizza', 'Sides', 'Beverages'];
|
type ProviderDetailsNavProp = StackNavigationProp<
|
||||||
|
AppStackParamList,
|
||||||
|
'ProviderDetailsScreen'
|
||||||
|
>;
|
||||||
|
type ProviderDetailsRouteProp = RouteProp<
|
||||||
|
AppStackParamList,
|
||||||
|
'ProviderDetailsScreen'
|
||||||
|
>;
|
||||||
|
|
||||||
|
const FALLBACK_CATEGORIES = ['Pizza', 'Sides', 'Beverages'];
|
||||||
|
|
||||||
|
const OFFERS = [
|
||||||
|
{ key: 'o1', icon: '🏷️', label: '50% OFF up to ₹100' },
|
||||||
|
{ key: 'o2', icon: '🚚', label: 'Free delivery above ₹299' },
|
||||||
|
{ key: 'o3', icon: '🎁', label: 'Buy 1 Get 1 on combos' },
|
||||||
|
];
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Presentational subcomponents
|
||||||
|
// Kept local to this screen since none are reused elsewhere yet. Promote to
|
||||||
|
// @components if a second screen needs them.
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
interface HeroSectionProps {
|
||||||
|
styles: ReturnType<typeof getStyles>;
|
||||||
|
imageUrl?: string;
|
||||||
|
onBack: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
const HeroSection: React.FC<HeroSectionProps> = ({
|
||||||
|
styles,
|
||||||
|
imageUrl,
|
||||||
|
onBack,
|
||||||
|
}) => (
|
||||||
|
<View style={styles.heroWrap}>
|
||||||
|
{imageUrl ? (
|
||||||
|
<ImageBackground source={{ uri: imageUrl }} style={styles.heroImage}>
|
||||||
|
<View style={styles.heroOverlay} />
|
||||||
|
</ImageBackground>
|
||||||
|
) : (
|
||||||
|
<View style={styles.heroFallback}>
|
||||||
|
<Text style={styles.heroFallbackEmoji}>🍽️</Text>
|
||||||
|
<View style={styles.heroOverlay} />
|
||||||
|
</View>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<View style={styles.topIconRow}>
|
||||||
|
<TouchableOpacity
|
||||||
|
style={styles.iconButton}
|
||||||
|
activeOpacity={0.75}
|
||||||
|
onPress={onBack}
|
||||||
|
>
|
||||||
|
<Text style={styles.iconButtonText}>←</Text>
|
||||||
|
</TouchableOpacity>
|
||||||
|
<View style={styles.iconButtonGroup}>
|
||||||
|
<TouchableOpacity
|
||||||
|
style={[styles.iconButton, { marginRight: 10 }]}
|
||||||
|
activeOpacity={0.75}
|
||||||
|
>
|
||||||
|
<Text style={styles.iconButtonText}>🔗</Text>
|
||||||
|
</TouchableOpacity>
|
||||||
|
<TouchableOpacity style={styles.iconButton} activeOpacity={0.75}>
|
||||||
|
<Text style={styles.iconButtonText}>🤍</Text>
|
||||||
|
</TouchableOpacity>
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
);
|
||||||
|
|
||||||
|
interface InfoCardProps {
|
||||||
|
styles: ReturnType<typeof getStyles>;
|
||||||
|
name: string;
|
||||||
|
rating: number;
|
||||||
|
deliveryTime: string;
|
||||||
|
tag: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const InfoCard: React.FC<InfoCardProps> = ({
|
||||||
|
styles,
|
||||||
|
name,
|
||||||
|
rating,
|
||||||
|
deliveryTime,
|
||||||
|
tag,
|
||||||
|
}) => (
|
||||||
|
<View style={styles.infoCard}>
|
||||||
|
<View style={styles.infoTopRow}>
|
||||||
|
<Text style={styles.heroName} numberOfLines={1}>
|
||||||
|
{name}
|
||||||
|
</Text>
|
||||||
|
<View style={styles.ratingBadge}>
|
||||||
|
<Text style={{ fontSize: 11 }}>⭐</Text>
|
||||||
|
<Text style={styles.ratingBadgeText}>{rating.toFixed(1)}</Text>
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
|
||||||
|
<Text style={styles.cuisineText}>{tag} • Multi-cuisine</Text>
|
||||||
|
|
||||||
|
<View style={styles.metaRow}>
|
||||||
|
<View style={styles.metaItem}>
|
||||||
|
<Text style={styles.metaIcon}>🕐</Text>
|
||||||
|
<Text style={styles.metaText}>{deliveryTime}</Text>
|
||||||
|
</View>
|
||||||
|
<View style={styles.metaDivider} />
|
||||||
|
<View style={styles.metaItem}>
|
||||||
|
<Text style={styles.metaIcon}>📍</Text>
|
||||||
|
<Text style={styles.metaText}>2.4 km away</Text>
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
|
||||||
|
<View style={styles.statusRow}>
|
||||||
|
<View style={styles.statusDot} />
|
||||||
|
<Text style={styles.statusText}>Open now</Text>
|
||||||
|
<Text style={styles.statusTextMuted}> • Closes 11:30 PM</Text>
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
);
|
||||||
|
|
||||||
|
interface OffersRowProps {
|
||||||
|
styles: ReturnType<typeof getStyles>;
|
||||||
|
}
|
||||||
|
|
||||||
|
const OffersRow: React.FC<OffersRowProps> = ({ styles }) => (
|
||||||
|
<View style={styles.offersSection}>
|
||||||
|
<ScrollView
|
||||||
|
horizontal
|
||||||
|
showsHorizontalScrollIndicator={false}
|
||||||
|
contentContainerStyle={styles.offersScrollContent}
|
||||||
|
>
|
||||||
|
{OFFERS.map(offer => (
|
||||||
|
<View key={offer.key} style={styles.offerChip}>
|
||||||
|
<Text style={styles.offerIcon}>{offer.icon}</Text>
|
||||||
|
<Text style={styles.offerText}>{offer.label}</Text>
|
||||||
|
</View>
|
||||||
|
))}
|
||||||
|
</ScrollView>
|
||||||
|
</View>
|
||||||
|
);
|
||||||
|
|
||||||
|
interface CategoryTabsProps {
|
||||||
|
styles: ReturnType<typeof getStyles>;
|
||||||
|
categories: string[];
|
||||||
|
activeCategory: string;
|
||||||
|
onSelect: (category: string) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
const CategoryTabs: React.FC<CategoryTabsProps> = ({
|
||||||
|
styles,
|
||||||
|
categories,
|
||||||
|
activeCategory,
|
||||||
|
onSelect,
|
||||||
|
}) => (
|
||||||
|
<ScrollView
|
||||||
|
horizontal
|
||||||
|
showsHorizontalScrollIndicator={false}
|
||||||
|
contentContainerStyle={styles.categoryRow}
|
||||||
|
>
|
||||||
|
{categories.map(cat => {
|
||||||
|
const isActive = activeCategory === cat;
|
||||||
|
return (
|
||||||
|
<TouchableOpacity
|
||||||
|
key={cat}
|
||||||
|
style={[styles.categoryTab, isActive && styles.categoryTabActive]}
|
||||||
|
onPress={() => onSelect(cat)}
|
||||||
|
activeOpacity={0.75}
|
||||||
|
>
|
||||||
|
<Text
|
||||||
|
style={[
|
||||||
|
styles.categoryTabText,
|
||||||
|
isActive && styles.categoryTabTextActive,
|
||||||
|
]}
|
||||||
|
>
|
||||||
|
{cat}
|
||||||
|
</Text>
|
||||||
|
</TouchableOpacity>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</ScrollView>
|
||||||
|
);
|
||||||
|
|
||||||
|
interface CartStripProps {
|
||||||
|
styles: ReturnType<typeof getStyles>;
|
||||||
|
totalItems: number;
|
||||||
|
totalPrice: number;
|
||||||
|
onPress: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
const CartStrip: React.FC<CartStripProps> = ({
|
||||||
|
styles,
|
||||||
|
totalItems,
|
||||||
|
totalPrice,
|
||||||
|
onPress,
|
||||||
|
}) => (
|
||||||
|
<View style={styles.cartStripWrap}>
|
||||||
|
<TouchableOpacity
|
||||||
|
style={styles.cartStrip}
|
||||||
|
activeOpacity={0.85}
|
||||||
|
onPress={onPress}
|
||||||
|
>
|
||||||
|
<View style={styles.cartStripLeft}>
|
||||||
|
<Text style={styles.cartBagIcon}>🛍️</Text>
|
||||||
|
<View style={styles.cartStripTextWrap}>
|
||||||
|
<Text style={styles.cartStripCount}>
|
||||||
|
{totalItems} item{totalItems === 1 ? '' : 's'}
|
||||||
|
</Text>
|
||||||
|
<Text style={styles.cartStripPrice}>₹{totalPrice}</Text>
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
<View style={styles.viewCartButton}>
|
||||||
|
<Text style={styles.viewCartText}>View Cart</Text>
|
||||||
|
<Text style={styles.viewCartArrow}>→</Text>
|
||||||
|
</View>
|
||||||
|
</TouchableOpacity>
|
||||||
|
</View>
|
||||||
|
);
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Screen
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
export const ProviderDetailsScreen: React.FC = () => {
|
export const ProviderDetailsScreen: React.FC = () => {
|
||||||
const { colors } = useAppTheme();
|
const { colors } = useAppTheme();
|
||||||
const styles = getStyles(colors);
|
const styles = getStyles(colors);
|
||||||
const dispatch = useAppDispatch();
|
const dispatch = useAppDispatch();
|
||||||
const cartItems = useAppSelector((state) => state.cart.items);
|
const navigation = useNavigation<ProviderDetailsNavProp>();
|
||||||
|
const route = useRoute<ProviderDetailsRouteProp>();
|
||||||
|
const { providerId, providerName } = route.params;
|
||||||
|
|
||||||
|
const cartItems = useAppSelector(state => state.cart.items);
|
||||||
|
|
||||||
|
const [provider, setProvider] = useState<Provider | null>(null);
|
||||||
const [catalog, setCatalog] = useState<CatalogItem[]>([]);
|
const [catalog, setCatalog] = useState<CatalogItem[]>([]);
|
||||||
const [activeCategory, setActiveCategory] = useState(CATEGORIES[0]);
|
const [activeCategory, setActiveCategory] = useState(FALLBACK_CATEGORIES[0]);
|
||||||
|
|
||||||
|
const resolvedProviderId = providerId || 'p1';
|
||||||
|
const resolvedProviderName = providerName || 'Pizza Planet';
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
deliveryService.getProviderCatalog('p1').then(setCatalog);
|
getProviderCatalogApi(resolvedProviderId).then(setCatalog);
|
||||||
}, []);
|
}, [resolvedProviderId]);
|
||||||
|
|
||||||
const filteredItems = catalog.filter((item) => item.category === activeCategory);
|
useEffect(() => {
|
||||||
|
getProvidersApi().then(providers => {
|
||||||
|
const match = providers.find(p => p.id === resolvedProviderId);
|
||||||
|
if (match) setProvider(match);
|
||||||
|
});
|
||||||
|
}, [resolvedProviderId]);
|
||||||
|
|
||||||
|
const categories = useMemo(() => {
|
||||||
|
const fromCatalog = Array.from(new Set(catalog.map(item => item.category)));
|
||||||
|
return fromCatalog.length > 0 ? fromCatalog : FALLBACK_CATEGORIES;
|
||||||
|
}, [catalog]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!categories.includes(activeCategory)) {
|
||||||
|
setActiveCategory(categories[0]);
|
||||||
|
}
|
||||||
|
}, [categories, activeCategory]);
|
||||||
|
|
||||||
|
const filteredItems = useMemo(
|
||||||
|
() => catalog.filter(item => item.category === activeCategory),
|
||||||
|
[catalog, activeCategory],
|
||||||
|
);
|
||||||
|
|
||||||
const getItemQuantity = (itemId: string) => {
|
const getItemQuantity = (itemId: string) => {
|
||||||
const item = cartItems.find((i) => i.item.id === itemId);
|
const match = cartItems.find(i => i.item.id === itemId);
|
||||||
return item ? item.quantity : 0;
|
return match ? match.quantity : 0;
|
||||||
};
|
};
|
||||||
|
|
||||||
const totalItems = cartItems.reduce((sum, i) => sum + i.quantity, 0);
|
const totalItems = cartItems.reduce((sum, i) => sum + i.quantity, 0);
|
||||||
@ -41,50 +292,64 @@ export const ProviderDetailsScreen: React.FC = () => {
|
|||||||
0,
|
0,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const handleAddItem = (item: CatalogItem) => {
|
||||||
|
dispatch(
|
||||||
|
addItem({
|
||||||
|
providerId: resolvedProviderId,
|
||||||
|
providerName: resolvedProviderName,
|
||||||
|
item,
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<View style={styles.container}>
|
<View style={styles.container}>
|
||||||
<Header title="Provider Details" onBack={() => {}} />
|
|
||||||
<ScrollView style={styles.content}>
|
|
||||||
<View style={styles.hero}>
|
|
||||||
<View style={styles.heroImage}>
|
|
||||||
<Text style={styles.heroEmoji}>🍕</Text>
|
|
||||||
</View>
|
|
||||||
<Text style={styles.heroName}>Pizza Planet</Text>
|
|
||||||
<View style={styles.heroStats}>
|
|
||||||
<Text style={styles.heroStat}>⭐ 4.5</Text>
|
|
||||||
<Text style={styles.heroStat}>🕐 25 min</Text>
|
|
||||||
<Text style={styles.heroStat}>🍕 Italian</Text>
|
|
||||||
</View>
|
|
||||||
</View>
|
|
||||||
|
|
||||||
<ScrollView
|
<ScrollView
|
||||||
horizontal
|
style={styles.content}
|
||||||
showsHorizontalScrollIndicator={false}
|
contentContainerStyle={styles.contentBody}
|
||||||
style={styles.categoryRow}
|
showsVerticalScrollIndicator={false}
|
||||||
>
|
>
|
||||||
{CATEGORIES.map((cat) => (
|
<HeroSection
|
||||||
<TouchableOpacity
|
styles={styles}
|
||||||
key={cat}
|
imageUrl={provider?.imageUrl}
|
||||||
style={[
|
onBack={() => navigation.goBack()}
|
||||||
styles.categoryTab,
|
/>
|
||||||
activeCategory === cat && styles.categoryTabActive,
|
|
||||||
]}
|
|
||||||
onPress={() => setActiveCategory(cat)}
|
|
||||||
activeOpacity={0.7}
|
|
||||||
>
|
|
||||||
<Text
|
|
||||||
style={[
|
|
||||||
styles.categoryTabText,
|
|
||||||
activeCategory === cat && styles.categoryTabTextActive,
|
|
||||||
]}
|
|
||||||
>
|
|
||||||
{cat}
|
|
||||||
</Text>
|
|
||||||
</TouchableOpacity>
|
|
||||||
))}
|
|
||||||
</ScrollView>
|
|
||||||
|
|
||||||
{filteredItems.map((item) => (
|
<InfoCard
|
||||||
|
styles={styles}
|
||||||
|
name={resolvedProviderName}
|
||||||
|
rating={provider?.rating ?? 4.5}
|
||||||
|
deliveryTime={provider?.deliveryTime ?? '25 min'}
|
||||||
|
tag={provider?.tag ?? 'Italian'}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<OffersRow styles={styles} />
|
||||||
|
|
||||||
|
<View style={styles.sectionDivider} />
|
||||||
|
|
||||||
|
<View style={styles.menuHeaderRow}>
|
||||||
|
<Text style={styles.menuTitle}>Menu</Text>
|
||||||
|
<Text style={styles.menuCount}>{catalog.length} items</Text>
|
||||||
|
</View>
|
||||||
|
|
||||||
|
<CategoryTabs
|
||||||
|
styles={styles}
|
||||||
|
categories={categories}
|
||||||
|
activeCategory={activeCategory}
|
||||||
|
onSelect={setActiveCategory}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<View style={styles.menuList}>
|
||||||
|
{filteredItems.length === 0 && (
|
||||||
|
<View style={styles.emptyMenuWrap}>
|
||||||
|
<Text style={styles.emptyMenuEmoji}>🍽️</Text>
|
||||||
|
<Text style={styles.emptyMenuText}>
|
||||||
|
No items in this category yet
|
||||||
|
</Text>
|
||||||
|
</View>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{filteredItems.map(item => (
|
||||||
<CatalogItemRow
|
<CatalogItemRow
|
||||||
key={item.id}
|
key={item.id}
|
||||||
imageUrl={item.imageUrl}
|
imageUrl={item.imageUrl}
|
||||||
@ -92,29 +357,20 @@ export const ProviderDetailsScreen: React.FC = () => {
|
|||||||
description={item.description}
|
description={item.description}
|
||||||
price={item.price}
|
price={item.price}
|
||||||
quantity={getItemQuantity(item.id)}
|
quantity={getItemQuantity(item.id)}
|
||||||
onAdd={() =>
|
onAdd={() => handleAddItem(item)}
|
||||||
dispatch(
|
|
||||||
addItem({
|
|
||||||
providerId: 'p1',
|
|
||||||
providerName: 'Pizza Planet',
|
|
||||||
item,
|
|
||||||
}),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
onRemove={() => {}}
|
onRemove={() => {}}
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
|
</View>
|
||||||
</ScrollView>
|
</ScrollView>
|
||||||
|
|
||||||
{totalItems > 0 && (
|
{totalItems > 0 && (
|
||||||
<View style={styles.cartStrip}>
|
<CartStrip
|
||||||
<Text style={styles.cartStripText}>
|
styles={styles}
|
||||||
{totalItems} Items | ₹{totalPrice}
|
totalItems={totalItems}
|
||||||
</Text>
|
totalPrice={totalPrice}
|
||||||
<TouchableOpacity style={styles.viewCartButton} activeOpacity={0.7}>
|
onPress={() => navigation.navigate('CartScreen')}
|
||||||
<Text style={styles.viewCartText}>View Cart</Text>
|
/>
|
||||||
</TouchableOpacity>
|
|
||||||
</View>
|
|
||||||
)}
|
)}
|
||||||
</View>
|
</View>
|
||||||
);
|
);
|
||||||
|
|||||||
@ -6,27 +6,37 @@ import {
|
|||||||
TouchableOpacity,
|
TouchableOpacity,
|
||||||
ScrollView,
|
ScrollView,
|
||||||
} from 'react-native';
|
} from 'react-native';
|
||||||
|
import { useNavigation, useRoute, RouteProp } from '@react-navigation/native';
|
||||||
|
import { StackNavigationProp } from '@react-navigation/stack';
|
||||||
import { getStyles } from './providerListScreen.styles';
|
import { getStyles } from './providerListScreen.styles';
|
||||||
import { ProviderCard, Header } from '@components';
|
import { ProviderCard, Header } from '@components';
|
||||||
import { useAppTheme } from '@theme';
|
import { useAppTheme } from '@theme';
|
||||||
import { deliveryService } from '../../../services/deliveryService';
|
import { getProvidersApi } from '../../../api/deliveryApi';
|
||||||
import { Provider } from '../../../interfaces';
|
import { Provider } from '../../../interfaces';
|
||||||
|
import { AppStackParamList } from '../../../navigation/appStack';
|
||||||
|
|
||||||
const FILTERS = ['Filters', 'Rating 4.0+', 'Fast Delivery'];
|
const FILTERS = ['Filters', 'Rating 4.0+', 'Fast Delivery'];
|
||||||
|
|
||||||
|
type ProviderListNavProp = StackNavigationProp<AppStackParamList, 'ProviderListScreen'>;
|
||||||
|
type ProviderListRouteProp = RouteProp<AppStackParamList, 'ProviderListScreen'>;
|
||||||
|
|
||||||
export const ProviderListScreen: React.FC = () => {
|
export const ProviderListScreen: React.FC = () => {
|
||||||
const { colors } = useAppTheme();
|
const { colors } = useAppTheme();
|
||||||
const styles = getStyles(colors);
|
const styles = getStyles(colors);
|
||||||
|
const navigation = useNavigation<ProviderListNavProp>();
|
||||||
|
const route = useRoute<ProviderListRouteProp>();
|
||||||
|
const category = route.params?.category;
|
||||||
const [providers, setProviders] = useState<Provider[]>([]);
|
const [providers, setProviders] = useState<Provider[]>([]);
|
||||||
const [activeFilter, setActiveFilter] = useState<string | null>(null);
|
const [activeFilter, setActiveFilter] = useState<string | null>(null);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
deliveryService.getProviders().then(setProviders);
|
// If a category was passed, we can filter or search by it, but for mock data we fetch all
|
||||||
}, []);
|
getProvidersApi().then(setProviders);
|
||||||
|
}, [category]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<View style={styles.container}>
|
<View style={styles.container}>
|
||||||
<Header title="Providers" onBack={() => {}} />
|
<Header title={category ? `${category} Providers` : 'Providers'} onBack={() => navigation.goBack()} />
|
||||||
<ScrollView horizontal showsHorizontalScrollIndicator={false} style={styles.filterRow}>
|
<ScrollView horizontal showsHorizontalScrollIndicator={false} style={styles.filterRow}>
|
||||||
{FILTERS.map((filter) => (
|
{FILTERS.map((filter) => (
|
||||||
<TouchableOpacity
|
<TouchableOpacity
|
||||||
@ -50,7 +60,7 @@ export const ProviderListScreen: React.FC = () => {
|
|||||||
))}
|
))}
|
||||||
</ScrollView>
|
</ScrollView>
|
||||||
<FlatList
|
<FlatList
|
||||||
data={providers}
|
data={category ? providers.filter(p => p.tag === category) : providers}
|
||||||
keyExtractor={(item) => item.id}
|
keyExtractor={(item) => item.id}
|
||||||
renderItem={({ item }) => (
|
renderItem={({ item }) => (
|
||||||
<ProviderCard
|
<ProviderCard
|
||||||
@ -60,6 +70,12 @@ export const ProviderListScreen: React.FC = () => {
|
|||||||
deliveryTime={item.deliveryTime}
|
deliveryTime={item.deliveryTime}
|
||||||
tag={item.tag}
|
tag={item.tag}
|
||||||
discountText={item.discountText}
|
discountText={item.discountText}
|
||||||
|
onPress={() =>
|
||||||
|
navigation.navigate('ProviderDetailsScreen', {
|
||||||
|
providerId: item.id,
|
||||||
|
providerName: item.name,
|
||||||
|
})
|
||||||
|
}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
contentContainerStyle={styles.list}
|
contentContainerStyle={styles.list}
|
||||||
|
|||||||
@ -4,22 +4,33 @@ import {
|
|||||||
Text,
|
Text,
|
||||||
FlatList,
|
FlatList,
|
||||||
} from 'react-native';
|
} from 'react-native';
|
||||||
|
import { useNavigation, CompositeNavigationProp } from '@react-navigation/native';
|
||||||
|
import { BottomTabNavigationProp } from '@react-navigation/bottom-tabs';
|
||||||
|
import { StackNavigationProp } from '@react-navigation/stack';
|
||||||
import { getStyles } from './searchScreen.styles';
|
import { getStyles } from './searchScreen.styles';
|
||||||
import { SearchBar, ProviderCard } from '@components';
|
import { SearchBar, ProviderCard } from '@components';
|
||||||
import { useAppTheme } from '@theme';
|
import { useAppTheme } from '@theme';
|
||||||
import { deliveryService } from '../../../services/deliveryService';
|
import { searchProvidersApi } from '../../../api/deliveryApi';
|
||||||
import { Provider } from '../../../interfaces';
|
import { Provider } from '../../../interfaces';
|
||||||
|
import { AppStackParamList } from '../../../navigation/appStack';
|
||||||
|
import { MainTabParamList } from '../../../navigation/mainTabNavigator';
|
||||||
|
|
||||||
|
type NavProp = CompositeNavigationProp<
|
||||||
|
BottomTabNavigationProp<MainTabParamList, 'SearchScreen'>,
|
||||||
|
StackNavigationProp<AppStackParamList>
|
||||||
|
>;
|
||||||
|
|
||||||
export const SearchScreen: React.FC = () => {
|
export const SearchScreen: React.FC = () => {
|
||||||
const { colors } = useAppTheme();
|
const { colors } = useAppTheme();
|
||||||
const styles = getStyles(colors);
|
const styles = getStyles(colors);
|
||||||
|
const navigation = useNavigation<NavProp>();
|
||||||
|
|
||||||
const [query, setQuery] = useState('');
|
const [query, setQuery] = useState('');
|
||||||
const [results, setResults] = useState<Provider[]>([]);
|
const [results, setResults] = useState<Provider[]>([]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (query.trim().length > 0) {
|
if (query.trim().length > 0) {
|
||||||
deliveryService.searchProviders(query).then(setResults);
|
searchProvidersApi(query).then(setResults);
|
||||||
} else {
|
} else {
|
||||||
setResults([]);
|
setResults([]);
|
||||||
}
|
}
|
||||||
@ -43,6 +54,12 @@ export const SearchScreen: React.FC = () => {
|
|||||||
deliveryTime={item.deliveryTime}
|
deliveryTime={item.deliveryTime}
|
||||||
tag={item.tag}
|
tag={item.tag}
|
||||||
discountText={item.discountText}
|
discountText={item.discountText}
|
||||||
|
onPress={() =>
|
||||||
|
navigation.navigate('ProviderDetailsScreen', {
|
||||||
|
providerId: item.id,
|
||||||
|
providerName: item.name,
|
||||||
|
})
|
||||||
|
}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
ListEmptyComponent={
|
ListEmptyComponent={
|
||||||
|
|||||||
@ -1,58 +1,104 @@
|
|||||||
import { StyleSheet } from 'react-native';
|
import { StyleSheet, Platform, Dimensions } from 'react-native';
|
||||||
import { typography } from '@theme';
|
import { typography } from '@theme';
|
||||||
|
|
||||||
export const getStyles = (colors: any) => StyleSheet.create({
|
const { width } = Dimensions.get('window');
|
||||||
|
|
||||||
|
export const getStyles = (colors: any) =>
|
||||||
|
StyleSheet.create({
|
||||||
container: {
|
container: {
|
||||||
flex: 1,
|
flex: 1,
|
||||||
backgroundColor: colors.background,
|
backgroundColor: colors.background,
|
||||||
},
|
},
|
||||||
scrollContainer: {
|
|
||||||
flexGrow: 1,
|
// ---------- Map ----------
|
||||||
paddingTop: 60,
|
map: {
|
||||||
|
flex: 1,
|
||||||
},
|
},
|
||||||
mapPlaceholder: {
|
|
||||||
height: 240,
|
// ---------- Locate me FAB ----------
|
||||||
backgroundColor: colors.inputBg,
|
locateButton: {
|
||||||
marginHorizontal: 24,
|
position: 'absolute',
|
||||||
borderRadius: 16,
|
right: 16,
|
||||||
justifyContent: 'center',
|
bottom: Platform.OS === 'ios' ? 310 : 290,
|
||||||
|
width: 48,
|
||||||
|
height: 48,
|
||||||
|
borderRadius: 24,
|
||||||
|
backgroundColor: '#FFFFFF',
|
||||||
alignItems: 'center',
|
alignItems: 'center',
|
||||||
marginBottom: 24,
|
justifyContent: 'center',
|
||||||
borderWidth: 1,
|
...Platform.select({
|
||||||
borderColor: colors.border,
|
ios: {
|
||||||
|
shadowColor: '#000',
|
||||||
|
shadowOffset: { width: 0, height: 3 },
|
||||||
|
shadowOpacity: 0.15,
|
||||||
|
shadowRadius: 6,
|
||||||
},
|
},
|
||||||
mapIcon: {
|
android: { elevation: 5 },
|
||||||
fontSize: 48,
|
}),
|
||||||
marginBottom: 8,
|
|
||||||
},
|
},
|
||||||
mapText: {
|
locateIcon: {
|
||||||
fontSize: typography.fontSize.lg,
|
fontSize: 22,
|
||||||
fontWeight: typography.fontWeight.semibold,
|
|
||||||
color: colors.textSecondary,
|
|
||||||
},
|
},
|
||||||
mapSubtext: {
|
|
||||||
fontSize: typography.fontSize.xs,
|
// ---------- Bottom sheet ----------
|
||||||
color: colors.placeholder,
|
bottomSheet: {
|
||||||
marginTop: 4,
|
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: {
|
card: {
|
||||||
backgroundColor: colors.cardBg,
|
backgroundColor: colors.cardBg,
|
||||||
borderRadius: 12,
|
borderRadius: 16,
|
||||||
padding: 16,
|
padding: 16,
|
||||||
marginHorizontal: 24,
|
marginBottom: 16,
|
||||||
marginBottom: 24,
|
|
||||||
borderWidth: 1,
|
borderWidth: 1,
|
||||||
borderColor: colors.border,
|
borderColor: colors.border,
|
||||||
},
|
},
|
||||||
|
cardHeader: {
|
||||||
|
flexDirection: 'row',
|
||||||
|
alignItems: 'center',
|
||||||
|
marginBottom: 8,
|
||||||
|
},
|
||||||
|
cardIcon: {
|
||||||
|
fontSize: 18,
|
||||||
|
marginRight: 8,
|
||||||
|
},
|
||||||
cardTitle: {
|
cardTitle: {
|
||||||
fontSize: typography.fontSize.sm,
|
fontSize: typography.fontSize.sm,
|
||||||
|
fontWeight: typography.fontWeight.semibold,
|
||||||
color: colors.textSecondary,
|
color: colors.textSecondary,
|
||||||
marginBottom: 4,
|
|
||||||
},
|
},
|
||||||
cardAddress: {
|
cardAddress: {
|
||||||
fontSize: typography.fontSize.md,
|
fontSize: typography.fontSize.md,
|
||||||
fontWeight: typography.fontWeight.semibold,
|
fontWeight: typography.fontWeight.semibold,
|
||||||
color: colors.text,
|
color: colors.text,
|
||||||
|
lineHeight: 22,
|
||||||
marginBottom: 8,
|
marginBottom: 8,
|
||||||
},
|
},
|
||||||
changeLink: {
|
changeLink: {
|
||||||
@ -63,13 +109,19 @@ export const getStyles = (colors: any) => StyleSheet.create({
|
|||||||
color: colors.primary,
|
color: colors.primary,
|
||||||
fontWeight: typography.fontWeight.semibold,
|
fontWeight: typography.fontWeight.semibold,
|
||||||
},
|
},
|
||||||
|
|
||||||
|
// ---------- Buttons ----------
|
||||||
|
confirmButton: {
|
||||||
|
borderRadius: 14,
|
||||||
|
marginBottom: 8,
|
||||||
|
},
|
||||||
manualButton: {
|
manualButton: {
|
||||||
alignItems: 'center',
|
alignItems: 'center',
|
||||||
paddingVertical: 16,
|
paddingVertical: 14,
|
||||||
},
|
},
|
||||||
manualButtonText: {
|
manualButtonText: {
|
||||||
fontSize: typography.fontSize.sm,
|
fontSize: typography.fontSize.sm,
|
||||||
color: colors.primary,
|
color: colors.primary,
|
||||||
fontWeight: typography.fontWeight.semibold,
|
fontWeight: typography.fontWeight.semibold,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|||||||
@ -1,54 +1,139 @@
|
|||||||
import React from 'react';
|
import React, { useState, useEffect, useRef, useCallback } from 'react';
|
||||||
import {
|
import {
|
||||||
View,
|
View,
|
||||||
Text,
|
Text,
|
||||||
TouchableOpacity,
|
TouchableOpacity,
|
||||||
KeyboardAvoidingView,
|
|
||||||
Platform,
|
Platform,
|
||||||
ScrollView,
|
ActivityIndicator,
|
||||||
} from 'react-native';
|
} from 'react-native';
|
||||||
|
import MapView, { Marker, Region } from 'react-native-maps';
|
||||||
import { useNavigation } from '@react-navigation/native';
|
import { useNavigation } from '@react-navigation/native';
|
||||||
import { StackNavigationProp } from '@react-navigation/stack';
|
import { StackNavigationProp } from '@react-navigation/stack';
|
||||||
import { getStyles } from './setLocationScreen.styles';
|
import { getStyles } from './setLocationScreen.styles';
|
||||||
import { PrimaryButton } from '@components';
|
import { PrimaryButton } from '@components';
|
||||||
import { useAppTheme } from '@theme';
|
import { useAppTheme } from '@theme';
|
||||||
import { AuthStackParamList } from '../../../navigation/authStack';
|
import { AuthStackParamList } from '../../../navigation/authStack';
|
||||||
|
import {
|
||||||
|
DEFAULT_LOCATION,
|
||||||
|
getCurrentLocationWithAddress,
|
||||||
|
reverseGeocode,
|
||||||
|
LatLng,
|
||||||
|
} from '../../../services/locationServices';
|
||||||
|
|
||||||
type NavProp = StackNavigationProp<AuthStackParamList, 'SetLocationScreen'>;
|
type NavProp = StackNavigationProp<AuthStackParamList, 'SetLocationScreen'>;
|
||||||
|
|
||||||
|
const DELTA = { latitudeDelta: 0.006, longitudeDelta: 0.006 };
|
||||||
|
|
||||||
export const SetLocationScreen: React.FC = () => {
|
export const SetLocationScreen: React.FC = () => {
|
||||||
const { colors } = useAppTheme();
|
const { colors } = useAppTheme();
|
||||||
const styles = getStyles(colors);
|
const styles = getStyles(colors);
|
||||||
const navigation = useNavigation<NavProp>();
|
const navigation = useNavigation<NavProp>();
|
||||||
|
const mapRef = useRef<MapView>(null);
|
||||||
|
|
||||||
|
const [coords, setCoords] = useState<LatLng>(DEFAULT_LOCATION);
|
||||||
|
const [address, setAddress] = useState('Fetching your location…');
|
||||||
|
const [loading, setLoading] = useState(true);
|
||||||
|
|
||||||
|
// ------------------------------------------------------------------
|
||||||
|
// Fetch current location on mount
|
||||||
|
// ------------------------------------------------------------------
|
||||||
|
const fetchCurrentLocation = useCallback(async () => {
|
||||||
|
setLoading(true);
|
||||||
|
try {
|
||||||
|
console.log('--------------');
|
||||||
|
const result = await getCurrentLocationWithAddress();
|
||||||
|
console.log('result', result);
|
||||||
|
setCoords(result.coords);
|
||||||
|
setAddress(result.address);
|
||||||
|
|
||||||
|
// Animate the map to the user's location
|
||||||
|
mapRef.current?.animateToRegion({ ...result.coords, ...DELTA }, 600);
|
||||||
|
} catch {
|
||||||
|
// Permission denied or GPS unavailable — keep the default location
|
||||||
|
console.log('--------error------');
|
||||||
|
setAddress('Could not determine location');
|
||||||
|
} finally {
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
fetchCurrentLocation();
|
||||||
|
}, [fetchCurrentLocation]);
|
||||||
|
|
||||||
|
// ------------------------------------------------------------------
|
||||||
|
// When user drags the map, update the marker + address
|
||||||
|
// ------------------------------------------------------------------
|
||||||
|
const handleRegionChangeComplete = useCallback(async (region: Region) => {
|
||||||
|
const newCoords: LatLng = {
|
||||||
|
latitude: region.latitude,
|
||||||
|
longitude: region.longitude,
|
||||||
|
};
|
||||||
|
setCoords(newCoords);
|
||||||
|
setAddress('Fetching address…');
|
||||||
|
|
||||||
|
const newAddress = await reverseGeocode(newCoords);
|
||||||
|
setAddress(newAddress);
|
||||||
|
}, []);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<KeyboardAvoidingView
|
<View style={styles.container}>
|
||||||
style={styles.container}
|
{/* ---------- Map ---------- */}
|
||||||
behavior={Platform.OS === 'ios' ? 'padding' : 'height'}
|
<MapView
|
||||||
|
ref={mapRef}
|
||||||
|
style={styles.map}
|
||||||
|
initialRegion={{ ...DEFAULT_LOCATION, ...DELTA }}
|
||||||
|
showsUserLocation
|
||||||
|
showsMyLocationButton={true}
|
||||||
|
onRegionChangeComplete={handleRegionChangeComplete}
|
||||||
>
|
>
|
||||||
<ScrollView contentContainerStyle={styles.scrollContainer}>
|
<Marker
|
||||||
<View style={styles.mapPlaceholder}>
|
coordinate={coords}
|
||||||
<Text style={styles.mapIcon}>🗺️</Text>
|
title="Your Location"
|
||||||
<Text style={styles.mapText}>Map View</Text>
|
description={address}
|
||||||
<Text style={styles.mapSubtext}>
|
/>
|
||||||
(react-native-maps integration)
|
</MapView>
|
||||||
</Text>
|
|
||||||
</View>
|
{/* ---------- "Locate me" floating button ---------- */}
|
||||||
|
<TouchableOpacity
|
||||||
|
style={styles.locateButton}
|
||||||
|
activeOpacity={0.8}
|
||||||
|
onPress={fetchCurrentLocation}
|
||||||
|
>
|
||||||
|
<Text style={styles.locateIcon}>📍</Text>
|
||||||
|
</TouchableOpacity>
|
||||||
|
|
||||||
|
{/* ---------- Bottom sheet ---------- */}
|
||||||
|
<View style={styles.bottomSheet}>
|
||||||
|
<View style={styles.sheetHandle} />
|
||||||
|
|
||||||
<View style={styles.card}>
|
<View style={styles.card}>
|
||||||
<Text style={styles.cardTitle}>Delivery Location</Text>
|
<View style={styles.cardHeader}>
|
||||||
<Text style={styles.cardAddress}>
|
<Text style={styles.cardIcon}>📍</Text>
|
||||||
Koramangala 4th Block, Bengaluru, 560034
|
<Text style={styles.cardTitle}>My Location</Text>
|
||||||
|
</View>
|
||||||
|
|
||||||
|
{loading ? (
|
||||||
|
<ActivityIndicator
|
||||||
|
size="small"
|
||||||
|
color={colors.primary}
|
||||||
|
style={{ marginVertical: 12 }}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<Text style={styles.cardAddress} numberOfLines={2}>
|
||||||
|
{address}
|
||||||
</Text>
|
</Text>
|
||||||
|
)}
|
||||||
|
|
||||||
<TouchableOpacity style={styles.changeLink} activeOpacity={0.7}>
|
<TouchableOpacity style={styles.changeLink} activeOpacity={0.7}>
|
||||||
<Text style={styles.changeText}>Change</Text>
|
<Text style={styles.changeText}>Change</Text>
|
||||||
</TouchableOpacity>
|
</TouchableOpacity>
|
||||||
</View>
|
</View>
|
||||||
|
|
||||||
<PrimaryButton
|
<PrimaryButton
|
||||||
title="Use this location"
|
title="Confirm Location"
|
||||||
onPress={() => navigation.navigate('CompleteProfileScreen')}
|
onPress={() => navigation.navigate('CompleteProfileScreen')}
|
||||||
style={{ marginHorizontal: 24 }}
|
style={styles.confirmButton}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<TouchableOpacity
|
<TouchableOpacity
|
||||||
@ -58,7 +143,7 @@ export const SetLocationScreen: React.FC = () => {
|
|||||||
>
|
>
|
||||||
<Text style={styles.manualButtonText}>Enter address manually</Text>
|
<Text style={styles.manualButtonText}>Enter address manually</Text>
|
||||||
</TouchableOpacity>
|
</TouchableOpacity>
|
||||||
</ScrollView>
|
</View>
|
||||||
</KeyboardAvoidingView>
|
</View>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
1
app/hooks/index.ts
Normal file
1
app/hooks/index.ts
Normal file
@ -0,0 +1 @@
|
|||||||
|
export * from './useLoginScreen';
|
||||||
@ -1,5 +0,0 @@
|
|||||||
import { useDispatch, useSelector, TypedUseSelectorHook } from 'react-redux';
|
|
||||||
import type { RootState, AppDispatch } from '../store';
|
|
||||||
|
|
||||||
export const useAppDispatch = () => useDispatch<AppDispatch>();
|
|
||||||
export const useAppSelector: TypedUseSelectorHook<RootState> = useSelector;
|
|
||||||
63
app/hooks/useLoginScreen.ts
Normal file
63
app/hooks/useLoginScreen.ts
Normal file
@ -0,0 +1,63 @@
|
|||||||
|
import { useState, useCallback } from 'react';
|
||||||
|
import { useNavigation } from '@react-navigation/native';
|
||||||
|
import { StackNavigationProp } from '@react-navigation/stack';
|
||||||
|
import { AuthStackParamList } from 'app/navigation/authStack';
|
||||||
|
import { loginWithPhone, useAppDispatch } from '../store';
|
||||||
|
|
||||||
|
type LoginNavProp = StackNavigationProp<AuthStackParamList, 'LoginScreen'>;
|
||||||
|
|
||||||
|
const MOBILE_NUMBER_LENGTH = 10;
|
||||||
|
|
||||||
|
interface UseLoginScreenResult {
|
||||||
|
mobileNumber: string;
|
||||||
|
error: string | undefined;
|
||||||
|
onChangeMobileNumber: (text: string) => void;
|
||||||
|
handleLogin: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const useLoginScreen = (): UseLoginScreenResult => {
|
||||||
|
const navigation = useNavigation<LoginNavProp>();
|
||||||
|
const dispatch = useAppDispatch();
|
||||||
|
|
||||||
|
const [mobileNumber, setMobileNumber] = useState('');
|
||||||
|
const [error, setError] = useState<string | undefined>(undefined);
|
||||||
|
|
||||||
|
const onChangeMobileNumber = useCallback(
|
||||||
|
(text: string) => {
|
||||||
|
setMobileNumber(text);
|
||||||
|
if (error) {
|
||||||
|
setError(undefined);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[error],
|
||||||
|
);
|
||||||
|
|
||||||
|
const validateMobileNumber = (value: string): string | undefined => {
|
||||||
|
if (!value) {
|
||||||
|
return 'Mobile number is required';
|
||||||
|
}
|
||||||
|
if (value.length < MOBILE_NUMBER_LENGTH) {
|
||||||
|
return 'Please enter a valid mobile number';
|
||||||
|
}
|
||||||
|
return undefined;
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleLogin = useCallback(() => {
|
||||||
|
const validationError = validateMobileNumber(mobileNumber);
|
||||||
|
if (validationError) {
|
||||||
|
setError(validationError);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
setError(undefined);
|
||||||
|
dispatch(loginWithPhone(mobileNumber));
|
||||||
|
navigation.navigate('OtpScreen', { mobileNumber });
|
||||||
|
}, [mobileNumber, dispatch, navigation]);
|
||||||
|
|
||||||
|
return {
|
||||||
|
mobileNumber,
|
||||||
|
error,
|
||||||
|
onChangeMobileNumber,
|
||||||
|
handleLogin,
|
||||||
|
};
|
||||||
|
};
|
||||||
@ -1,82 +1,50 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { createBottomTabNavigator } from '@react-navigation/bottom-tabs';
|
import { createStackNavigator } from '@react-navigation/stack';
|
||||||
import { Text } from 'react-native';
|
import { NavigatorScreenParams } from '@react-navigation/native';
|
||||||
|
import { MainTabNavigator, MainTabParamList } from './mainTabNavigator';
|
||||||
import {
|
import {
|
||||||
HomeScreen,
|
ProviderListScreen,
|
||||||
SearchScreen,
|
ProviderDetailsScreen,
|
||||||
MyOrdersScreen,
|
CartScreen,
|
||||||
OffersScreen,
|
CheckoutAddressScreen,
|
||||||
AccountScreen,
|
CheckoutPaymentScreen,
|
||||||
|
OrderConfirmedScreen,
|
||||||
|
OrderTrackingScreen,
|
||||||
|
LiveTrackingScreen,
|
||||||
|
OrderDeliveredScreen,
|
||||||
|
HelpSupportScreen,
|
||||||
} from '@features/screens';
|
} from '@features/screens';
|
||||||
|
|
||||||
export type AppStackParamList = {
|
export type AppStackParamList = {
|
||||||
HomeScreen: undefined;
|
MainTabs: NavigatorScreenParams<MainTabParamList> | undefined;
|
||||||
SearchScreen: undefined;
|
ProviderListScreen: { category?: string } | undefined;
|
||||||
MyOrdersScreen: undefined;
|
ProviderDetailsScreen: { providerId: string; providerName?: string };
|
||||||
OffersScreen: undefined;
|
CartScreen: undefined;
|
||||||
AccountScreen: undefined;
|
CheckoutAddressScreen: undefined;
|
||||||
|
CheckoutPaymentScreen: undefined;
|
||||||
|
OrderConfirmedScreen: { orderId?: string } | undefined;
|
||||||
|
OrderTrackingScreen: { orderId?: string } | undefined;
|
||||||
|
LiveTrackingScreen: { orderId?: string } | undefined;
|
||||||
|
OrderDeliveredScreen: { orderId?: string } | undefined;
|
||||||
|
HelpSupportScreen: undefined;
|
||||||
};
|
};
|
||||||
|
|
||||||
const Tab = createBottomTabNavigator<AppStackParamList>();
|
const Stack = createStackNavigator<AppStackParamList>();
|
||||||
|
|
||||||
const tabIcons: Record<string, string> = {
|
|
||||||
HomeScreen: '🏠',
|
|
||||||
SearchScreen: '🔍',
|
|
||||||
MyOrdersScreen: '📦',
|
|
||||||
OffersScreen: '🏷️',
|
|
||||||
AccountScreen: '👤',
|
|
||||||
};
|
|
||||||
|
|
||||||
const renderTabIcon = (routeName: string, focused: boolean) => (
|
|
||||||
<Text style={{ fontSize: 22, opacity: focused ? 1 : 0.5 }}>
|
|
||||||
{tabIcons[routeName]}
|
|
||||||
</Text>
|
|
||||||
);
|
|
||||||
|
|
||||||
export const AppStack: React.FC = () => {
|
export const AppStack: React.FC = () => {
|
||||||
return (
|
return (
|
||||||
<Tab.Navigator
|
<Stack.Navigator screenOptions={{ headerShown: false }}>
|
||||||
screenOptions={({ route }) => ({
|
<Stack.Screen name="MainTabs" component={MainTabNavigator} />
|
||||||
headerShown: false,
|
<Stack.Screen name="ProviderListScreen" component={ProviderListScreen} />
|
||||||
tabBarIcon: ({ focused }) => renderTabIcon(route.name, focused),
|
<Stack.Screen name="ProviderDetailsScreen" component={ProviderDetailsScreen} />
|
||||||
tabBarActiveTintColor: '#05824C',
|
<Stack.Screen name="CartScreen" component={CartScreen} />
|
||||||
tabBarInactiveTintColor: '#666666',
|
<Stack.Screen name="CheckoutAddressScreen" component={CheckoutAddressScreen} />
|
||||||
tabBarStyle: {
|
<Stack.Screen name="CheckoutPaymentScreen" component={CheckoutPaymentScreen} />
|
||||||
paddingBottom: 8,
|
<Stack.Screen name="OrderConfirmedScreen" component={OrderConfirmedScreen} />
|
||||||
paddingTop: 8,
|
<Stack.Screen name="OrderTrackingScreen" component={OrderTrackingScreen} />
|
||||||
height: 60,
|
<Stack.Screen name="LiveTrackingScreen" component={LiveTrackingScreen} />
|
||||||
},
|
<Stack.Screen name="OrderDeliveredScreen" component={OrderDeliveredScreen} />
|
||||||
tabBarLabelStyle: {
|
<Stack.Screen name="HelpSupportScreen" component={HelpSupportScreen} />
|
||||||
fontSize: 11,
|
</Stack.Navigator>
|
||||||
fontWeight: '500',
|
|
||||||
},
|
|
||||||
})}
|
|
||||||
>
|
|
||||||
<Tab.Screen
|
|
||||||
name="HomeScreen"
|
|
||||||
component={HomeScreen}
|
|
||||||
options={{ tabBarLabel: 'Home' }}
|
|
||||||
/>
|
|
||||||
<Tab.Screen
|
|
||||||
name="SearchScreen"
|
|
||||||
component={SearchScreen}
|
|
||||||
options={{ tabBarLabel: 'Search' }}
|
|
||||||
/>
|
|
||||||
<Tab.Screen
|
|
||||||
name="MyOrdersScreen"
|
|
||||||
component={MyOrdersScreen}
|
|
||||||
options={{ tabBarLabel: 'Orders' }}
|
|
||||||
/>
|
|
||||||
<Tab.Screen
|
|
||||||
name="OffersScreen"
|
|
||||||
component={OffersScreen}
|
|
||||||
options={{ tabBarLabel: 'Offers' }}
|
|
||||||
/>
|
|
||||||
<Tab.Screen
|
|
||||||
name="AccountScreen"
|
|
||||||
component={AccountScreen}
|
|
||||||
options={{ tabBarLabel: 'Account' }}
|
|
||||||
/>
|
|
||||||
</Tab.Navigator>
|
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
82
app/navigation/mainTabNavigator.tsx
Normal file
82
app/navigation/mainTabNavigator.tsx
Normal file
@ -0,0 +1,82 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { createBottomTabNavigator } from '@react-navigation/bottom-tabs';
|
||||||
|
import { Text } from 'react-native';
|
||||||
|
import {
|
||||||
|
HomeScreen,
|
||||||
|
SearchScreen,
|
||||||
|
MyOrdersScreen,
|
||||||
|
OffersScreen,
|
||||||
|
AccountScreen,
|
||||||
|
} from '@features/screens';
|
||||||
|
|
||||||
|
export type MainTabParamList = {
|
||||||
|
HomeScreen: undefined;
|
||||||
|
SearchScreen: undefined;
|
||||||
|
MyOrdersScreen: undefined;
|
||||||
|
OffersScreen: undefined;
|
||||||
|
AccountScreen: undefined;
|
||||||
|
};
|
||||||
|
|
||||||
|
const Tab = createBottomTabNavigator<MainTabParamList>();
|
||||||
|
|
||||||
|
const tabIcons: Record<string, string> = {
|
||||||
|
HomeScreen: '🏠',
|
||||||
|
SearchScreen: '🔍',
|
||||||
|
MyOrdersScreen: '📦',
|
||||||
|
OffersScreen: '🏷️',
|
||||||
|
AccountScreen: '👤',
|
||||||
|
};
|
||||||
|
|
||||||
|
const renderTabIcon = (routeName: string, focused: boolean) => (
|
||||||
|
<Text style={{ fontSize: 22, opacity: focused ? 1 : 0.5 }}>
|
||||||
|
{tabIcons[routeName]}
|
||||||
|
</Text>
|
||||||
|
);
|
||||||
|
|
||||||
|
export const MainTabNavigator: React.FC = () => {
|
||||||
|
return (
|
||||||
|
<Tab.Navigator
|
||||||
|
screenOptions={({ route }) => ({
|
||||||
|
headerShown: false,
|
||||||
|
tabBarIcon: ({ focused }) => renderTabIcon(route.name, focused),
|
||||||
|
tabBarActiveTintColor: '#05824C',
|
||||||
|
tabBarInactiveTintColor: '#666666',
|
||||||
|
tabBarStyle: {
|
||||||
|
paddingBottom: 8,
|
||||||
|
paddingTop: 8,
|
||||||
|
height: 60,
|
||||||
|
},
|
||||||
|
tabBarLabelStyle: {
|
||||||
|
fontSize: 11,
|
||||||
|
fontWeight: '500',
|
||||||
|
},
|
||||||
|
})}
|
||||||
|
>
|
||||||
|
<Tab.Screen
|
||||||
|
name="HomeScreen"
|
||||||
|
component={HomeScreen}
|
||||||
|
options={{ tabBarLabel: 'Home' }}
|
||||||
|
/>
|
||||||
|
<Tab.Screen
|
||||||
|
name="SearchScreen"
|
||||||
|
component={SearchScreen}
|
||||||
|
options={{ tabBarLabel: 'Search' }}
|
||||||
|
/>
|
||||||
|
<Tab.Screen
|
||||||
|
name="MyOrdersScreen"
|
||||||
|
component={MyOrdersScreen}
|
||||||
|
options={{ tabBarLabel: 'Orders' }}
|
||||||
|
/>
|
||||||
|
<Tab.Screen
|
||||||
|
name="OffersScreen"
|
||||||
|
component={OffersScreen}
|
||||||
|
options={{ tabBarLabel: 'Offers' }}
|
||||||
|
/>
|
||||||
|
<Tab.Screen
|
||||||
|
name="AccountScreen"
|
||||||
|
component={AccountScreen}
|
||||||
|
options={{ tabBarLabel: 'Account' }}
|
||||||
|
/>
|
||||||
|
</Tab.Navigator>
|
||||||
|
);
|
||||||
|
};
|
||||||
@ -1,18 +1,212 @@
|
|||||||
const delay = (ms: number) => new Promise<void>((resolve) => setTimeout(resolve, ms));
|
import axios, {
|
||||||
|
AxiosInstance,
|
||||||
|
AxiosRequestConfig,
|
||||||
|
InternalAxiosRequestConfig,
|
||||||
|
AxiosResponse,
|
||||||
|
AxiosError,
|
||||||
|
} from 'axios';
|
||||||
|
import AsyncStorage from '@react-native-async-storage/async-storage';
|
||||||
|
import { Store } from '@reduxjs/toolkit';
|
||||||
|
|
||||||
export const apiClient = {
|
// ─── Storage Keys ────────────────────────────────────────────────────────────
|
||||||
async get<T>(url: string, mockData: T): Promise<T> {
|
const STORAGE_KEYS = {
|
||||||
await delay(800);
|
ACCESS_TOKEN: '@auth_access_token',
|
||||||
return mockData;
|
REFRESH_TOKEN: '@auth_refresh_token',
|
||||||
|
} as const;
|
||||||
|
|
||||||
|
// ─── Config ──────────────────────────────────────────────────────────────────
|
||||||
|
const BASE_URL = 'https://api.example.com/v1'; // TODO: replace with your actual base URL
|
||||||
|
|
||||||
|
// ─── Token Helpers ───────────────────────────────────────────────────────────
|
||||||
|
export const tokenManager = {
|
||||||
|
async getAccessToken(): Promise<string | null> {
|
||||||
|
return AsyncStorage.getItem(STORAGE_KEYS.ACCESS_TOKEN);
|
||||||
},
|
},
|
||||||
|
|
||||||
async post<T>(url: string, body: unknown, mockResponse: T): Promise<T> {
|
async getRefreshToken(): Promise<string | null> {
|
||||||
await delay(1000);
|
return AsyncStorage.getItem(STORAGE_KEYS.REFRESH_TOKEN);
|
||||||
return mockResponse;
|
|
||||||
},
|
},
|
||||||
|
|
||||||
async put<T>(url: string, body: unknown, mockResponse: T): Promise<T> {
|
async setTokens(accessToken: string, refreshToken: string): Promise<void> {
|
||||||
await delay(800);
|
await AsyncStorage.setItem(STORAGE_KEYS.ACCESS_TOKEN, accessToken);
|
||||||
return mockResponse;
|
await AsyncStorage.setItem(STORAGE_KEYS.REFRESH_TOKEN, refreshToken);
|
||||||
|
},
|
||||||
|
|
||||||
|
async clearTokens(): Promise<void> {
|
||||||
|
await AsyncStorage.removeItem(STORAGE_KEYS.ACCESS_TOKEN);
|
||||||
|
await AsyncStorage.removeItem(STORAGE_KEYS.REFRESH_TOKEN);
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// ─── Axios Instance ──────────────────────────────────────────────────────────
|
||||||
|
const axiosInstance: AxiosInstance = axios.create({
|
||||||
|
baseURL: BASE_URL,
|
||||||
|
timeout: 15000,
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
Accept: 'application/json',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
// ─── Request Interceptor — Attach Access Token ──────────────────────────────
|
||||||
|
axiosInstance.interceptors.request.use(
|
||||||
|
async (config: InternalAxiosRequestConfig) => {
|
||||||
|
// Skip attaching token for auth endpoints (login, register, refresh)
|
||||||
|
const publicPaths = [
|
||||||
|
'/auth/login',
|
||||||
|
'/auth/register',
|
||||||
|
'/auth/refresh-token',
|
||||||
|
];
|
||||||
|
const isPublic = publicPaths.some(path => config.url?.includes(path));
|
||||||
|
|
||||||
|
if (!isPublic) {
|
||||||
|
const token = await tokenManager.getAccessToken();
|
||||||
|
if (token) {
|
||||||
|
config.headers.Authorization = `Bearer ${token}`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Debug logging (remove in production)
|
||||||
|
if (__DEV__) {
|
||||||
|
console.log(`🚀 [API] ${config.method?.toUpperCase()} ${config.url}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
return config;
|
||||||
|
},
|
||||||
|
(error: AxiosError) => {
|
||||||
|
return Promise.reject(error);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
// ─── Response Interceptor — Handle Token Refresh ────────────────────────────
|
||||||
|
let isRefreshing = false;
|
||||||
|
let failedQueue: Array<{
|
||||||
|
resolve: (token: string) => void;
|
||||||
|
reject: (error: unknown) => void;
|
||||||
|
}> = [];
|
||||||
|
|
||||||
|
const processQueue = (error: unknown, token: string | null = null) => {
|
||||||
|
failedQueue.forEach(({ resolve, reject }) => {
|
||||||
|
if (error) {
|
||||||
|
reject(error);
|
||||||
|
} else if (token) {
|
||||||
|
resolve(token);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
failedQueue = [];
|
||||||
|
};
|
||||||
|
|
||||||
|
axiosInstance.interceptors.response.use(
|
||||||
|
(response: AxiosResponse) => {
|
||||||
|
return response;
|
||||||
|
},
|
||||||
|
async (error: AxiosError) => {
|
||||||
|
const originalRequest = error.config as InternalAxiosRequestConfig & {
|
||||||
|
_retry?: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
|
// If 401 and we haven't already retried this request
|
||||||
|
if (error.response?.status === 401 && !originalRequest._retry) {
|
||||||
|
// If already refreshing, queue this request
|
||||||
|
if (isRefreshing) {
|
||||||
|
return new Promise<AxiosResponse>((resolve, reject) => {
|
||||||
|
failedQueue.push({
|
||||||
|
resolve: (token: string) => {
|
||||||
|
originalRequest.headers.Authorization = `Bearer ${token}`;
|
||||||
|
resolve(axiosInstance(originalRequest));
|
||||||
|
},
|
||||||
|
reject,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
originalRequest._retry = true;
|
||||||
|
isRefreshing = true;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const refreshToken = await tokenManager.getRefreshToken();
|
||||||
|
|
||||||
|
if (!refreshToken) {
|
||||||
|
throw new Error('No refresh token available');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Call refresh endpoint (uses a fresh axios call to avoid interceptor loop)
|
||||||
|
const { data } = await axios.post(`${BASE_URL}/auth/refresh-token`, {
|
||||||
|
refreshToken,
|
||||||
|
});
|
||||||
|
|
||||||
|
const { accessToken: newAccessToken, refreshToken: newRefreshToken } =
|
||||||
|
data;
|
||||||
|
|
||||||
|
await tokenManager.setTokens(newAccessToken, newRefreshToken);
|
||||||
|
|
||||||
|
// Retry all queued requests with the new token
|
||||||
|
processQueue(null, newAccessToken);
|
||||||
|
|
||||||
|
// Retry the original request
|
||||||
|
originalRequest.headers.Authorization = `Bearer ${newAccessToken}`;
|
||||||
|
return axiosInstance(originalRequest);
|
||||||
|
} catch (refreshError) {
|
||||||
|
processQueue(refreshError, null);
|
||||||
|
await tokenManager.clearTokens();
|
||||||
|
|
||||||
|
// TODO: Navigate to login screen or dispatch a logout action
|
||||||
|
// e.g. store.dispatch(logout());
|
||||||
|
|
||||||
|
return Promise.reject(refreshError);
|
||||||
|
} finally {
|
||||||
|
isRefreshing = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// For all other errors, reject as-is
|
||||||
|
return Promise.reject(error);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
let storeInstance: Store | null = null;
|
||||||
|
export const injectStore = (_store: Store | null): void => {
|
||||||
|
storeInstance = _store;
|
||||||
|
};
|
||||||
|
|
||||||
|
// ─── API Methods ─────────────────────────────────────────────────────────────
|
||||||
|
export const apiClient = {
|
||||||
|
async get<T>(url: string, config?: AxiosRequestConfig): Promise<T> {
|
||||||
|
const response = await axiosInstance.get<T>(url, config);
|
||||||
|
return response.data;
|
||||||
|
},
|
||||||
|
|
||||||
|
async post<T>(
|
||||||
|
url: string,
|
||||||
|
body?: unknown,
|
||||||
|
config?: AxiosRequestConfig,
|
||||||
|
): Promise<T> {
|
||||||
|
const response = await axiosInstance.post<T>(url, body, config);
|
||||||
|
return response.data;
|
||||||
|
},
|
||||||
|
|
||||||
|
async put<T>(
|
||||||
|
url: string,
|
||||||
|
body?: unknown,
|
||||||
|
config?: AxiosRequestConfig,
|
||||||
|
): Promise<T> {
|
||||||
|
const response = await axiosInstance.put<T>(url, body, config);
|
||||||
|
return response.data;
|
||||||
|
},
|
||||||
|
|
||||||
|
async patch<T>(
|
||||||
|
url: string,
|
||||||
|
body?: unknown,
|
||||||
|
config?: AxiosRequestConfig,
|
||||||
|
): Promise<T> {
|
||||||
|
const response = await axiosInstance.patch<T>(url, body, config);
|
||||||
|
return response.data;
|
||||||
|
},
|
||||||
|
|
||||||
|
async delete<T>(url: string, config?: AxiosRequestConfig): Promise<T> {
|
||||||
|
const response = await axiosInstance.delete<T>(url, config);
|
||||||
|
return response.data;
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
// Export the raw instance if needed for advanced usage
|
||||||
|
export { axiosInstance };
|
||||||
|
|||||||
@ -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 });
|
|
||||||
},
|
|
||||||
};
|
|
||||||
@ -1,71 +0,0 @@
|
|||||||
import { apiClient } from './apiClient';
|
|
||||||
import { Provider, CatalogItem, Order, DeliveryAgent, Location } from '../interfaces';
|
|
||||||
|
|
||||||
const mockProviders: Provider[] = [
|
|
||||||
{ id: 'p1', imageUrl: '', name: 'Pizza Planet', rating: 4.5, deliveryTime: '25 min', tag: 'Food', discountText: '50% OFF', serviceType: 'Italian' },
|
|
||||||
{ id: 'p2', imageUrl: '', name: 'Fresh Mart', rating: 4.2, deliveryTime: '30 min', tag: 'Groceries', discountText: '20% OFF', serviceType: 'Grocery' },
|
|
||||||
{ id: 'p3', imageUrl: '', name: 'MediQuick', rating: 4.7, deliveryTime: '15 min', tag: 'Pharmacy', discountText: undefined, serviceType: 'Pharmacy' },
|
|
||||||
{ id: 'p4', imageUrl: '', name: 'Burger Barn', rating: 4.3, deliveryTime: '20 min', tag: 'Food', discountText: 'Buy 1 Get 1', serviceType: 'Fast Food' },
|
|
||||||
{ id: 'p5', imageUrl: '', name: 'Green Basket', rating: 4.0, deliveryTime: '35 min', tag: 'Groceries', discountText: undefined, serviceType: 'Organic' },
|
|
||||||
];
|
|
||||||
|
|
||||||
const mockCatalogItems: CatalogItem[] = [
|
|
||||||
{ id: 'i1', imageUrl: '', title: 'Margherita Pizza', description: 'Classic cheese and tomato', price: 299, category: 'Pizza' },
|
|
||||||
{ id: 'i2', imageUrl: '', title: 'Pepperoni Pizza', description: 'Loaded with pepperoni slices', price: 399, category: 'Pizza' },
|
|
||||||
{ id: 'i3', imageUrl: '', title: 'French Fries', description: 'Crispy golden fries', price: 99, category: 'Sides' },
|
|
||||||
{ id: 'i4', imageUrl: '', title: 'Garlic Bread', description: 'Toasted bread with garlic butter', price: 149, category: 'Sides' },
|
|
||||||
{ id: 'i5', imageUrl: '', title: 'Chocolate Shake', description: 'Rich chocolate milkshake', price: 199, category: 'Beverages' },
|
|
||||||
{ id: 'i6', imageUrl: '', title: 'Cola', description: 'Refreshing cold drink', price: 49, category: 'Beverages' },
|
|
||||||
];
|
|
||||||
|
|
||||||
export const deliveryService = {
|
|
||||||
async getProviders() {
|
|
||||||
return apiClient.get('/providers', mockProviders);
|
|
||||||
},
|
|
||||||
|
|
||||||
async getProviderCatalog(providerId: string) {
|
|
||||||
return apiClient.get(`/providers/${providerId}/catalog`, mockCatalogItems);
|
|
||||||
},
|
|
||||||
|
|
||||||
async searchProviders(query: string) {
|
|
||||||
const results = mockProviders.filter(
|
|
||||||
(p) =>
|
|
||||||
p.name.toLowerCase().includes(query.toLowerCase()) ||
|
|
||||||
p.tag.toLowerCase().includes(query.toLowerCase()),
|
|
||||||
);
|
|
||||||
return apiClient.get(`/search?q=${query}`, results);
|
|
||||||
},
|
|
||||||
|
|
||||||
async placeOrder(orderData: Partial<Order>) {
|
|
||||||
return apiClient.post('/orders', orderData, {
|
|
||||||
success: true,
|
|
||||||
order: {
|
|
||||||
id: `ORD-${Date.now()}`,
|
|
||||||
...orderData,
|
|
||||||
status: 'placed' as const,
|
|
||||||
createdAt: new Date().toISOString(),
|
|
||||||
},
|
|
||||||
});
|
|
||||||
},
|
|
||||||
|
|
||||||
async getDeliveryAgent() {
|
|
||||||
const agent: DeliveryAgent = {
|
|
||||||
name: 'Rahul Sharma',
|
|
||||||
rating: 4.8,
|
|
||||||
vehicleDetails: 'KA-01-AB-1234 - Honda Activa',
|
|
||||||
phoneNumber: '+91 98765 43210',
|
|
||||||
};
|
|
||||||
return apiClient.get('/delivery/agent', agent);
|
|
||||||
},
|
|
||||||
|
|
||||||
async getLiveLocation() {
|
|
||||||
const location: Location = {
|
|
||||||
latitude: 12.9352,
|
|
||||||
longitude: 77.6245,
|
|
||||||
address: 'Koramangala 4th Block',
|
|
||||||
city: 'Bengaluru',
|
|
||||||
pincode: '560034',
|
|
||||||
};
|
|
||||||
return apiClient.get('/delivery/live-location', location);
|
|
||||||
},
|
|
||||||
};
|
|
||||||
2
app/services/index.ts
Normal file
2
app/services/index.ts
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
export * from './apiClient';
|
||||||
|
export * from './locationServices';
|
||||||
182
app/services/locationServices.ts
Normal file
182
app/services/locationServices.ts
Normal file
@ -0,0 +1,182 @@
|
|||||||
|
import { Platform, PermissionsAndroid, Alert, Linking } from 'react-native';
|
||||||
|
import Geolocation from 'react-native-geolocation-service';
|
||||||
|
import type { GeoPosition, GeoError } from 'react-native-geolocation-service';
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Types
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
export interface LatLng {
|
||||||
|
latitude: number;
|
||||||
|
longitude: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface LocationResult {
|
||||||
|
coords: LatLng;
|
||||||
|
address: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Constants
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
const GOOGLE_MAPS_API_KEY = 'AIzaSyDKaKVyQCmSpHMXcMkNCccSXpILpuvwsVM';
|
||||||
|
|
||||||
|
/** Default region (Bengaluru) used as a fallback before the user's location loads. */
|
||||||
|
export const DEFAULT_LOCATION: LatLng = {
|
||||||
|
latitude: 12.9352,
|
||||||
|
longitude: 77.6245,
|
||||||
|
};
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Permission helpers
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Request location permission from the user.
|
||||||
|
* Returns `true` if permission was granted, `false` otherwise.
|
||||||
|
*/
|
||||||
|
export const requestLocationPermission = async (): Promise<boolean> => {
|
||||||
|
if (Platform.OS === 'ios') {
|
||||||
|
try {
|
||||||
|
const result = await Geolocation.requestAuthorization('whenInUse');
|
||||||
|
return result === 'granted';
|
||||||
|
} catch {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Android
|
||||||
|
try {
|
||||||
|
const granted = await PermissionsAndroid.request(
|
||||||
|
PermissionsAndroid.PERMISSIONS.ACCESS_FINE_LOCATION,
|
||||||
|
{
|
||||||
|
title: 'Location Permission',
|
||||||
|
message: 'This app needs access to your location to set it on the map.',
|
||||||
|
buttonNeutral: 'Ask Me Later',
|
||||||
|
buttonNegative: 'Cancel',
|
||||||
|
buttonPositive: 'OK',
|
||||||
|
},
|
||||||
|
);
|
||||||
|
console.log('granted', granted);
|
||||||
|
return granted === PermissionsAndroid.RESULTS.GRANTED;
|
||||||
|
} catch {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Show an alert when the user denies location permission, with an option
|
||||||
|
* to open app settings.
|
||||||
|
*/
|
||||||
|
export const showPermissionDeniedAlert = (): void => {
|
||||||
|
Alert.alert(
|
||||||
|
'Location Permission Required',
|
||||||
|
'Please enable location permission in your device settings to use this feature.',
|
||||||
|
[
|
||||||
|
{ text: 'Cancel', style: 'cancel' },
|
||||||
|
{ text: 'Open Settings', onPress: () => Linking.openSettings() },
|
||||||
|
],
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Position helpers
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the device's current GPS position.
|
||||||
|
* Wraps the callback-based Geolocation API in a Promise.
|
||||||
|
*/
|
||||||
|
export const getCurrentPosition = async (): Promise<LatLng> => {
|
||||||
|
try {
|
||||||
|
console.log('get current position');
|
||||||
|
console.log('Geolocation:', Geolocation);
|
||||||
|
console.log('getCurrentPosition:', Geolocation?.getCurrentPosition);
|
||||||
|
|
||||||
|
const location = await new Promise<LatLng>((resolve, reject) => {
|
||||||
|
Geolocation.getCurrentPosition(
|
||||||
|
(position: GeoPosition) => {
|
||||||
|
resolve({
|
||||||
|
latitude: position.coords.latitude,
|
||||||
|
longitude: position.coords.longitude,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
(error: GeoError) => {
|
||||||
|
console.log('Geolocation Error:', error);
|
||||||
|
reject(error);
|
||||||
|
},
|
||||||
|
{
|
||||||
|
enableHighAccuracy: true,
|
||||||
|
timeout: 15000,
|
||||||
|
maximumAge: 10000,
|
||||||
|
forceRequestLocation: true,
|
||||||
|
showLocationDialog: true,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
return location;
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to get current position:', error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Geocoding
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reverse-geocode a lat/lng pair into a human-readable address string
|
||||||
|
* using the Google Maps Geocoding API.
|
||||||
|
*/
|
||||||
|
export const reverseGeocode = async (coords: LatLng): Promise<string> => {
|
||||||
|
try {
|
||||||
|
const url =
|
||||||
|
`https://maps.googleapis.com/maps/api/geocode/json` +
|
||||||
|
`?latlng=${coords.latitude},${coords.longitude}` +
|
||||||
|
`&key=${GOOGLE_MAPS_API_KEY}`;
|
||||||
|
|
||||||
|
const response = await fetch(url);
|
||||||
|
const data = await response.json();
|
||||||
|
|
||||||
|
if (data.status === 'OK' && data.results.length > 0) {
|
||||||
|
// Return the most specific formatted address.
|
||||||
|
return data.results[0].formatted_address;
|
||||||
|
}
|
||||||
|
|
||||||
|
return 'Address not found';
|
||||||
|
} catch (error) {
|
||||||
|
console.log('reverseGeocode error', error);
|
||||||
|
return 'Unable to fetch address';
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Combined helper
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
/**
|
||||||
|
* All-in-one helper: request permissions → get position → reverse geocode.
|
||||||
|
* Returns both the coordinates and the human-readable address.
|
||||||
|
*
|
||||||
|
* Throws if permission is denied or position cannot be obtained.
|
||||||
|
*/
|
||||||
|
export const getCurrentLocationWithAddress =
|
||||||
|
async (): Promise<LocationResult> => {
|
||||||
|
const hasPermission = await requestLocationPermission();
|
||||||
|
|
||||||
|
if (!hasPermission) {
|
||||||
|
showPermissionDeniedAlert();
|
||||||
|
throw new Error('Location permission denied');
|
||||||
|
}
|
||||||
|
|
||||||
|
const coords = await getCurrentPosition();
|
||||||
|
const address = await reverseGeocode(coords);
|
||||||
|
|
||||||
|
console.log('coords', coords);
|
||||||
|
console.log('address', address);
|
||||||
|
|
||||||
|
return { coords, address };
|
||||||
|
};
|
||||||
@ -1,102 +0,0 @@
|
|||||||
import { createSlice, createAsyncThunk, PayloadAction } from '@reduxjs/toolkit';
|
|
||||||
import { User, Location } from '../interfaces';
|
|
||||||
import { authService } from '../services/authService';
|
|
||||||
|
|
||||||
interface AuthState {
|
|
||||||
user: User | null;
|
|
||||||
isAuthenticated: boolean;
|
|
||||||
isLoading: boolean;
|
|
||||||
onboardingComplete: boolean;
|
|
||||||
error: string | null;
|
|
||||||
}
|
|
||||||
|
|
||||||
const initialState: AuthState = {
|
|
||||||
user: null,
|
|
||||||
isAuthenticated: false,
|
|
||||||
isLoading: false,
|
|
||||||
onboardingComplete: false,
|
|
||||||
error: null,
|
|
||||||
};
|
|
||||||
|
|
||||||
export const loginWithPhone = createAsyncThunk(
|
|
||||||
'auth/loginWithPhone',
|
|
||||||
async (mobileNumber: string) => {
|
|
||||||
const response = await authService.login(mobileNumber);
|
|
||||||
return response;
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
export const verifyOtp = createAsyncThunk(
|
|
||||||
'auth/verifyOtp',
|
|
||||||
async ({ mobileNumber, otp }: { mobileNumber: string; otp: string }) => {
|
|
||||||
const response = await authService.verifyOtp(mobileNumber, otp);
|
|
||||||
return response;
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
const authSlice = createSlice({
|
|
||||||
name: 'auth',
|
|
||||||
initialState,
|
|
||||||
reducers: {
|
|
||||||
setUser(state, action: PayloadAction<User>) {
|
|
||||||
state.user = action.payload;
|
|
||||||
},
|
|
||||||
setLocation(state, _action: PayloadAction<Location>) {
|
|
||||||
if (state.user) {
|
|
||||||
state.user = { ...state.user };
|
|
||||||
}
|
|
||||||
},
|
|
||||||
completeProfile(state, action: PayloadAction<{ name: string; email: string; locationLabels: string[] }>) {
|
|
||||||
if (state.user) {
|
|
||||||
state.user = {
|
|
||||||
...state.user,
|
|
||||||
name: action.payload.name,
|
|
||||||
email: action.payload.email,
|
|
||||||
locationLabels: action.payload.locationLabels,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
},
|
|
||||||
completeOnboarding(state) {
|
|
||||||
state.onboardingComplete = true;
|
|
||||||
},
|
|
||||||
logout(state) {
|
|
||||||
state.user = null;
|
|
||||||
state.isAuthenticated = false;
|
|
||||||
state.onboardingComplete = false;
|
|
||||||
state.error = null;
|
|
||||||
},
|
|
||||||
clearError(state) {
|
|
||||||
state.error = null;
|
|
||||||
},
|
|
||||||
},
|
|
||||||
extraReducers: (builder) => {
|
|
||||||
builder
|
|
||||||
.addCase(loginWithPhone.pending, (state) => {
|
|
||||||
state.isLoading = true;
|
|
||||||
state.error = null;
|
|
||||||
})
|
|
||||||
.addCase(loginWithPhone.fulfilled, (state) => {
|
|
||||||
state.isLoading = false;
|
|
||||||
})
|
|
||||||
.addCase(loginWithPhone.rejected, (state, action) => {
|
|
||||||
state.isLoading = false;
|
|
||||||
state.error = action.error.message || 'Login failed';
|
|
||||||
})
|
|
||||||
.addCase(verifyOtp.pending, (state) => {
|
|
||||||
state.isLoading = true;
|
|
||||||
state.error = null;
|
|
||||||
})
|
|
||||||
.addCase(verifyOtp.fulfilled, (state, action) => {
|
|
||||||
state.isLoading = false;
|
|
||||||
state.isAuthenticated = true;
|
|
||||||
state.user = action.payload.user;
|
|
||||||
})
|
|
||||||
.addCase(verifyOtp.rejected, (state, action) => {
|
|
||||||
state.isLoading = false;
|
|
||||||
state.error = action.error.message || 'OTP verification failed';
|
|
||||||
});
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
export const { setUser, setLocation, completeProfile, completeOnboarding, logout, clearError } = authSlice.actions;
|
|
||||||
export default authSlice.reducer;
|
|
||||||
@ -1,96 +0,0 @@
|
|||||||
import { createSlice, PayloadAction } from '@reduxjs/toolkit';
|
|
||||||
import { CartItem, CatalogItem } from '../interfaces';
|
|
||||||
|
|
||||||
interface CartState {
|
|
||||||
items: CartItem[];
|
|
||||||
couponCode: string | null;
|
|
||||||
discount: number;
|
|
||||||
providerId: string | null;
|
|
||||||
providerName: string | null;
|
|
||||||
}
|
|
||||||
|
|
||||||
const initialState: CartState = {
|
|
||||||
items: [],
|
|
||||||
couponCode: null,
|
|
||||||
discount: 0,
|
|
||||||
providerId: null,
|
|
||||||
providerName: null,
|
|
||||||
};
|
|
||||||
|
|
||||||
const cartSlice = createSlice({
|
|
||||||
name: 'cart',
|
|
||||||
initialState,
|
|
||||||
reducers: {
|
|
||||||
addItem(state, action: PayloadAction<{ providerId: string; providerName: string; item: CatalogItem }>) {
|
|
||||||
const { providerId, providerName, item } = action.payload;
|
|
||||||
if (state.providerId && state.providerId !== providerId) {
|
|
||||||
state.items = [];
|
|
||||||
}
|
|
||||||
state.providerId = providerId;
|
|
||||||
state.providerName = providerName;
|
|
||||||
const existing = state.items.find((i) => i.item.id === item.id);
|
|
||||||
if (existing) {
|
|
||||||
existing.quantity += 1;
|
|
||||||
} else {
|
|
||||||
state.items.push({
|
|
||||||
id: `${providerId}-${item.id}`,
|
|
||||||
providerId,
|
|
||||||
providerName,
|
|
||||||
item,
|
|
||||||
quantity: 1,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
},
|
|
||||||
removeItem(state, action: PayloadAction<string>) {
|
|
||||||
const existing = state.items.find((i) => i.item.id === action.payload);
|
|
||||||
if (existing) {
|
|
||||||
if (existing.quantity > 1) {
|
|
||||||
existing.quantity -= 1;
|
|
||||||
} else {
|
|
||||||
state.items = state.items.filter((i) => i.item.id !== action.payload);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (state.items.length === 0) {
|
|
||||||
state.providerId = null;
|
|
||||||
state.providerName = null;
|
|
||||||
}
|
|
||||||
},
|
|
||||||
clearCart(state) {
|
|
||||||
state.items = [];
|
|
||||||
state.couponCode = null;
|
|
||||||
state.discount = 0;
|
|
||||||
state.providerId = null;
|
|
||||||
state.providerName = null;
|
|
||||||
},
|
|
||||||
applyCoupon(state, action: PayloadAction<string>) {
|
|
||||||
state.couponCode = action.payload;
|
|
||||||
state.discount = 50;
|
|
||||||
},
|
|
||||||
removeCoupon(state) {
|
|
||||||
state.couponCode = null;
|
|
||||||
state.discount = 0;
|
|
||||||
},
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
export const { addItem, removeItem, clearCart, applyCoupon, removeCoupon } = cartSlice.actions;
|
|
||||||
|
|
||||||
export const selectCartTotal = (state: { cart: CartState }) => {
|
|
||||||
const subtotal = state.cart.items.reduce(
|
|
||||||
(sum, item) => sum + item.item.price * item.quantity,
|
|
||||||
0,
|
|
||||||
);
|
|
||||||
const deliveryFee = 40;
|
|
||||||
const platformFee = 10;
|
|
||||||
const discount = state.cart.discount;
|
|
||||||
return {
|
|
||||||
subtotal,
|
|
||||||
deliveryFee,
|
|
||||||
platformFee,
|
|
||||||
discount,
|
|
||||||
total: Math.max(0, subtotal + deliveryFee + platformFee - discount),
|
|
||||||
itemCount: state.cart.items.reduce((sum, item) => sum + item.quantity, 0),
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
export default cartSlice.reducer;
|
|
||||||
16
app/store/commonreducers/auth/index.ts
Normal file
16
app/store/commonreducers/auth/index.ts
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
export { default as authReducer } from './reducer';
|
||||||
|
export {
|
||||||
|
setUser,
|
||||||
|
setLocation,
|
||||||
|
completeProfile,
|
||||||
|
completeOnboarding,
|
||||||
|
logout,
|
||||||
|
clearError,
|
||||||
|
} from './reducer';
|
||||||
|
export type { AuthState } from './reducer';
|
||||||
|
export {
|
||||||
|
loginWithPhone,
|
||||||
|
verifyOtp,
|
||||||
|
updateProfile,
|
||||||
|
logoutUser,
|
||||||
|
} from './thunk';
|
||||||
130
app/store/commonreducers/auth/reducer.ts
Normal file
130
app/store/commonreducers/auth/reducer.ts
Normal file
@ -0,0 +1,130 @@
|
|||||||
|
import { createAction, createReducer } from '@reduxjs/toolkit';
|
||||||
|
import { User, Location } from '../../../interfaces';
|
||||||
|
import { loginWithPhone, verifyOtp, updateProfile, logoutUser } from './thunk';
|
||||||
|
|
||||||
|
// ─── State ───────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
export interface AuthState {
|
||||||
|
user: User | null;
|
||||||
|
isAuthenticated: boolean;
|
||||||
|
isLoading: boolean;
|
||||||
|
onboardingComplete: boolean;
|
||||||
|
error: string | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const initialState: AuthState = {
|
||||||
|
user: null,
|
||||||
|
isAuthenticated: false,
|
||||||
|
isLoading: false,
|
||||||
|
onboardingComplete: false,
|
||||||
|
error: null,
|
||||||
|
};
|
||||||
|
|
||||||
|
// ─── Sync Actions ────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
export const setUser = createAction<User>('auth/setUser');
|
||||||
|
export const setLocation = createAction<Location>('auth/setLocation');
|
||||||
|
export const completeProfile = createAction<{
|
||||||
|
name: string;
|
||||||
|
email: string;
|
||||||
|
locationLabels: string[];
|
||||||
|
}>('auth/completeProfile');
|
||||||
|
export const completeOnboarding = createAction('auth/completeOnboarding');
|
||||||
|
export const logout = createAction('auth/logout');
|
||||||
|
export const clearError = createAction('auth/clearError');
|
||||||
|
|
||||||
|
// ─── Reducer ─────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
const authReducer = createReducer(initialState, builder => {
|
||||||
|
builder
|
||||||
|
// ── Sync Actions ───────────────────────────────────────────────────────
|
||||||
|
.addCase(setUser, (state, action) => {
|
||||||
|
state.user = action.payload;
|
||||||
|
})
|
||||||
|
.addCase(setLocation, (state, _action) => {
|
||||||
|
if (state.user) {
|
||||||
|
state.user = { ...state.user };
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.addCase(completeProfile, (state, action) => {
|
||||||
|
if (state.user) {
|
||||||
|
state.user = {
|
||||||
|
...state.user,
|
||||||
|
name: action.payload.name,
|
||||||
|
email: action.payload.email,
|
||||||
|
locationLabels: action.payload.locationLabels,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.addCase(completeOnboarding, state => {
|
||||||
|
state.onboardingComplete = true;
|
||||||
|
})
|
||||||
|
.addCase(logout, state => {
|
||||||
|
state.user = null;
|
||||||
|
state.isAuthenticated = false;
|
||||||
|
state.onboardingComplete = false;
|
||||||
|
state.error = null;
|
||||||
|
})
|
||||||
|
.addCase(clearError, state => {
|
||||||
|
state.error = null;
|
||||||
|
})
|
||||||
|
|
||||||
|
// ── loginWithPhone ─────────────────────────────────────────────────────
|
||||||
|
.addCase(loginWithPhone.pending, state => {
|
||||||
|
state.isLoading = true;
|
||||||
|
state.error = null;
|
||||||
|
})
|
||||||
|
.addCase(loginWithPhone.fulfilled, state => {
|
||||||
|
state.isLoading = false;
|
||||||
|
})
|
||||||
|
.addCase(loginWithPhone.rejected, (state, action) => {
|
||||||
|
state.isLoading = false;
|
||||||
|
state.error = (action.payload as string) || 'Login failed';
|
||||||
|
})
|
||||||
|
|
||||||
|
// ── verifyOtp ──────────────────────────────────────────────────────────
|
||||||
|
.addCase(verifyOtp.pending, state => {
|
||||||
|
state.isLoading = true;
|
||||||
|
state.error = null;
|
||||||
|
})
|
||||||
|
.addCase(verifyOtp.fulfilled, (state, action) => {
|
||||||
|
state.isLoading = false;
|
||||||
|
state.isAuthenticated = true;
|
||||||
|
state.user = action.payload.user;
|
||||||
|
})
|
||||||
|
.addCase(verifyOtp.rejected, (state, action) => {
|
||||||
|
state.isLoading = false;
|
||||||
|
state.error = (action.payload as string) || 'OTP verification failed';
|
||||||
|
})
|
||||||
|
|
||||||
|
// ── updateProfile ──────────────────────────────────────────────────────
|
||||||
|
.addCase(updateProfile.pending, state => {
|
||||||
|
state.isLoading = true;
|
||||||
|
state.error = null;
|
||||||
|
})
|
||||||
|
.addCase(updateProfile.fulfilled, (state, action) => {
|
||||||
|
state.isLoading = false;
|
||||||
|
state.user = action.payload.user;
|
||||||
|
})
|
||||||
|
.addCase(updateProfile.rejected, (state, action) => {
|
||||||
|
state.isLoading = false;
|
||||||
|
state.error = (action.payload as string) || 'Profile update failed';
|
||||||
|
})
|
||||||
|
|
||||||
|
// ── logoutUser ─────────────────────────────────────────────────────────
|
||||||
|
.addCase(logoutUser.fulfilled, state => {
|
||||||
|
state.user = null;
|
||||||
|
state.isAuthenticated = false;
|
||||||
|
state.onboardingComplete = false;
|
||||||
|
state.error = null;
|
||||||
|
})
|
||||||
|
.addCase(logoutUser.rejected, state => {
|
||||||
|
// Still clear state even if API fails (tokens already cleared in thunk)
|
||||||
|
state.user = null;
|
||||||
|
state.isAuthenticated = false;
|
||||||
|
state.onboardingComplete = false;
|
||||||
|
state.error = null;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
export default authReducer;
|
||||||
85
app/store/commonreducers/auth/thunk.ts
Normal file
85
app/store/commonreducers/auth/thunk.ts
Normal file
@ -0,0 +1,85 @@
|
|||||||
|
import { createAsyncThunk } from '@reduxjs/toolkit';
|
||||||
|
import { loginApi, verifyOtpApi, updateProfileApi, logoutApi } from '../../../api/authApi';
|
||||||
|
import { tokenManager } from '../../../services/apiClient';
|
||||||
|
|
||||||
|
// ─── Login ───────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
export const loginWithPhone = createAsyncThunk(
|
||||||
|
'auth/loginWithPhone',
|
||||||
|
async (mobileNumber: string, { rejectWithValue }) => {
|
||||||
|
try {
|
||||||
|
const response = await loginApi(mobileNumber);
|
||||||
|
return response;
|
||||||
|
} catch (error: unknown) {
|
||||||
|
const message =
|
||||||
|
error instanceof Error ? error.message : 'Login failed';
|
||||||
|
return rejectWithValue(message);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
// ─── Verify OTP ──────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
export const verifyOtp = createAsyncThunk(
|
||||||
|
'auth/verifyOtp',
|
||||||
|
async (
|
||||||
|
{ mobileNumber, otp }: { mobileNumber: string; otp: string },
|
||||||
|
{ rejectWithValue },
|
||||||
|
) => {
|
||||||
|
try {
|
||||||
|
const response = await verifyOtpApi(mobileNumber, otp);
|
||||||
|
|
||||||
|
// Persist tokens on successful verification
|
||||||
|
if (response.accessToken && response.refreshToken) {
|
||||||
|
await tokenManager.setTokens(
|
||||||
|
response.accessToken,
|
||||||
|
response.refreshToken,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return response;
|
||||||
|
} catch (error: unknown) {
|
||||||
|
const message =
|
||||||
|
error instanceof Error ? error.message : 'OTP verification failed';
|
||||||
|
return rejectWithValue(message);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
// ─── Update Profile ──────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
export const updateProfile = createAsyncThunk(
|
||||||
|
'auth/updateProfile',
|
||||||
|
async (
|
||||||
|
data: { name: string; email: string; locationLabels: string[] },
|
||||||
|
{ rejectWithValue },
|
||||||
|
) => {
|
||||||
|
try {
|
||||||
|
const response = await updateProfileApi(data);
|
||||||
|
return response;
|
||||||
|
} catch (error: unknown) {
|
||||||
|
const message =
|
||||||
|
error instanceof Error ? error.message : 'Profile update failed';
|
||||||
|
return rejectWithValue(message);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
// ─── Logout ──────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
export const logoutUser = createAsyncThunk(
|
||||||
|
'auth/logoutUser',
|
||||||
|
async (_, { rejectWithValue }) => {
|
||||||
|
try {
|
||||||
|
const response = await logoutApi();
|
||||||
|
await tokenManager.clearTokens();
|
||||||
|
return response;
|
||||||
|
} catch (error: unknown) {
|
||||||
|
// Clear tokens even if API call fails
|
||||||
|
await tokenManager.clearTokens();
|
||||||
|
const message =
|
||||||
|
error instanceof Error ? error.message : 'Logout failed';
|
||||||
|
return rejectWithValue(message);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
);
|
||||||
10
app/store/commonreducers/cart/index.ts
Normal file
10
app/store/commonreducers/cart/index.ts
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
export { default as cartReducer } from './reducer';
|
||||||
|
export {
|
||||||
|
addItem,
|
||||||
|
removeItem,
|
||||||
|
clearCart,
|
||||||
|
applyCoupon,
|
||||||
|
removeCoupon,
|
||||||
|
selectCartTotal,
|
||||||
|
} from './reducer';
|
||||||
|
export type { CartState } from './reducer';
|
||||||
114
app/store/commonreducers/cart/reducer.ts
Normal file
114
app/store/commonreducers/cart/reducer.ts
Normal file
@ -0,0 +1,114 @@
|
|||||||
|
import { createAction, createReducer } from '@reduxjs/toolkit';
|
||||||
|
import { CartItem, CatalogItem } from '../../../interfaces';
|
||||||
|
|
||||||
|
// ─── State ───────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
export interface CartState {
|
||||||
|
items: CartItem[];
|
||||||
|
couponCode: string | null;
|
||||||
|
discount: number;
|
||||||
|
providerId: string | null;
|
||||||
|
providerName: string | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const initialState: CartState = {
|
||||||
|
items: [],
|
||||||
|
couponCode: null,
|
||||||
|
discount: 0,
|
||||||
|
providerId: null,
|
||||||
|
providerName: null,
|
||||||
|
};
|
||||||
|
|
||||||
|
// ─── Actions ─────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
export const addItem = createAction<{
|
||||||
|
providerId: string;
|
||||||
|
providerName: string;
|
||||||
|
item: CatalogItem;
|
||||||
|
}>('cart/addItem');
|
||||||
|
|
||||||
|
export const removeItem = createAction<string>('cart/removeItem');
|
||||||
|
export const clearCart = createAction('cart/clearCart');
|
||||||
|
export const applyCoupon = createAction<string>('cart/applyCoupon');
|
||||||
|
export const removeCoupon = createAction('cart/removeCoupon');
|
||||||
|
|
||||||
|
// ─── Selector ────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
export const selectCartTotal = (state: { cart: CartState }) => {
|
||||||
|
const subtotal = state.cart.items.reduce(
|
||||||
|
(sum, item) => sum + item.item.price * item.quantity,
|
||||||
|
0,
|
||||||
|
);
|
||||||
|
const deliveryFee = 40;
|
||||||
|
const platformFee = 10;
|
||||||
|
const discount = state.cart.discount;
|
||||||
|
return {
|
||||||
|
subtotal,
|
||||||
|
deliveryFee,
|
||||||
|
platformFee,
|
||||||
|
discount,
|
||||||
|
total: Math.max(0, subtotal + deliveryFee + platformFee - discount),
|
||||||
|
itemCount: state.cart.items.reduce((sum, item) => sum + item.quantity, 0),
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
// ─── Reducer ─────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
const cartReducer = createReducer(initialState, builder => {
|
||||||
|
builder
|
||||||
|
.addCase(addItem, (state, action) => {
|
||||||
|
const { providerId, providerName, item } = action.payload;
|
||||||
|
|
||||||
|
// Clear cart if switching providers
|
||||||
|
if (state.providerId && state.providerId !== providerId) {
|
||||||
|
state.items = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
state.providerId = providerId;
|
||||||
|
state.providerName = providerName;
|
||||||
|
|
||||||
|
const existing = state.items.find(i => i.item.id === item.id);
|
||||||
|
if (existing) {
|
||||||
|
existing.quantity += 1;
|
||||||
|
} else {
|
||||||
|
state.items.push({
|
||||||
|
id: `${providerId}-${item.id}`,
|
||||||
|
providerId,
|
||||||
|
providerName,
|
||||||
|
item,
|
||||||
|
quantity: 1,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.addCase(removeItem, (state, action) => {
|
||||||
|
const existing = state.items.find(i => i.item.id === action.payload);
|
||||||
|
if (existing) {
|
||||||
|
if (existing.quantity > 1) {
|
||||||
|
existing.quantity -= 1;
|
||||||
|
} else {
|
||||||
|
state.items = state.items.filter(i => i.item.id !== action.payload);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (state.items.length === 0) {
|
||||||
|
state.providerId = null;
|
||||||
|
state.providerName = null;
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.addCase(clearCart, state => {
|
||||||
|
state.items = [];
|
||||||
|
state.couponCode = null;
|
||||||
|
state.discount = 0;
|
||||||
|
state.providerId = null;
|
||||||
|
state.providerName = null;
|
||||||
|
})
|
||||||
|
.addCase(applyCoupon, (state, action) => {
|
||||||
|
state.couponCode = action.payload;
|
||||||
|
state.discount = 50;
|
||||||
|
})
|
||||||
|
.addCase(removeCoupon, state => {
|
||||||
|
state.couponCode = null;
|
||||||
|
state.discount = 0;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
export default cartReducer;
|
||||||
2
app/store/commonreducers/cart/thunk.ts
Normal file
2
app/store/commonreducers/cart/thunk.ts
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
// Cart thunks — placeholder for future async cart operations
|
||||||
|
// (e.g., syncing cart with backend, applying server-side coupons)
|
||||||
3
app/store/commonreducers/index.ts
Normal file
3
app/store/commonreducers/index.ts
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
export * from './auth';
|
||||||
|
export * from './cart';
|
||||||
|
export * from './order';
|
||||||
9
app/store/commonreducers/order/index.ts
Normal file
9
app/store/commonreducers/order/index.ts
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
export { default as orderReducer } from './reducer';
|
||||||
|
export {
|
||||||
|
placeOrder,
|
||||||
|
updateOrderStatus,
|
||||||
|
setDeliveryAgent,
|
||||||
|
updateLiveLocation,
|
||||||
|
addOrder,
|
||||||
|
} from './reducer';
|
||||||
|
export type { OrderState } from './reducer';
|
||||||
82
app/store/commonreducers/order/reducer.ts
Normal file
82
app/store/commonreducers/order/reducer.ts
Normal file
@ -0,0 +1,82 @@
|
|||||||
|
import { createAction, createReducer } from '@reduxjs/toolkit';
|
||||||
|
import { Order, OrderStatus, TrackingStep, DeliveryAgent, Location } from '../../../interfaces';
|
||||||
|
|
||||||
|
// ─── State ───────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
export interface OrderState {
|
||||||
|
currentOrder: Order | null;
|
||||||
|
orders: Order[];
|
||||||
|
trackingSteps: TrackingStep[];
|
||||||
|
deliveryAgent: DeliveryAgent | null;
|
||||||
|
liveLocation: Location | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const initialState: OrderState = {
|
||||||
|
currentOrder: null,
|
||||||
|
orders: [],
|
||||||
|
trackingSteps: [],
|
||||||
|
deliveryAgent: null,
|
||||||
|
liveLocation: null,
|
||||||
|
};
|
||||||
|
|
||||||
|
// ─── Helpers ─────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
const buildTrackingSteps = (status: OrderStatus): TrackingStep[] => {
|
||||||
|
const steps: { key: OrderStatus; label: string }[] = [
|
||||||
|
{ key: 'placed', label: 'Order Placed' },
|
||||||
|
{ key: 'confirmed', label: 'Confirmed' },
|
||||||
|
{ key: 'dispatched', label: 'Dispatched' },
|
||||||
|
{ key: 'delivered', label: 'Delivered' },
|
||||||
|
];
|
||||||
|
const currentIndex = steps.findIndex(s => s.key === status);
|
||||||
|
return steps.map((step, index) => ({
|
||||||
|
...step,
|
||||||
|
isCompleted: index < currentIndex,
|
||||||
|
isActive: index === currentIndex,
|
||||||
|
}));
|
||||||
|
};
|
||||||
|
|
||||||
|
// ─── Actions ─────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
export const placeOrder = createAction<Order>('order/placeOrder');
|
||||||
|
export const updateOrderStatus = createAction<OrderStatus>('order/updateOrderStatus');
|
||||||
|
export const setDeliveryAgent = createAction<DeliveryAgent>('order/setDeliveryAgent');
|
||||||
|
export const updateLiveLocation = createAction<Location>('order/updateLiveLocation');
|
||||||
|
export const addOrder = createAction<Order>('order/addOrder');
|
||||||
|
|
||||||
|
// ─── Reducer ─────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
const orderReducer = createReducer(initialState, builder => {
|
||||||
|
builder
|
||||||
|
.addCase(placeOrder, (state, action) => {
|
||||||
|
state.currentOrder = action.payload;
|
||||||
|
state.orders.unshift(action.payload);
|
||||||
|
state.trackingSteps = buildTrackingSteps('placed');
|
||||||
|
})
|
||||||
|
.addCase(updateOrderStatus, (state, action) => {
|
||||||
|
if (state.currentOrder) {
|
||||||
|
state.currentOrder.status = action.payload;
|
||||||
|
state.trackingSteps = buildTrackingSteps(action.payload);
|
||||||
|
const idx = state.orders.findIndex(
|
||||||
|
o => o.id === state.currentOrder!.id,
|
||||||
|
);
|
||||||
|
if (idx !== -1) {
|
||||||
|
state.orders[idx] = {
|
||||||
|
...state.orders[idx],
|
||||||
|
status: action.payload,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.addCase(setDeliveryAgent, (state, action) => {
|
||||||
|
state.deliveryAgent = action.payload;
|
||||||
|
})
|
||||||
|
.addCase(updateLiveLocation, (state, action) => {
|
||||||
|
state.liveLocation = action.payload;
|
||||||
|
})
|
||||||
|
.addCase(addOrder, (state, action) => {
|
||||||
|
state.orders.unshift(action.payload);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
export default orderReducer;
|
||||||
2
app/store/commonreducers/order/thunk.ts
Normal file
2
app/store/commonreducers/order/thunk.ts
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
// Order thunks — placeholder for future async order operations
|
||||||
|
// (e.g., fetching order history, real-time status polling)
|
||||||
@ -1,15 +1,4 @@
|
|||||||
import { configureStore } from '@reduxjs/toolkit';
|
export * from './commonreducers';
|
||||||
import authReducer from './authSlice';
|
export * from './store';
|
||||||
import cartReducer from './cartSlice';
|
export * from './rootReducer';
|
||||||
import orderReducer from './orderSlice';
|
export * from './migration';
|
||||||
|
|
||||||
export const store = configureStore({
|
|
||||||
reducer: {
|
|
||||||
auth: authReducer,
|
|
||||||
cart: cartReducer,
|
|
||||||
order: orderReducer,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
export type RootState = ReturnType<typeof store.getState>;
|
|
||||||
export type AppDispatch = typeof store.dispatch;
|
|
||||||
|
|||||||
10
app/store/migration.ts
Normal file
10
app/store/migration.ts
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
/* eslint-disable @typescript-eslint/explicit-module-boundary-types */
|
||||||
|
// Here we avoid typing things as the more migrations we have the more complex types we will have to create
|
||||||
|
|
||||||
|
export const migrations = {
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
|
1: (state: any) => state,
|
||||||
|
};
|
||||||
|
|
||||||
|
const migrationVersions: number[] = Object.keys(migrations).map(k => Number(k));
|
||||||
|
export const persistVersion = migrationVersions[migrationVersions.length - 1];
|
||||||
@ -1,67 +0,0 @@
|
|||||||
import { createSlice, PayloadAction } from '@reduxjs/toolkit';
|
|
||||||
import { Order, OrderStatus, TrackingStep, DeliveryAgent, Location } from '../interfaces';
|
|
||||||
|
|
||||||
interface OrderState {
|
|
||||||
currentOrder: Order | null;
|
|
||||||
orders: Order[];
|
|
||||||
trackingSteps: TrackingStep[];
|
|
||||||
deliveryAgent: DeliveryAgent | null;
|
|
||||||
liveLocation: Location | null;
|
|
||||||
}
|
|
||||||
|
|
||||||
const buildTrackingSteps = (status: OrderStatus): TrackingStep[] => {
|
|
||||||
const steps: { key: OrderStatus; label: string }[] = [
|
|
||||||
{ key: 'placed', label: 'Order Placed' },
|
|
||||||
{ key: 'confirmed', label: 'Confirmed' },
|
|
||||||
{ key: 'dispatched', label: 'Dispatched' },
|
|
||||||
{ key: 'delivered', label: 'Delivered' },
|
|
||||||
];
|
|
||||||
const currentIndex = steps.findIndex((s) => s.key === status);
|
|
||||||
return steps.map((step, index) => ({
|
|
||||||
...step,
|
|
||||||
isCompleted: index < currentIndex,
|
|
||||||
isActive: index === currentIndex,
|
|
||||||
}));
|
|
||||||
};
|
|
||||||
|
|
||||||
const initialState: OrderState = {
|
|
||||||
currentOrder: null,
|
|
||||||
orders: [],
|
|
||||||
trackingSteps: [],
|
|
||||||
deliveryAgent: null,
|
|
||||||
liveLocation: null,
|
|
||||||
};
|
|
||||||
|
|
||||||
const orderSlice = createSlice({
|
|
||||||
name: 'order',
|
|
||||||
initialState,
|
|
||||||
reducers: {
|
|
||||||
placeOrder(state, action: PayloadAction<Order>) {
|
|
||||||
state.currentOrder = action.payload;
|
|
||||||
state.orders.unshift(action.payload);
|
|
||||||
state.trackingSteps = buildTrackingSteps('placed');
|
|
||||||
},
|
|
||||||
updateOrderStatus(state, action: PayloadAction<OrderStatus>) {
|
|
||||||
if (state.currentOrder) {
|
|
||||||
state.currentOrder.status = action.payload;
|
|
||||||
state.trackingSteps = buildTrackingSteps(action.payload);
|
|
||||||
const idx = state.orders.findIndex((o) => o.id === state.currentOrder!.id);
|
|
||||||
if (idx !== -1) {
|
|
||||||
state.orders[idx] = { ...state.orders[idx], status: action.payload };
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
setDeliveryAgent(state, action: PayloadAction<DeliveryAgent>) {
|
|
||||||
state.deliveryAgent = action.payload;
|
|
||||||
},
|
|
||||||
updateLiveLocation(state, action: PayloadAction<Location>) {
|
|
||||||
state.liveLocation = action.payload;
|
|
||||||
},
|
|
||||||
addOrder(state, action: PayloadAction<Order>) {
|
|
||||||
state.orders.unshift(action.payload);
|
|
||||||
},
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
export const { placeOrder, updateOrderStatus, setDeliveryAgent, updateLiveLocation, addOrder } = orderSlice.actions;
|
|
||||||
export default orderSlice.reducer;
|
|
||||||
13
app/store/rootReducer.ts
Normal file
13
app/store/rootReducer.ts
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
import { combineReducers } from '@reduxjs/toolkit';
|
||||||
|
import { authReducer } from './commonreducers/auth';
|
||||||
|
import { cartReducer } from './commonreducers/cart';
|
||||||
|
import { orderReducer } from './commonreducers/order';
|
||||||
|
|
||||||
|
const rootReducer = combineReducers({
|
||||||
|
auth: authReducer,
|
||||||
|
cart: cartReducer,
|
||||||
|
order: orderReducer,
|
||||||
|
});
|
||||||
|
|
||||||
|
export type RootState = ReturnType<typeof rootReducer>;
|
||||||
|
export default rootReducer;
|
||||||
53
app/store/store.ts
Normal file
53
app/store/store.ts
Normal file
@ -0,0 +1,53 @@
|
|||||||
|
import { useDispatch, useSelector, TypedUseSelectorHook } from 'react-redux';
|
||||||
|
import AsyncStorage from '@react-native-async-storage/async-storage';
|
||||||
|
import {
|
||||||
|
createMigrate,
|
||||||
|
FLUSH,
|
||||||
|
PAUSE,
|
||||||
|
PERSIST,
|
||||||
|
persistReducer,
|
||||||
|
persistStore,
|
||||||
|
PURGE,
|
||||||
|
REGISTER,
|
||||||
|
REHYDRATE,
|
||||||
|
} from 'redux-persist';
|
||||||
|
import { ThunkAction } from 'redux-thunk';
|
||||||
|
import { Action, configureStore } from '@reduxjs/toolkit';
|
||||||
|
import { migrations, persistVersion } from './migration';
|
||||||
|
import rootReducer, { RootState } from './rootReducer';
|
||||||
|
import { injectStore } from '../services';
|
||||||
|
|
||||||
|
const persistConfig = {
|
||||||
|
key: 'root',
|
||||||
|
version: persistVersion,
|
||||||
|
storage: AsyncStorage,
|
||||||
|
blacklist: [],
|
||||||
|
migrate: createMigrate(migrations, { debug: __DEV__ }),
|
||||||
|
};
|
||||||
|
|
||||||
|
const persistedReducer = persistReducer(persistConfig, rootReducer);
|
||||||
|
|
||||||
|
export const store = configureStore({
|
||||||
|
reducer: persistedReducer,
|
||||||
|
devTools: __DEV__,
|
||||||
|
middleware: getDefaultMiddleware =>
|
||||||
|
getDefaultMiddleware({
|
||||||
|
immutableCheck: false,
|
||||||
|
serializableCheck: {
|
||||||
|
ignoredActions: [FLUSH, REHYDRATE, PAUSE, PERSIST, PURGE, REGISTER],
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
|
||||||
|
injectStore(store);
|
||||||
|
|
||||||
|
export const persistor = persistStore(store);
|
||||||
|
|
||||||
|
export type AppDispatch = typeof store.dispatch;
|
||||||
|
|
||||||
|
export const useAppDispatch = (): AppDispatch => useDispatch<AppDispatch>();
|
||||||
|
export const useAppSelector: TypedUseSelectorHook<RootState> = useSelector;
|
||||||
|
|
||||||
|
export type AppThunk = ThunkAction<void, RootState, unknown, Action<string>>;
|
||||||
|
|
||||||
|
export default store;
|
||||||
@ -10,22 +10,27 @@
|
|||||||
"test": "jest"
|
"test": "jest"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"@react-native-async-storage/async-storage": "^3.1.1",
|
||||||
"@react-native-community/checkbox": "^0.5.20",
|
"@react-native-community/checkbox": "^0.5.20",
|
||||||
"@react-native/new-app-screen": "0.86.0",
|
"@react-native/new-app-screen": "0.86.0",
|
||||||
"@react-navigation/bottom-tabs": "^7.18.3",
|
"@react-navigation/bottom-tabs": "^7.18.3",
|
||||||
"@react-navigation/native": "^7.3.4",
|
"@react-navigation/native": "^7.3.4",
|
||||||
"@react-navigation/stack": "^7.10.6",
|
"@react-navigation/stack": "^7.10.6",
|
||||||
"@reduxjs/toolkit": "^2.12.0",
|
"@reduxjs/toolkit": "^2.12.0",
|
||||||
|
"axios": "^1.18.1",
|
||||||
"react": "19.2.3",
|
"react": "19.2.3",
|
||||||
"react-native": "0.86.0",
|
"react-native": "0.86.0",
|
||||||
"react-native-gesture-handler": "^3.0.2",
|
"react-native-geolocation-service": "^5.3.1",
|
||||||
|
"react-native-gesture-handler": "^2.32.0",
|
||||||
"react-native-maps": "^1.29.0",
|
"react-native-maps": "^1.29.0",
|
||||||
"react-native-reanimated": "^4.5.0",
|
"react-native-reanimated": "^4.5.0",
|
||||||
"react-native-safe-area-context": "^5.8.0",
|
"react-native-safe-area-context": "^5.8.0",
|
||||||
"react-native-screens": "^4.25.2",
|
"react-native-screens": "^4.25.2",
|
||||||
"react-native-svg": "^15.15.5",
|
"react-native-svg": "^15.15.5",
|
||||||
"react-native-worklets": "^0.10.0",
|
"react-native-worklets": "^0.10.0",
|
||||||
"react-redux": "^9.3.0"
|
"react-redux": "^9.3.0",
|
||||||
|
"redux-persist": "^6.0.0",
|
||||||
|
"redux-thunk": "^3.1.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@babel/core": "^7.25.2",
|
"@babel/core": "^7.25.2",
|
||||||
|
|||||||
194
yarn.lock
194
yarn.lock
@ -1026,6 +1026,13 @@
|
|||||||
resolved "https://registry.yarnpkg.com/@bcoe/v8-coverage/-/v8-coverage-0.2.3.tgz#75a2e8b51cb758a7553d6804a5932d7aace75c39"
|
resolved "https://registry.yarnpkg.com/@bcoe/v8-coverage/-/v8-coverage-0.2.3.tgz#75a2e8b51cb758a7553d6804a5932d7aace75c39"
|
||||||
integrity sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw==
|
integrity sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw==
|
||||||
|
|
||||||
|
"@egjs/hammerjs@^2.0.17":
|
||||||
|
version "2.0.17"
|
||||||
|
resolved "https://registry.yarnpkg.com/@egjs/hammerjs/-/hammerjs-2.0.17.tgz#5dc02af75a6a06e4c2db0202cae38c9263895124"
|
||||||
|
integrity sha512-XQsZgjm2EcVUiZQf11UBJQfmZeEmOW8DpI1gsFeln6w0ae0ii4dMQEQ0kjl6DspdWX1aGY1/loyXnP0JS06e/A==
|
||||||
|
dependencies:
|
||||||
|
"@types/hammerjs" "^2.0.36"
|
||||||
|
|
||||||
"@eslint-community/eslint-utils@^4.2.0", "@eslint-community/eslint-utils@^4.9.1":
|
"@eslint-community/eslint-utils@^4.2.0", "@eslint-community/eslint-utils@^4.9.1":
|
||||||
version "4.9.1"
|
version "4.9.1"
|
||||||
resolved "https://registry.yarnpkg.com/@eslint-community/eslint-utils/-/eslint-utils-4.9.1.tgz#4e90af67bc51ddee6cdef5284edf572ec376b595"
|
resolved "https://registry.yarnpkg.com/@eslint-community/eslint-utils/-/eslint-utils-4.9.1.tgz#4e90af67bc51ddee6cdef5284edf572ec376b595"
|
||||||
@ -1372,6 +1379,13 @@
|
|||||||
"@nodelib/fs.scandir" "2.1.5"
|
"@nodelib/fs.scandir" "2.1.5"
|
||||||
fastq "^1.6.0"
|
fastq "^1.6.0"
|
||||||
|
|
||||||
|
"@react-native-async-storage/async-storage@^3.1.1":
|
||||||
|
version "3.1.1"
|
||||||
|
resolved "https://registry.yarnpkg.com/@react-native-async-storage/async-storage/-/async-storage-3.1.1.tgz#051caeb93919260041e53165f1b6c8a975b15105"
|
||||||
|
integrity sha512-z+PnLz1n6ECKhgoHZHkfc+dijXZEyZnNFSajbtE0NEbsJhmX8x9GlOeiMQIKX2E4DUqPSgfIh4FYBv1M49KgPQ==
|
||||||
|
dependencies:
|
||||||
|
idb "8.0.3"
|
||||||
|
|
||||||
"@react-native-community/checkbox@^0.5.20":
|
"@react-native-community/checkbox@^0.5.20":
|
||||||
version "0.5.20"
|
version "0.5.20"
|
||||||
resolved "https://registry.yarnpkg.com/@react-native-community/checkbox/-/checkbox-0.5.20.tgz#7b8004a80a90988d5dff208693524f069270b849"
|
resolved "https://registry.yarnpkg.com/@react-native-community/checkbox/-/checkbox-0.5.20.tgz#7b8004a80a90988d5dff208693524f069270b849"
|
||||||
@ -1716,18 +1730,18 @@
|
|||||||
nullthrows "^1.1.1"
|
nullthrows "^1.1.1"
|
||||||
|
|
||||||
"@react-navigation/bottom-tabs@^7.18.3":
|
"@react-navigation/bottom-tabs@^7.18.3":
|
||||||
version "7.18.3"
|
version "7.18.4"
|
||||||
resolved "https://registry.yarnpkg.com/@react-navigation/bottom-tabs/-/bottom-tabs-7.18.3.tgz#371c39aee03aa90979d64e8d97da78559fbbcc92"
|
resolved "https://registry.yarnpkg.com/@react-navigation/bottom-tabs/-/bottom-tabs-7.18.4.tgz#a0157c4d449bec8e5440967934c07ef27dca81fc"
|
||||||
integrity sha512-715guj97M1yklVkN9ikUl6KJL9wO6hNot7URyUm+A1r5ltEHVgahKTgR8w7e4dNwiMjUjAnOdSErR0LaX1h+kQ==
|
integrity sha512-L7D1tlPnIsXlp45iZCpAIRRKlZSuqM9WyUNFbooRTlcaaLDSlOr0X6cKbGmJmCvSOoqT6bAsQjCKfb5WRqhtQA==
|
||||||
dependencies:
|
dependencies:
|
||||||
"@react-navigation/elements" "^2.9.26"
|
"@react-navigation/elements" "^2.9.27"
|
||||||
color "^4.2.3"
|
color "^4.2.3"
|
||||||
sf-symbols-typescript "^2.1.0"
|
sf-symbols-typescript "^2.1.0"
|
||||||
|
|
||||||
"@react-navigation/core@^7.21.2":
|
"@react-navigation/core@^7.21.3":
|
||||||
version "7.21.2"
|
version "7.21.3"
|
||||||
resolved "https://registry.yarnpkg.com/@react-navigation/core/-/core-7.21.2.tgz#448591d27d6c1472491a8ea9b901954ea727a310"
|
resolved "https://registry.yarnpkg.com/@react-navigation/core/-/core-7.21.3.tgz#a4fc63da29f3dcbcbfe63da95b32c88d969c980c"
|
||||||
integrity sha512-EsJhQnsL5NuIhXsWF7URijS5hd2AaJUREdq1fOIowlyNNQBA3jCnkGU0DkW7+eI40cQ+GXH8DQw+ci7TefLIxQ==
|
integrity sha512-HUoGmT9IdpKpbAl9AZJmXFbid+jQWQWjzww9vV9uBSd+8fa1hTwM3UBsZBHrpMru0s1ikwknoAIwV827fqGlhA==
|
||||||
dependencies:
|
dependencies:
|
||||||
"@react-navigation/routers" "^7.6.0"
|
"@react-navigation/routers" "^7.6.0"
|
||||||
escape-string-regexp "^4.0.0"
|
escape-string-regexp "^4.0.0"
|
||||||
@ -1738,21 +1752,21 @@
|
|||||||
use-latest-callback "^0.2.4"
|
use-latest-callback "^0.2.4"
|
||||||
use-sync-external-store "^1.5.0"
|
use-sync-external-store "^1.5.0"
|
||||||
|
|
||||||
"@react-navigation/elements@^2.9.26":
|
"@react-navigation/elements@^2.9.27":
|
||||||
version "2.9.26"
|
version "2.9.27"
|
||||||
resolved "https://registry.yarnpkg.com/@react-navigation/elements/-/elements-2.9.26.tgz#89cd09db890534c115031b1a6d99d916fdcda96c"
|
resolved "https://registry.yarnpkg.com/@react-navigation/elements/-/elements-2.9.27.tgz#74247f508a7ad38fe03321cd6acfbfd3d1898716"
|
||||||
integrity sha512-EaHSJf42MjnH5VFL+KZfQIyqJvdQWK9Z27J7WUSuIVX5NXlR925sPmhWf540DX9gD1ny8BfUGPZAuw/PO4tK2w==
|
integrity sha512-6bka/gWvP3+5Dw9Gf2ac83y/F0XEl2ktezc6Yp3gJBs22Rm9dluGKphe1B2Hj33XoCMRofxL8w3z3kmtlJiC0Q==
|
||||||
dependencies:
|
dependencies:
|
||||||
color "^4.2.3"
|
color "^4.2.3"
|
||||||
use-latest-callback "^0.2.4"
|
use-latest-callback "^0.2.4"
|
||||||
use-sync-external-store "^1.5.0"
|
use-sync-external-store "^1.5.0"
|
||||||
|
|
||||||
"@react-navigation/native@^7.3.4":
|
"@react-navigation/native@^7.3.4":
|
||||||
version "7.3.4"
|
version "7.3.5"
|
||||||
resolved "https://registry.yarnpkg.com/@react-navigation/native/-/native-7.3.4.tgz#a9ba7b0fde595b019a16a87ab609ad5b618bfbef"
|
resolved "https://registry.yarnpkg.com/@react-navigation/native/-/native-7.3.5.tgz#d56ba57e130844f5eeac4c9d06e57386834c8330"
|
||||||
integrity sha512-zyAqFLeZuaakerMMnhYqBeGAzJ+WFQUTgezP3FxSlXNwFq5XxnjP28vi2XHGqm4zg4pGWls9Ay4JKshIHUOpwA==
|
integrity sha512-lzcRpMoLCIypXXKm8KhpyZ81XpQBO/i8Oy1due4kUR97kG8kBlJxKUeP8yNnT5Fcl85rNdLXWbpwEr+9HPu4Ig==
|
||||||
dependencies:
|
dependencies:
|
||||||
"@react-navigation/core" "^7.21.2"
|
"@react-navigation/core" "^7.21.3"
|
||||||
escape-string-regexp "^4.0.0"
|
escape-string-regexp "^4.0.0"
|
||||||
fast-deep-equal "^3.1.3"
|
fast-deep-equal "^3.1.3"
|
||||||
nanoid "^3.3.11"
|
nanoid "^3.3.11"
|
||||||
@ -1767,11 +1781,11 @@
|
|||||||
nanoid "^3.3.11"
|
nanoid "^3.3.11"
|
||||||
|
|
||||||
"@react-navigation/stack@^7.10.6":
|
"@react-navigation/stack@^7.10.6":
|
||||||
version "7.10.6"
|
version "7.10.7"
|
||||||
resolved "https://registry.yarnpkg.com/@react-navigation/stack/-/stack-7.10.6.tgz#5adf039d106a097bcb5514f07c1afc9e3ec75483"
|
resolved "https://registry.yarnpkg.com/@react-navigation/stack/-/stack-7.10.7.tgz#76b91b693bbc00182090a2c71743cb8c5754409b"
|
||||||
integrity sha512-LWtBSCPNi3Htnb3051wan5hVTsoORxUocBeuAjZ3VS1u6wTtfObeqOOhanz/L+uiAzvTWcAHhevQhtlGeFn/mg==
|
integrity sha512-rkJ9CEJnOHRSBVYNq7M8+0a34munKcTho/HSJiniRM3ZDjWR0aQKDXZcYpKi+IQ8jKv2XyYOvZLGHrBNJc5ipA==
|
||||||
dependencies:
|
dependencies:
|
||||||
"@react-navigation/elements" "^2.9.26"
|
"@react-navigation/elements" "^2.9.27"
|
||||||
color "^4.2.3"
|
color "^4.2.3"
|
||||||
use-latest-callback "^0.2.4"
|
use-latest-callback "^0.2.4"
|
||||||
|
|
||||||
@ -1878,6 +1892,11 @@
|
|||||||
dependencies:
|
dependencies:
|
||||||
"@types/node" "*"
|
"@types/node" "*"
|
||||||
|
|
||||||
|
"@types/hammerjs@^2.0.36":
|
||||||
|
version "2.0.46"
|
||||||
|
resolved "https://registry.yarnpkg.com/@types/hammerjs/-/hammerjs-2.0.46.tgz#381daaca1360ff8a7c8dff63f32e69745b9fb1e1"
|
||||||
|
integrity sha512-ynRvcq6wvqexJ9brDMS4BnBLzmr0e14d6ZJTEShTBWKymQiHwlAyGu0ZPEFI2Fh1U53F7tN9ufClWM5KvqkKOw==
|
||||||
|
|
||||||
"@types/istanbul-lib-coverage@*", "@types/istanbul-lib-coverage@^2.0.0", "@types/istanbul-lib-coverage@^2.0.1":
|
"@types/istanbul-lib-coverage@*", "@types/istanbul-lib-coverage@^2.0.0", "@types/istanbul-lib-coverage@^2.0.1":
|
||||||
version "2.0.6"
|
version "2.0.6"
|
||||||
resolved "https://registry.yarnpkg.com/@types/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.6.tgz#7739c232a1fee9b4d3ce8985f314c0c6d33549d7"
|
resolved "https://registry.yarnpkg.com/@types/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.6.tgz#7739c232a1fee9b4d3ce8985f314c0c6d33549d7"
|
||||||
@ -1906,9 +1925,9 @@
|
|||||||
pretty-format "^29.0.0"
|
pretty-format "^29.0.0"
|
||||||
|
|
||||||
"@types/node@*":
|
"@types/node@*":
|
||||||
version "26.0.1"
|
version "26.1.0"
|
||||||
resolved "https://registry.yarnpkg.com/@types/node/-/node-26.0.1.tgz#4a60e2c7a6d68bd261e265f8983bfe1601263ce3"
|
resolved "https://registry.yarnpkg.com/@types/node/-/node-26.1.0.tgz#aa85f0727fc5611347091c478341c63650903439"
|
||||||
integrity sha512-fc3KiUoBt6kie0N9bIW3E47vZsuaMf0PM2AaUpLCLT0s/LvX1nxAim6Fc049cNxODPpGm6qRAuUOB86SkRuPQw==
|
integrity sha512-O0A1G3xPGy4w7AgQdAQYUlQ+BKk2Oovw8eRpofyp5KdBZULnbe+WqaOVNrm705SHphCiG4XHsACrSmPu1f+Kgw==
|
||||||
dependencies:
|
dependencies:
|
||||||
undici-types "~8.3.0"
|
undici-types "~8.3.0"
|
||||||
|
|
||||||
@ -2087,6 +2106,13 @@ acorn@^8.15.0, acorn@^8.9.0:
|
|||||||
resolved "https://registry.yarnpkg.com/acorn/-/acorn-8.17.0.tgz#1785adb84faf8d8add10369b93826fc2bd08f1fe"
|
resolved "https://registry.yarnpkg.com/acorn/-/acorn-8.17.0.tgz#1785adb84faf8d8add10369b93826fc2bd08f1fe"
|
||||||
integrity sha512-xRQbDb9BnwDafYNn6Vwl839DYVjqXYb1XVGtWAZ1kcDc6iwAL4hg3B1dZlRiuENFeO2H53gFG3in621AdERVAg==
|
integrity sha512-xRQbDb9BnwDafYNn6Vwl839DYVjqXYb1XVGtWAZ1kcDc6iwAL4hg3B1dZlRiuENFeO2H53gFG3in621AdERVAg==
|
||||||
|
|
||||||
|
agent-base@6:
|
||||||
|
version "6.0.2"
|
||||||
|
resolved "https://registry.yarnpkg.com/agent-base/-/agent-base-6.0.2.tgz#49fff58577cfee3f37176feab4c22e00f86d7f77"
|
||||||
|
integrity sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==
|
||||||
|
dependencies:
|
||||||
|
debug "4"
|
||||||
|
|
||||||
agent-base@^7.1.2:
|
agent-base@^7.1.2:
|
||||||
version "7.1.4"
|
version "7.1.4"
|
||||||
resolved "https://registry.yarnpkg.com/agent-base/-/agent-base-7.1.4.tgz#e3cd76d4c548ee895d3c3fd8dc1f6c5b9032e7a8"
|
resolved "https://registry.yarnpkg.com/agent-base/-/agent-base-7.1.4.tgz#e3cd76d4c548ee895d3c3fd8dc1f6c5b9032e7a8"
|
||||||
@ -2275,6 +2301,11 @@ async-limiter@~1.0.0:
|
|||||||
resolved "https://registry.yarnpkg.com/async-limiter/-/async-limiter-1.0.1.tgz#dd379e94f0db8310b08291f9d64c3209766617fd"
|
resolved "https://registry.yarnpkg.com/async-limiter/-/async-limiter-1.0.1.tgz#dd379e94f0db8310b08291f9d64c3209766617fd"
|
||||||
integrity sha512-csOlWGAcRFJaI6m+F2WKdnMKr4HhdhFVBk0H/QbJFMCr+uO2kwohwXQPxw/9OCxp05r5ghVBFSyioixx3gfkNQ==
|
integrity sha512-csOlWGAcRFJaI6m+F2WKdnMKr4HhdhFVBk0H/QbJFMCr+uO2kwohwXQPxw/9OCxp05r5ghVBFSyioixx3gfkNQ==
|
||||||
|
|
||||||
|
asynckit@^0.4.0:
|
||||||
|
version "0.4.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/asynckit/-/asynckit-0.4.0.tgz#c79ed97f7f34cb8f2ba1bc9790bcc366474b4b79"
|
||||||
|
integrity sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==
|
||||||
|
|
||||||
available-typed-arrays@^1.0.7:
|
available-typed-arrays@^1.0.7:
|
||||||
version "1.0.7"
|
version "1.0.7"
|
||||||
resolved "https://registry.yarnpkg.com/available-typed-arrays/-/available-typed-arrays-1.0.7.tgz#a5cc375d6a03c2efc87a553f3e0b1522def14846"
|
resolved "https://registry.yarnpkg.com/available-typed-arrays/-/available-typed-arrays-1.0.7.tgz#a5cc375d6a03c2efc87a553f3e0b1522def14846"
|
||||||
@ -2282,6 +2313,16 @@ available-typed-arrays@^1.0.7:
|
|||||||
dependencies:
|
dependencies:
|
||||||
possible-typed-array-names "^1.0.0"
|
possible-typed-array-names "^1.0.0"
|
||||||
|
|
||||||
|
axios@^1.18.1:
|
||||||
|
version "1.18.1"
|
||||||
|
resolved "https://registry.yarnpkg.com/axios/-/axios-1.18.1.tgz#d63f9863bcd8938815c86f9e2abd380189d96dfe"
|
||||||
|
integrity sha512-3nTvFlvpn9Zu/RkHUqtc7/+al4UpRW5az71ap5zccp6e8RAYEzhMTecX8Dz1wWDYrPpUoB1HAQEGEAEvUr7S9g==
|
||||||
|
dependencies:
|
||||||
|
follow-redirects "^1.16.0"
|
||||||
|
form-data "^4.0.5"
|
||||||
|
https-proxy-agent "^5.0.1"
|
||||||
|
proxy-from-env "^2.1.0"
|
||||||
|
|
||||||
babel-jest@^29.7.0:
|
babel-jest@^29.7.0:
|
||||||
version "29.7.0"
|
version "29.7.0"
|
||||||
resolved "https://registry.yarnpkg.com/babel-jest/-/babel-jest-29.7.0.tgz#f4369919225b684c56085998ac63dbd05be020d5"
|
resolved "https://registry.yarnpkg.com/babel-jest/-/babel-jest-29.7.0.tgz#f4369919225b684c56085998ac63dbd05be020d5"
|
||||||
@ -2704,6 +2745,13 @@ colorette@^1.0.7:
|
|||||||
resolved "https://registry.yarnpkg.com/colorette/-/colorette-1.4.0.tgz#5190fbb87276259a86ad700bff2c6d6faa3fca40"
|
resolved "https://registry.yarnpkg.com/colorette/-/colorette-1.4.0.tgz#5190fbb87276259a86ad700bff2c6d6faa3fca40"
|
||||||
integrity sha512-Y2oEozpomLn7Q3HFP7dpww7AtMJplbM9lGZP6RDfHqmbeRjiwRg4n6VM6j4KLmRke85uWEI7JqF17f3pqdRA0g==
|
integrity sha512-Y2oEozpomLn7Q3HFP7dpww7AtMJplbM9lGZP6RDfHqmbeRjiwRg4n6VM6j4KLmRke85uWEI7JqF17f3pqdRA0g==
|
||||||
|
|
||||||
|
combined-stream@^1.0.8:
|
||||||
|
version "1.0.8"
|
||||||
|
resolved "https://registry.yarnpkg.com/combined-stream/-/combined-stream-1.0.8.tgz#c3d45a8b34fd730631a110a8a2520682b31d5a7f"
|
||||||
|
integrity sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==
|
||||||
|
dependencies:
|
||||||
|
delayed-stream "~1.0.0"
|
||||||
|
|
||||||
command-exists@^1.2.8:
|
command-exists@^1.2.8:
|
||||||
version "1.2.9"
|
version "1.2.9"
|
||||||
resolved "https://registry.yarnpkg.com/command-exists/-/command-exists-1.2.9.tgz#c50725af3808c8ab0260fd60b01fbfa25b954f69"
|
resolved "https://registry.yarnpkg.com/command-exists/-/command-exists-1.2.9.tgz#c50725af3808c8ab0260fd60b01fbfa25b954f69"
|
||||||
@ -2933,6 +2981,11 @@ define-properties@^1.1.3, define-properties@^1.2.1:
|
|||||||
has-property-descriptors "^1.0.0"
|
has-property-descriptors "^1.0.0"
|
||||||
object-keys "^1.1.1"
|
object-keys "^1.1.1"
|
||||||
|
|
||||||
|
delayed-stream@~1.0.0:
|
||||||
|
version "1.0.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/delayed-stream/-/delayed-stream-1.0.0.tgz#df3ae199acadfb7d440aaae0b29e2272b24ec619"
|
||||||
|
integrity sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==
|
||||||
|
|
||||||
depd@2.0.0, depd@~2.0.0:
|
depd@2.0.0, depd@~2.0.0:
|
||||||
version "2.0.0"
|
version "2.0.0"
|
||||||
resolved "https://registry.yarnpkg.com/depd/-/depd-2.0.0.tgz#b696163cc757560d09cf22cc8fad1571b79e76df"
|
resolved "https://registry.yarnpkg.com/depd/-/depd-2.0.0.tgz#b696163cc757560d09cf22cc8fad1571b79e76df"
|
||||||
@ -3012,9 +3065,9 @@ ee-first@1.1.1:
|
|||||||
integrity sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==
|
integrity sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==
|
||||||
|
|
||||||
electron-to-chromium@^1.5.376:
|
electron-to-chromium@^1.5.376:
|
||||||
version "1.5.382"
|
version "1.5.383"
|
||||||
resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.5.382.tgz#e76f05d3ec337524b9c61761ebbc16fe91ecf5b4"
|
resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.5.383.tgz#5bd22306497d454103b289b0fef97260c56d0855"
|
||||||
integrity sha512-8ETaWbV6SZOrno+G93Ffd9ENsMtetqdnqj4nlfxFW90Sm5GgnuV28Kf62hqQVD6VUgzm7qFQKsTsAPmeUiU3Ug==
|
integrity sha512-I2484/KkAvl8lm9VyjH2JnbOIV0d/UCqT7gbzs6l+o6Vmn9wgB66uVcKX+Vk6HrXtY6fbWTOEXuv8waDTuFNCw==
|
||||||
|
|
||||||
emittery@^0.13.1:
|
emittery@^0.13.1:
|
||||||
version "0.13.1"
|
version "0.13.1"
|
||||||
@ -3613,6 +3666,11 @@ flow-enums-runtime@^0.0.6:
|
|||||||
resolved "https://registry.yarnpkg.com/flow-enums-runtime/-/flow-enums-runtime-0.0.6.tgz#5bb0cd1b0a3e471330f4d109039b7eba5cb3e787"
|
resolved "https://registry.yarnpkg.com/flow-enums-runtime/-/flow-enums-runtime-0.0.6.tgz#5bb0cd1b0a3e471330f4d109039b7eba5cb3e787"
|
||||||
integrity sha512-3PYnM29RFXwvAN6Pc/scUfkI7RwhQ/xqyLUyPNlXUp9S40zI8nup9tUSrTLSVnWGBN38FNiGWbwZOB6uR4OGdw==
|
integrity sha512-3PYnM29RFXwvAN6Pc/scUfkI7RwhQ/xqyLUyPNlXUp9S40zI8nup9tUSrTLSVnWGBN38FNiGWbwZOB6uR4OGdw==
|
||||||
|
|
||||||
|
follow-redirects@^1.16.0:
|
||||||
|
version "1.16.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.16.0.tgz#28474a159d3b9d11ef62050a14ed60e4df6d61bc"
|
||||||
|
integrity sha512-y5rN/uOsadFT/JfYwhxRS5R7Qce+g3zG97+JrtFZlC9klX/W5hD7iiLzScI4nZqUS7DNUdhPgw4xI8W2LuXlUw==
|
||||||
|
|
||||||
for-each@^0.3.3, for-each@^0.3.5:
|
for-each@^0.3.3, for-each@^0.3.5:
|
||||||
version "0.3.5"
|
version "0.3.5"
|
||||||
resolved "https://registry.yarnpkg.com/for-each/-/for-each-0.3.5.tgz#d650688027826920feeb0af747ee7b9421a41d47"
|
resolved "https://registry.yarnpkg.com/for-each/-/for-each-0.3.5.tgz#d650688027826920feeb0af747ee7b9421a41d47"
|
||||||
@ -3620,6 +3678,17 @@ for-each@^0.3.3, for-each@^0.3.5:
|
|||||||
dependencies:
|
dependencies:
|
||||||
is-callable "^1.2.7"
|
is-callable "^1.2.7"
|
||||||
|
|
||||||
|
form-data@^4.0.5:
|
||||||
|
version "4.0.6"
|
||||||
|
resolved "https://registry.yarnpkg.com/form-data/-/form-data-4.0.6.tgz#28e864e1b786dbebb68db1f452f9635278665827"
|
||||||
|
integrity sha512-vKatAh4SlVfgbv+YtmhiRjhEMJsYpsG1Y2rMQtR+SVSbytsSD1YGzDIcrAJmdFec88u/+VoGmxnl+80gL1tRCQ==
|
||||||
|
dependencies:
|
||||||
|
asynckit "^0.4.0"
|
||||||
|
combined-stream "^1.0.8"
|
||||||
|
es-set-tostringtag "^2.1.0"
|
||||||
|
hasown "^2.0.4"
|
||||||
|
mime-types "^2.1.35"
|
||||||
|
|
||||||
fresh@~0.5.2:
|
fresh@~0.5.2:
|
||||||
version "0.5.2"
|
version "0.5.2"
|
||||||
resolved "https://registry.yarnpkg.com/fresh/-/fresh-0.5.2.tgz#3d8cadd90d976569fa835ab1f8e4b23a105605a7"
|
resolved "https://registry.yarnpkg.com/fresh/-/fresh-0.5.2.tgz#3d8cadd90d976569fa835ab1f8e4b23a105605a7"
|
||||||
@ -3877,6 +3946,13 @@ hermes-parser@^0.25.1:
|
|||||||
dependencies:
|
dependencies:
|
||||||
hermes-estree "0.25.1"
|
hermes-estree "0.25.1"
|
||||||
|
|
||||||
|
hoist-non-react-statics@^3.3.0:
|
||||||
|
version "3.3.2"
|
||||||
|
resolved "https://registry.yarnpkg.com/hoist-non-react-statics/-/hoist-non-react-statics-3.3.2.tgz#ece0acaf71d62c2969c2ec59feff42a4b1a85b45"
|
||||||
|
integrity sha512-/gGivxi8JPKWNm/W0jSmzcMPpfpPLc3dY/6GxhX2hQ9iGj3aDfklV4ET7NjKpSinLpJ5vafa9iiGIEZg10SfBw==
|
||||||
|
dependencies:
|
||||||
|
react-is "^16.7.0"
|
||||||
|
|
||||||
html-escaper@^2.0.0:
|
html-escaper@^2.0.0:
|
||||||
version "2.0.2"
|
version "2.0.2"
|
||||||
resolved "https://registry.yarnpkg.com/html-escaper/-/html-escaper-2.0.2.tgz#dfd60027da36a36dfcbe236262c00a5822681453"
|
resolved "https://registry.yarnpkg.com/html-escaper/-/html-escaper-2.0.2.tgz#dfd60027da36a36dfcbe236262c00a5822681453"
|
||||||
@ -3893,6 +3969,14 @@ http-errors@~2.0.1:
|
|||||||
statuses "~2.0.2"
|
statuses "~2.0.2"
|
||||||
toidentifier "~1.0.1"
|
toidentifier "~1.0.1"
|
||||||
|
|
||||||
|
https-proxy-agent@^5.0.1:
|
||||||
|
version "5.0.1"
|
||||||
|
resolved "https://registry.yarnpkg.com/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz#c59ef224a04fe8b754f3db0063a25ea30d0005d6"
|
||||||
|
integrity sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==
|
||||||
|
dependencies:
|
||||||
|
agent-base "6"
|
||||||
|
debug "4"
|
||||||
|
|
||||||
https-proxy-agent@^7.0.5:
|
https-proxy-agent@^7.0.5:
|
||||||
version "7.0.6"
|
version "7.0.6"
|
||||||
resolved "https://registry.yarnpkg.com/https-proxy-agent/-/https-proxy-agent-7.0.6.tgz#da8dfeac7da130b05c2ba4b59c9b6cd66611a6b9"
|
resolved "https://registry.yarnpkg.com/https-proxy-agent/-/https-proxy-agent-7.0.6.tgz#da8dfeac7da130b05c2ba4b59c9b6cd66611a6b9"
|
||||||
@ -3913,6 +3997,11 @@ iconv-lite@~0.4.24:
|
|||||||
dependencies:
|
dependencies:
|
||||||
safer-buffer ">= 2.1.2 < 3"
|
safer-buffer ">= 2.1.2 < 3"
|
||||||
|
|
||||||
|
idb@8.0.3:
|
||||||
|
version "8.0.3"
|
||||||
|
resolved "https://registry.yarnpkg.com/idb/-/idb-8.0.3.tgz#c91e558f15a8d53f1d7f53a094d226fc3ad71fd9"
|
||||||
|
integrity sha512-LtwtVyVYO5BqRvcsKuB2iUMnHwPVByPCXFXOpuU96IZPPoPN6xjOGxZQ74pgSVVLQWtUOYgyeL4GE98BY5D3wg==
|
||||||
|
|
||||||
ieee754@^1.1.13:
|
ieee754@^1.1.13:
|
||||||
version "1.2.1"
|
version "1.2.1"
|
||||||
resolved "https://registry.yarnpkg.com/ieee754/-/ieee754-1.2.1.tgz#8eb7a10a63fff25d15a57b001586d177d1b0d352"
|
resolved "https://registry.yarnpkg.com/ieee754/-/ieee754-1.2.1.tgz#8eb7a10a63fff25d15a57b001586d177d1b0d352"
|
||||||
@ -3936,9 +4025,9 @@ image-size@^1.0.2:
|
|||||||
queue "6.0.2"
|
queue "6.0.2"
|
||||||
|
|
||||||
immer@^11.0.0:
|
immer@^11.0.0:
|
||||||
version "11.1.8"
|
version "11.1.9"
|
||||||
resolved "https://registry.yarnpkg.com/immer/-/immer-11.1.8.tgz#08a6426f7019dbce8d6dff8c4a43bb25c550a575"
|
resolved "https://registry.yarnpkg.com/immer/-/immer-11.1.9.tgz#e0ce69359d29cafdb14fc5b6d2cd56f826097966"
|
||||||
integrity sha512-/tbkHMW7y10Lx6i1crLjD4/OhNkRG+Fo7byZHtah0547nIeXYcpIXaUh0IAQY6gO5459qpGGYapcEOHtFXkIuA==
|
integrity sha512-sc/z0Cyti70bZa0ZU4sWfAElfovFb9Ni8tArJZLuklYWxegPiK3pDOql1Rq5H0FIRAW9LSQRG6OX4KqBldbhBA==
|
||||||
|
|
||||||
import-fresh@^3.2.1, import-fresh@^3.3.0:
|
import-fresh@^3.2.1, import-fresh@^3.3.0:
|
||||||
version "3.3.1"
|
version "3.3.1"
|
||||||
@ -5141,6 +5230,13 @@ mime-db@1.52.0:
|
|||||||
resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.54.0.tgz#cddb3ee4f9c64530dff640236661d42cb6a314f5"
|
resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.54.0.tgz#cddb3ee4f9c64530dff640236661d42cb6a314f5"
|
||||||
integrity sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==
|
integrity sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==
|
||||||
|
|
||||||
|
mime-types@^2.1.35, mime-types@~2.1.24, mime-types@~2.1.34:
|
||||||
|
version "2.1.35"
|
||||||
|
resolved "https://registry.yarnpkg.com/mime-types/-/mime-types-2.1.35.tgz#381a871b62a734450660ae3deee44813f70d959a"
|
||||||
|
integrity sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==
|
||||||
|
dependencies:
|
||||||
|
mime-db "1.52.0"
|
||||||
|
|
||||||
mime-types@^3.0.0, mime-types@^3.0.1:
|
mime-types@^3.0.0, mime-types@^3.0.1:
|
||||||
version "3.0.2"
|
version "3.0.2"
|
||||||
resolved "https://registry.yarnpkg.com/mime-types/-/mime-types-3.0.2.tgz#39002d4182575d5af036ffa118100f2524b2e2ab"
|
resolved "https://registry.yarnpkg.com/mime-types/-/mime-types-3.0.2.tgz#39002d4182575d5af036ffa118100f2524b2e2ab"
|
||||||
@ -5148,13 +5244,6 @@ mime-types@^3.0.0, mime-types@^3.0.1:
|
|||||||
dependencies:
|
dependencies:
|
||||||
mime-db "^1.54.0"
|
mime-db "^1.54.0"
|
||||||
|
|
||||||
mime-types@~2.1.24, mime-types@~2.1.34:
|
|
||||||
version "2.1.35"
|
|
||||||
resolved "https://registry.yarnpkg.com/mime-types/-/mime-types-2.1.35.tgz#381a871b62a734450660ae3deee44813f70d959a"
|
|
||||||
integrity sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==
|
|
||||||
dependencies:
|
|
||||||
mime-db "1.52.0"
|
|
||||||
|
|
||||||
mime@1.6.0:
|
mime@1.6.0:
|
||||||
version "1.6.0"
|
version "1.6.0"
|
||||||
resolved "https://registry.yarnpkg.com/mime/-/mime-1.6.0.tgz#32cd9e5c64553bd58d19a568af452acff04981b1"
|
resolved "https://registry.yarnpkg.com/mime/-/mime-1.6.0.tgz#32cd9e5c64553bd58d19a568af452acff04981b1"
|
||||||
@ -5620,6 +5709,11 @@ prop-types@^15.8.1:
|
|||||||
object-assign "^4.1.1"
|
object-assign "^4.1.1"
|
||||||
react-is "^16.13.1"
|
react-is "^16.13.1"
|
||||||
|
|
||||||
|
proxy-from-env@^2.1.0:
|
||||||
|
version "2.1.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/proxy-from-env/-/proxy-from-env-2.1.0.tgz#a7487568adad577cfaaa7e88c49cab3ab3081aba"
|
||||||
|
integrity sha512-cJ+oHTW1VAEa8cJslgmUZrc+sjRKgAKl3Zyse6+PV38hZe/V6Z14TbCuXcan9F9ghlz4QrFr2c92TNF82UkYHA==
|
||||||
|
|
||||||
punycode@^2.1.0:
|
punycode@^2.1.0:
|
||||||
version "2.3.1"
|
version "2.3.1"
|
||||||
resolved "https://registry.yarnpkg.com/punycode/-/punycode-2.3.1.tgz#027422e2faec0b25e1549c3e1bd8309b9133b6e5"
|
resolved "https://registry.yarnpkg.com/punycode/-/punycode-2.3.1.tgz#027422e2faec0b25e1549c3e1bd8309b9133b6e5"
|
||||||
@ -5688,7 +5782,7 @@ react-freeze@^1.0.0:
|
|||||||
resolved "https://registry.yarnpkg.com/react-freeze/-/react-freeze-1.0.4.tgz#cbbea2762b0368b05cbe407ddc9d518c57c6f3ad"
|
resolved "https://registry.yarnpkg.com/react-freeze/-/react-freeze-1.0.4.tgz#cbbea2762b0368b05cbe407ddc9d518c57c6f3ad"
|
||||||
integrity sha512-r4F0Sec0BLxWicc7HEyo2x3/2icUTrRmDjaaRyzzn+7aDyFZliszMDOgLVwSnQnYENOlL1o569Ze2HZefk8clA==
|
integrity sha512-r4F0Sec0BLxWicc7HEyo2x3/2icUTrRmDjaaRyzzn+7aDyFZliszMDOgLVwSnQnYENOlL1o569Ze2HZefk8clA==
|
||||||
|
|
||||||
react-is@^16.13.1:
|
react-is@^16.13.1, react-is@^16.7.0:
|
||||||
version "16.13.1"
|
version "16.13.1"
|
||||||
resolved "https://registry.yarnpkg.com/react-is/-/react-is-16.13.1.tgz#789729a4dc36de2999dc156dd6c1d9c18cea56a4"
|
resolved "https://registry.yarnpkg.com/react-is/-/react-is-16.13.1.tgz#789729a4dc36de2999dc156dd6c1d9c18cea56a4"
|
||||||
integrity sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==
|
integrity sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==
|
||||||
@ -5703,12 +5797,19 @@ react-is@^19.1.0, react-is@^19.2.3:
|
|||||||
resolved "https://registry.yarnpkg.com/react-is/-/react-is-19.2.7.tgz#57668ee86a78574a542b0a539455212b2c086df2"
|
resolved "https://registry.yarnpkg.com/react-is/-/react-is-19.2.7.tgz#57668ee86a78574a542b0a539455212b2c086df2"
|
||||||
integrity sha512-kZFnouyVv7eP/Phmrlo9FK+zcAdriZJvzxXHF1Sl1P377WSGe2G/JxVolhTrB/jeV47lKImhNUsijjHAAbcl/A==
|
integrity sha512-kZFnouyVv7eP/Phmrlo9FK+zcAdriZJvzxXHF1Sl1P377WSGe2G/JxVolhTrB/jeV47lKImhNUsijjHAAbcl/A==
|
||||||
|
|
||||||
react-native-gesture-handler@^3.0.2:
|
react-native-geolocation-service@^5.3.1:
|
||||||
version "3.0.2"
|
version "5.3.1"
|
||||||
resolved "https://registry.yarnpkg.com/react-native-gesture-handler/-/react-native-gesture-handler-3.0.2.tgz#ea80fd2424d08f8b624977221f0544a5201f6d83"
|
resolved "https://registry.yarnpkg.com/react-native-geolocation-service/-/react-native-geolocation-service-5.3.1.tgz#4ce1017789da6fdfcf7576eb6f59435622af4289"
|
||||||
integrity sha512-XBTgmvr2jh/xrMwlHTV2Z1x6IFUl3zfLxyaCAnvj3GSXK5YlY3ZmiIs57hniPmh3I7RtjPFxtGdRr0i+PzyBrg==
|
integrity sha512-LTXPtPNmrdhx+yeWG47sAaCgQc3nG1z+HLLHlhK/5YfOgfLcAb9HAkhREPjQKPZOUx8pKZMIpdGFUGfJYtimXQ==
|
||||||
|
|
||||||
|
react-native-gesture-handler@^2.32.0:
|
||||||
|
version "2.32.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/react-native-gesture-handler/-/react-native-gesture-handler-2.32.0.tgz#59b8658a4e1586ce6fb0ca0053ac80f6acf7f882"
|
||||||
|
integrity sha512-uYIMOKlKENORq2SABE+jIjbPU+h5I/sQKcq2v16zRq848nwEp1fWRVwML4QWqijc8UcXJC25o54S8GQd4Mf2OA==
|
||||||
dependencies:
|
dependencies:
|
||||||
|
"@egjs/hammerjs" "^2.0.17"
|
||||||
"@types/react-test-renderer" "^19.1.0"
|
"@types/react-test-renderer" "^19.1.0"
|
||||||
|
hoist-non-react-statics "^3.3.0"
|
||||||
invariant "^2.2.4"
|
invariant "^2.2.4"
|
||||||
|
|
||||||
react-native-is-edge-to-edge@^1.3.1:
|
react-native-is-edge-to-edge@^1.3.1:
|
||||||
@ -5753,9 +5854,9 @@ react-native-svg@^15.15.5:
|
|||||||
css-tree "^1.1.3"
|
css-tree "^1.1.3"
|
||||||
|
|
||||||
react-native-worklets@^0.10.0:
|
react-native-worklets@^0.10.0:
|
||||||
version "0.10.0"
|
version "0.10.1"
|
||||||
resolved "https://registry.yarnpkg.com/react-native-worklets/-/react-native-worklets-0.10.0.tgz#e2e8f8e1ba0eccc4e69f3a4e75894ecab4ba19b0"
|
resolved "https://registry.yarnpkg.com/react-native-worklets/-/react-native-worklets-0.10.1.tgz#7a3038a3b5ec66cab030a00e1568b6599696f5f2"
|
||||||
integrity sha512-JhE6IxDf6iabC0qu3+TAKA4v9RlluXmoIngPQX7/QUByf75lfrsHZ6/dQhyjEWnp1EEQiwzz8Cpew140ZcewDw==
|
integrity sha512-62mRM19bDpfpdI8HLkEErcdOsrAPDtE9lA/sw+5lLRpzBHNhxaoj9QyY2KjXqUmirelxkX4zuPGTC3VdA0feJA==
|
||||||
dependencies:
|
dependencies:
|
||||||
"@babel/plugin-transform-arrow-functions" "^7.27.1"
|
"@babel/plugin-transform-arrow-functions" "^7.27.1"
|
||||||
"@babel/plugin-transform-class-properties" "^7.28.6"
|
"@babel/plugin-transform-class-properties" "^7.28.6"
|
||||||
@ -5843,6 +5944,11 @@ readable-stream@^3.4.0:
|
|||||||
string_decoder "^1.1.1"
|
string_decoder "^1.1.1"
|
||||||
util-deprecate "^1.0.1"
|
util-deprecate "^1.0.1"
|
||||||
|
|
||||||
|
redux-persist@^6.0.0:
|
||||||
|
version "6.0.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/redux-persist/-/redux-persist-6.0.0.tgz#b4d2972f9859597c130d40d4b146fecdab51b3a8"
|
||||||
|
integrity sha512-71LLMbUq2r02ng2We9S215LtPu3fY0KgaGE0k8WRgl6RkqxtGfl7HUozz1Dftwsb0D/5mZ8dwAaPbtnzfvbEwQ==
|
||||||
|
|
||||||
redux-thunk@^3.1.0:
|
redux-thunk@^3.1.0:
|
||||||
version "3.1.0"
|
version "3.1.0"
|
||||||
resolved "https://registry.yarnpkg.com/redux-thunk/-/redux-thunk-3.1.0.tgz#94aa6e04977c30e14e892eae84978c1af6058ff3"
|
resolved "https://registry.yarnpkg.com/redux-thunk/-/redux-thunk-3.1.0.tgz#94aa6e04977c30e14e892eae84978c1af6058ff3"
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user