Compare commits

..

No commits in common. "feat(api-integration)" and "main" have entirely different histories.

101 changed files with 1253 additions and 4848 deletions

View File

@ -1,5 +0,0 @@
import Reactotron from 'reactotron-react-native';
Reactotron.configure() // controls connection & communication settings
.useReactNative() // add all built-in react native plugins
.connect(); // let's connect!

View File

@ -1,8 +1,20 @@
import { apiClient } from '../services/apiClient'; import { apiClient } from '../services/apiClient';
import { LoginResponse, User, VerifyOtpResponse } from '../interfaces'; import { User } from '../interfaces';
// ─── Types ─────────────────────────────────────────────────────────────────── // ─── Types ───────────────────────────────────────────────────────────────────
export interface LoginResponse {
success: boolean;
message: string;
}
export interface VerifyOtpResponse {
success: boolean;
user: User;
accessToken: string;
refreshToken: string;
}
export interface UpdateProfileResponse { export interface UpdateProfileResponse {
success: boolean; success: boolean;
user: User; user: User;
@ -14,11 +26,11 @@ export interface LogoutResponse {
// ─── Endpoints ─────────────────────────────────────────────────────────────── // ─── Endpoints ───────────────────────────────────────────────────────────────
export const loginApi = (phone: string) => export const loginApi = (mobileNumber: string) =>
apiClient.post<LoginResponse>('/auth/otp/request', { phone }); apiClient.post<LoginResponse>('/auth/login', { mobileNumber });
export const verifyOtpApi = (phone: string, code: string, role: string) => export const verifyOtpApi = (mobileNumber: string, otp: string) =>
apiClient.post<VerifyOtpResponse>('/auth/otp/verify', { phone, code, role }); apiClient.post<VerifyOtpResponse>('/auth/verify-otp', { mobileNumber, otp });
export const updateProfileApi = (data: { export const updateProfileApi = (data: {
name: string; name: string;

View File

@ -1,16 +0,0 @@
import { AddToCartRequest, CartResponse } from '@interfaces/cart';
import { apiClient } from '@services';
export const addToCartApi = async (addToCartRequest: AddToCartRequest) => {
return apiClient.post<CartResponse>('/cart/items', addToCartRequest);
};
export const getCartApi = async () => {
return apiClient.get<CartResponse>('/cart');
};
export const updateCartItem = async (productId: string, quantity: number) => {
return apiClient.patch<CartResponse>(`/cart/items/${productId}`, {
quantity,
});
};

View File

@ -1,6 +0,0 @@
import { CustomerResponse } from '@interfaces';
import { apiClient } from '@services';
export const getCustomerDetails = async () => {
return await apiClient.get<CustomerResponse>('/customers/profile');
};

View File

@ -1,11 +1,5 @@
import { apiClient } from '../services/apiClient'; import { apiClient } from '../services/apiClient';
import { import { Provider, CatalogItem, Order, DeliveryAgent, Location } from '../interfaces';
Provider,
CatalogItem,
Order,
DeliveryAgent,
Location,
} from '../interfaces';
// ─── Types ─────────────────────────────────────────────────────────────────── // ─── Types ───────────────────────────────────────────────────────────────────
@ -16,7 +10,8 @@ export interface PlaceOrderResponse {
// ─── Endpoints ─────────────────────────────────────────────────────────────── // ─── Endpoints ───────────────────────────────────────────────────────────────
export const getProvidersApi = () => apiClient.get<Provider[]>('/providers'); export const getProvidersApi = () =>
apiClient.get<Provider[]>('/providers');
export const getProviderCatalogApi = (providerId: string) => export const getProviderCatalogApi = (providerId: string) =>
apiClient.get<CatalogItem[]>(`/providers/${providerId}/catalog`); apiClient.get<CatalogItem[]>(`/providers/${providerId}/catalog`);
@ -24,8 +19,8 @@ export const getProviderCatalogApi = (providerId: string) =>
export const searchProvidersApi = (query: string) => export const searchProvidersApi = (query: string) =>
apiClient.get<Provider[]>(`/search?q=${query}`); apiClient.get<Provider[]>(`/search?q=${query}`);
// export const placeOrderApi = (orderData: Partial<Order>) => export const placeOrderApi = (orderData: Partial<Order>) =>
// apiClient.post<PlaceOrderResponse>('/orders', orderData); apiClient.post<PlaceOrderResponse>('/orders', orderData);
export const getDeliveryAgentApi = () => export const getDeliveryAgentApi = () =>
apiClient.get<DeliveryAgent>('/delivery/agent'); apiClient.get<DeliveryAgent>('/delivery/agent');

View File

@ -1,8 +1,2 @@
export * from './authApi'; export * from './authApi';
export * from './deliveryApi'; export * from './deliveryApi';
export * from './onboardApi';
export * from './productApi';
export * from './cartApi';
export * from './paymentMethodsApi';
export * from './customerDetailsApi';
export * from './orderApi';

View File

@ -1,12 +0,0 @@
import { Category, onBoardPayload, UserProfile } from '@interfaces/onboard';
import { apiClient } from '@services';
export const getCatagories = async (): Promise<Category[]> => {
return await apiClient.get('/categories');
};
export const onBoardComplete = async (
payload: onBoardPayload,
): Promise<UserProfile> => {
return await apiClient.post('/auth/onboard', payload);
};

View File

@ -1,14 +0,0 @@
import { apiClient } from '@services';
import { Order, OrderRequest, PlaceOrderResponse } from '@interfaces';
export const placeOrderApi = async (payload: OrderRequest) => {
return await apiClient.post<PlaceOrderResponse>('/orders', payload);
};
export const getOrderHistoryApi = async () => {
return await apiClient.get<Order[]>('/orders');
};
export const getOrderByIdApi = async (orderId: string) => {
return await apiClient.get<Order>(`/orders/${orderId}`);
};

View File

@ -1,6 +0,0 @@
import { PaymentMethodsResponse } from '@interfaces';
import { apiClient } from '@services';
export const getAllPaymentMethodsApi = async () => {
return await apiClient.get<PaymentMethodsResponse>('/payments/options');
};

View File

@ -1,17 +0,0 @@
import { Products, PaginationMeta, Product } from '@interfaces';
import { apiClient } from '@services';
export interface GetProductsResponse {
products: Products[];
meta: PaginationMeta;
}
export const getProductsApi = async (): Promise<GetProductsResponse> => {
return await apiClient.get<GetProductsResponse>(`/products`);
};
export const getProductDetailsApi = async (
productId: string,
): Promise<Product> => {
return await apiClient.get<Product>(`/products/${productId}`);
};

View File

View File

@ -1,7 +0,0 @@
import { OrderStatus, TrackingEntry } from '@interfaces';
export interface OrderStatusTimelineProps {
tracking?: TrackingEntry[];
currentStatus: OrderStatus;
styles?: any; // optional component uses its own internal styles via useAppTheme
}

View File

@ -1,317 +0,0 @@
import { StyleSheet } from 'react-native';
import { typography } from '@theme';
export const getStyles = (colors: any) =>
StyleSheet.create({
// Status Card / Container
cardContainer: {
backgroundColor: colors.cardBg,
borderRadius: 16,
borderWidth: 1,
borderColor: colors.border,
overflow: 'hidden',
},
headerSection: {
padding: 16,
flexDirection: 'row',
alignItems: 'center',
borderBottomWidth: 1,
borderBottomColor: colors.border,
},
headerIconContainer: {
width: 48,
height: 48,
borderRadius: 24,
backgroundColor: '#E8F5E9',
justifyContent: 'center',
alignItems: 'center',
marginRight: 16,
},
headerIconText: {
fontSize: 24,
},
headerTextContainer: {
flex: 1,
},
statusHeaderTitle: {
fontSize: typography.fontSize.lg,
fontWeight: typography.fontWeight.bold,
color: colors.text,
marginBottom: 4,
},
statusHeaderDesc: {
fontSize: typography.fontSize.sm,
color: colors.textSecondary,
lineHeight: 18,
},
// Horizontal Stepper
stepperWrapper: {
paddingVertical: 20,
paddingHorizontal: 12,
borderBottomWidth: 1,
borderBottomColor: colors.border,
},
stepperRow: {
flexDirection: 'row',
alignItems: 'center',
justifyContent: 'space-between',
position: 'relative',
},
stepLineBackground: {
position: 'absolute',
top: 14,
left: '12%',
right: '12%',
height: 3,
backgroundColor: colors.border,
zIndex: 1,
},
stepLineActive: {
position: 'absolute',
top: 14,
left: '12%',
height: 3,
backgroundColor: colors.primary,
zIndex: 2,
},
stepItem: {
alignItems: 'center',
flex: 1,
zIndex: 3,
},
stepDot: {
width: 28,
height: 28,
borderRadius: 14,
backgroundColor: colors.cardBg,
borderWidth: 2,
borderColor: colors.border,
justifyContent: 'center',
alignItems: 'center',
marginBottom: 6,
},
stepDotCompleted: {
borderColor: colors.primary,
backgroundColor: colors.primary,
},
stepDotActive: {
borderColor: colors.primary,
backgroundColor: colors.cardBg,
borderWidth: 3,
},
stepCheckmark: {
color: '#FFFFFF',
fontSize: 12,
fontWeight: 'bold',
},
stepInnerDotActive: {
width: 10,
height: 10,
borderRadius: 5,
backgroundColor: colors.primary,
},
stepLabel: {
fontSize: 11,
fontWeight: typography.fontWeight.medium,
color: colors.textSecondary,
textAlign: 'center',
},
stepLabelActive: {
color: colors.primary,
fontWeight: typography.fontWeight.bold,
},
// Driver Section
driverSection: {
padding: 16,
borderBottomWidth: 1,
borderBottomColor: colors.border,
backgroundColor: colors.inputBg,
},
driverInfoRow: {
flexDirection: 'row',
alignItems: 'center',
marginBottom: 12,
},
driverAvatarContainer: {
width: 44,
height: 44,
borderRadius: 22,
backgroundColor: colors.primary,
justifyContent: 'center',
alignItems: 'center',
marginRight: 12,
},
driverAvatarText: {
color: '#FFFFFF',
fontSize: 16,
fontWeight: typography.fontWeight.bold,
},
driverMeta: {
flex: 1,
},
driverRoleText: {
fontSize: 10,
textTransform: 'uppercase',
letterSpacing: 0.5,
color: colors.textSecondary,
fontWeight: typography.fontWeight.semibold,
marginBottom: 2,
},
driverNameText: {
fontSize: typography.fontSize.md,
fontWeight: typography.fontWeight.bold,
color: colors.text,
},
driverVehicleText: {
fontSize: typography.fontSize.xs,
color: colors.textSecondary,
marginTop: 2,
},
ratingBadge: {
flexDirection: 'row',
alignItems: 'center',
backgroundColor: '#FFF9C4',
paddingHorizontal: 8,
paddingVertical: 4,
borderRadius: 12,
},
ratingText: {
fontSize: 12,
fontWeight: typography.fontWeight.bold,
color: '#F57F17',
},
driverActionsRow: {
flexDirection: 'row',
justifyContent: 'space-between',
},
actionButton: {
flex: 1,
flexDirection: 'row',
height: 40,
borderRadius: 8,
justifyContent: 'center',
alignItems: 'center',
borderWidth: 1,
},
callButton: {
backgroundColor: colors.primary,
borderColor: colors.primary,
marginRight: 6,
},
messageButton: {
backgroundColor: colors.cardBg,
borderColor: colors.border,
marginLeft: 6,
},
actionIcon: {
marginRight: 6,
fontSize: 16,
},
callButtonText: {
color: '#FFFFFF',
fontWeight: typography.fontWeight.bold,
fontSize: typography.fontSize.sm,
},
messageButtonText: {
color: colors.text,
fontWeight: typography.fontWeight.semibold,
fontSize: typography.fontSize.sm,
},
// Collapsible Trigger / Detailed Logs
toggleSection: {
alignItems: 'center',
justifyContent: 'center',
paddingVertical: 12,
},
toggleButton: {
flexDirection: 'row',
alignItems: 'center',
paddingHorizontal: 16,
paddingVertical: 4,
},
toggleButtonText: {
fontSize: typography.fontSize.sm,
fontWeight: typography.fontWeight.bold,
color: colors.primary,
marginRight: 4,
},
toggleChevron: {
fontSize: 12,
color: colors.primary,
},
detailedTimelineWrapper: {
paddingHorizontal: 20,
paddingBottom: 16,
},
// Legacy vertical timeline classes (re-styled for premium feel)
timelineRow: {
flexDirection: 'row',
},
timelineDotColumn: {
alignItems: 'center',
width: 24,
},
timelineDot: {
width: 14,
height: 14,
borderRadius: 7,
borderWidth: 2,
borderColor: colors.border,
backgroundColor: colors.cardBg,
justifyContent: 'center',
alignItems: 'center',
marginTop: 4,
},
timelineDotCompleted: {
borderColor: colors.primary,
backgroundColor: colors.primary,
},
timelineDotCancelled: {
borderColor: colors.error,
backgroundColor: colors.error,
},
timelineCheck: {
color: '#FFFFFF',
fontSize: 8,
fontWeight: 'bold',
},
timelineLine: {
width: 2,
flex: 1,
minHeight: 36,
backgroundColor: colors.border,
marginVertical: 4,
},
timelineLineCompleted: {
backgroundColor: colors.primary,
},
timelineContent: {
flex: 1,
paddingBottom: 18,
paddingLeft: 16,
},
timelineLabelCompleted: {
fontSize: typography.fontSize.sm,
fontWeight: typography.fontWeight.semibold,
color: colors.text,
},
timelineLabelPending: {
fontSize: typography.fontSize.sm,
fontWeight: typography.fontWeight.regular,
color: colors.textSecondary,
},
timelineLabelCancelled: {
fontSize: typography.fontSize.sm,
fontWeight: typography.fontWeight.bold,
color: colors.error,
},
timelineTime: {
fontSize: typography.fontSize.xs,
color: colors.textSecondary,
marginTop: 4,
},
});

View File

@ -1,377 +0,0 @@
import React, { useState } from 'react';
import {
View,
Text,
TouchableOpacity,
Linking,
Platform,
LayoutAnimation,
UIManager,
} from 'react-native';
import { OrderStatus, TrackingEntry } from '@interfaces';
import { formatDate, formatTime } from '@utils/helper';
import { OrderStatusTimelineProps } from './OrderStatusTimeline.props';
import { useAppTheme } from '@theme';
import { getStyles } from './OrderStatusTimeline.styles';
if (
Platform.OS === 'android' &&
UIManager.setLayoutAnimationEnabledExperimental
) {
UIManager.setLayoutAnimationEnabledExperimental(true);
}
export const STATUS_FLOW: OrderStatus[] = [
'PENDING',
'CONFIRMED',
'PREPARING',
'READY_FOR_PICKUP',
'OUT_FOR_DELIVERY',
'DELIVERED',
];
export const STATUS_LABEL: Record<OrderStatus, string> = {
PENDING: 'Order Placed',
CONFIRMED: 'Confirmed',
PREPARING: 'Preparing',
READY_FOR_PICKUP: 'Ready for Pickup',
OUT_FOR_DELIVERY: 'Out for Delivery',
DELIVERED: 'Delivered',
CANCELLED: 'Cancelled',
};
const STATUS_DETAILS: Record<
OrderStatus,
{ title: string; desc: string; icon: string }
> = {
PENDING: {
title: 'Waiting for Confirmation',
desc: 'The merchant is reviewing your order. We will start preparing it soon.',
icon: '⏳',
},
CONFIRMED: {
title: 'Order Confirmed',
desc: 'Your order has been accepted. The chef will start preparing it shortly.',
icon: '✅',
},
PREPARING: {
title: 'Preparing Your Food',
desc: 'The kitchen is preparing your delicious order with fresh ingredients.',
icon: '🍳',
},
READY_FOR_PICKUP: {
title: 'Ready for Pickup',
desc: 'Your order is ready. The delivery partner is picking it up now.',
icon: '📦',
},
OUT_FOR_DELIVERY: {
title: 'Out for Delivery',
desc: 'Our delivery partner is on the way with your food. Keep your phone handy!',
icon: '🛵',
},
DELIVERED: {
title: 'Order Delivered',
desc: 'Your order has been successfully delivered. Enjoy your meal!',
icon: '🎉',
},
CANCELLED: {
title: 'Order Cancelled',
desc: 'This order was cancelled. Any refund will be processed shortly.',
icon: '❌',
},
};
const getStepIndex = (status: OrderStatus): number => {
switch (status) {
case 'PENDING':
case 'CONFIRMED':
return 0;
case 'PREPARING':
return 1;
case 'READY_FOR_PICKUP':
case 'OUT_FOR_DELIVERY':
return 2;
case 'DELIVERED':
return 3;
default:
return 0;
}
};
export const OrderStatusTimeline: React.FC<OrderStatusTimelineProps> = ({
tracking = [],
currentStatus,
styles: propStyles,
}) => {
const { colors } = useAppTheme();
const localStyles = getStyles(colors);
// If the caller passes the screen styles, they won't have timelineRow etc, so we fallback to localStyles
const styles =
propStyles && propStyles.timelineRow ? propStyles : localStyles;
const [expanded, setExpanded] = useState(false);
const toggleExpand = () => {
LayoutAnimation.configureNext(LayoutAnimation.Presets.easeInEaseOut);
setExpanded(!expanded);
};
const handleCall = () => {
Linking.openURL('tel:+919876543210');
};
const handleMessage = () => {
Linking.openURL('sms:+919876543210');
};
const trackingMap = tracking.reduce<Record<string, string>>((acc, t) => {
acc[t.status] = t.time;
return acc;
}, {});
const details = STATUS_DETAILS[currentStatus] || STATUS_DETAILS.PENDING;
if (currentStatus === 'CANCELLED') {
return (
<View style={localStyles.cardContainer}>
{/* Cancelled Status Banner */}
<View style={localStyles.headerSection}>
<View
style={[
localStyles.headerIconContainer,
{ backgroundColor: '#FFEBEE' },
]}
>
<Text style={localStyles.headerIconText}></Text>
</View>
<View style={localStyles.headerTextContainer}>
<Text
style={[localStyles.statusHeaderTitle, { color: colors.error }]}
>
{details.title}
</Text>
<Text style={localStyles.statusHeaderDesc}>{details.desc}</Text>
</View>
</View>
{/* Collapsible detailed view */}
<View style={localStyles.toggleSection}>
<TouchableOpacity
onPress={toggleExpand}
style={localStyles.toggleButton}
activeOpacity={0.7}
>
<Text style={localStyles.toggleButtonText}>
{expanded ? 'Hide Details' : 'View Detailed History'}
</Text>
<Text style={localStyles.toggleChevron}>
{expanded ? '▲' : '▼'}
</Text>
</TouchableOpacity>
</View>
{expanded && (
<View style={localStyles.detailedTimelineWrapper}>
<View style={styles.timelineRow}>
<View style={styles.timelineDotColumn}>
<View
style={[styles.timelineDot, styles.timelineDotCancelled]}
/>
</View>
<View style={styles.timelineContent}>
<Text style={styles.timelineLabelCancelled}>
Order Cancelled
</Text>
{trackingMap['CANCELLED'] && (
<Text style={styles.timelineTime}>
{formatDate(trackingMap['CANCELLED'])} at{' '}
{formatTime(trackingMap['CANCELLED'])}
</Text>
)}
</View>
</View>
</View>
)}
</View>
);
}
const activeStep = getStepIndex(currentStatus);
const steps = [
{ label: 'Placed', status: 'PENDING' },
{ label: 'Preparing', status: 'PREPARING' },
{ label: 'On the Way', status: 'OUT_FOR_DELIVERY' },
{ label: 'Delivered', status: 'DELIVERED' },
];
const showDriver = [
'PREPARING',
'READY_FOR_PICKUP',
'OUT_FOR_DELIVERY',
'DELIVERED',
].includes(currentStatus);
const currentIndex = STATUS_FLOW.indexOf(currentStatus);
return (
<View style={localStyles.cardContainer}>
{/* Current Status Header Banner */}
<View style={localStyles.headerSection}>
<View style={localStyles.headerIconContainer}>
<Text style={localStyles.headerIconText}>{details.icon}</Text>
</View>
<View style={localStyles.headerTextContainer}>
<Text style={localStyles.statusHeaderTitle}>{details.title}</Text>
<Text style={localStyles.statusHeaderDesc}>{details.desc}</Text>
</View>
</View>
{/* Horizontal Progress Stepper */}
<View style={localStyles.stepperWrapper}>
<View style={localStyles.stepperRow}>
<View style={localStyles.stepLineBackground} />
<View
style={[
localStyles.stepLineActive,
{ width: `${(activeStep / (steps.length - 1)) * 76}%` },
]}
/>
{steps.map((step, idx) => {
const isCompleted = idx < activeStep;
const isActive = idx === activeStep;
return (
<View key={step.label} style={localStyles.stepItem}>
<View
style={[
localStyles.stepDot,
isCompleted && localStyles.stepDotCompleted,
isActive && localStyles.stepDotActive,
]}
>
{isCompleted ? (
<Text style={localStyles.stepCheckmark}></Text>
) : isActive ? (
<View style={localStyles.stepInnerDotActive} />
) : null}
</View>
<Text
style={[
localStyles.stepLabel,
(isCompleted || isActive) && localStyles.stepLabelActive,
]}
>
{step.label}
</Text>
</View>
);
})}
</View>
</View>
{/* Delivery Partner Details (if driver assigned) */}
{showDriver && (
<View style={localStyles.driverSection}>
<View style={localStyles.driverInfoRow}>
<View style={localStyles.driverAvatarContainer}>
<Text style={localStyles.driverAvatarText}>RS</Text>
</View>
<View style={localStyles.driverMeta}>
<Text style={localStyles.driverRoleText}>Your Delivery Hero</Text>
<Text style={localStyles.driverNameText}>Rahul Sharma</Text>
<Text style={localStyles.driverVehicleText}>
KA-01-AB-1234 Honda Activa
</Text>
</View>
<View style={localStyles.ratingBadge}>
<Text style={localStyles.ratingText}> 4.8</Text>
</View>
</View>
<View style={localStyles.driverActionsRow}>
<TouchableOpacity
style={[localStyles.actionButton, localStyles.callButton]}
onPress={handleCall}
activeOpacity={0.8}
>
<Text style={localStyles.actionIcon}>📞</Text>
<Text style={localStyles.callButtonText}>Call Partner</Text>
</TouchableOpacity>
<TouchableOpacity
style={[localStyles.actionButton, localStyles.messageButton]}
onPress={handleMessage}
activeOpacity={0.8}
>
<Text style={localStyles.actionIcon}>💬</Text>
<Text style={localStyles.messageButtonText}>Chat</Text>
</TouchableOpacity>
</View>
</View>
)}
{/* View Detailed History Toggle */}
<View style={localStyles.toggleSection}>
<TouchableOpacity
onPress={toggleExpand}
style={localStyles.toggleButton}
activeOpacity={0.7}
>
<Text style={localStyles.toggleButtonText}>
{expanded ? 'Hide Details' : 'View Detailed History'}
</Text>
<Text style={localStyles.toggleChevron}>{expanded ? '▲' : '▼'}</Text>
</TouchableOpacity>
</View>
{/* Detailed Vertical Logs */}
{expanded && (
<View style={localStyles.detailedTimelineWrapper}>
{STATUS_FLOW.map((status, index) => {
const isCompleted = index <= currentIndex;
const isLast = index === STATUS_FLOW.length - 1;
const time = trackingMap[status];
return (
<View key={status} style={styles.timelineRow}>
<View style={styles.timelineDotColumn}>
<View
style={[
styles.timelineDot,
isCompleted && styles.timelineDotCompleted,
]}
>
{isCompleted && <Text style={styles.timelineCheck}></Text>}
</View>
{!isLast && (
<View
style={[
styles.timelineLine,
index < currentIndex && styles.timelineLineCompleted,
]}
/>
)}
</View>
<View style={styles.timelineContent}>
<Text
style={
isCompleted
? styles.timelineLabelCompleted
: styles.timelineLabelPending
}
>
{STATUS_LABEL[status]}
</Text>
{time && (
<Text style={styles.timelineTime}>
{formatDate(time)} at {formatTime(time)}
</Text>
)}
</View>
</View>
);
})}
</View>
)}
</View>
);
};

View File

@ -1,2 +0,0 @@
export * from './OrderStatusTimeline';
export type * from './OrderStatusTimeline.props';

View File

@ -4,7 +4,6 @@ export * from './socialButton';
export * from './header'; export * from './header';
export * from './searchBar'; export * from './searchBar';
export * from './providerCard'; export * from './providerCard';
export * from './productCard';
export * from './catalogItemRow'; export * from './catalogItemRow';
export * from './stepProgress'; export * from './stepProgress';
export * from './badge'; export * from './badge';
@ -13,4 +12,3 @@ export * from './categoryChip';
export * from './ratingStars'; export * from './ratingStars';
export * from './orderHistoryCard'; export * from './orderHistoryCard';
export * from './paymentOption'; export * from './paymentOption';
export * from './OrderStatusTimeline';

View File

@ -1,5 +1,3 @@
import { OrderItem } from '@interfaces';
export interface OrderHistoryCardProps { export interface OrderHistoryCardProps {
providerName: string; providerName: string;
providerImage: string; providerImage: string;
@ -8,5 +6,4 @@ export interface OrderHistoryCardProps {
total: number; total: number;
onReorder?: () => void; onReorder?: () => void;
onDetails?: () => void; onDetails?: () => void;
items?: OrderItem[];
} }

View File

@ -53,42 +53,12 @@ export const getStyles = (colors: any) => StyleSheet.create({
fontWeight: typography.fontWeight.medium, fontWeight: typography.fontWeight.medium,
color: colors.primary, color: colors.primary,
}, },
itemsContainer: {
paddingVertical: 12,
borderTopWidth: 1,
borderTopColor: colors.border,
},
itemRow: {
flexDirection: 'row',
alignItems: 'flex-start',
marginBottom: 8,
},
itemQuantityBadge: {
backgroundColor: colors.surface,
borderWidth: 1,
borderColor: colors.border,
borderRadius: 4,
paddingHorizontal: 6,
paddingVertical: 2,
marginRight: 8,
marginTop: 2,
},
itemQuantityText: {
fontSize: typography.fontSize.xs,
color: colors.text,
fontWeight: typography.fontWeight.medium,
},
itemName: {
flex: 1,
fontSize: typography.fontSize.sm,
color: colors.text,
lineHeight: 20,
},
footer: { footer: {
flexDirection: 'row', flexDirection: 'row',
justifyContent: 'space-between', justifyContent: 'space-between',
alignItems: 'center', alignItems: 'center',
paddingTop: 12, marginTop: 10,
paddingTop: 10,
borderTopWidth: 1, borderTopWidth: 1,
borderTopColor: colors.border, borderTopColor: colors.border,
}, },

View File

@ -12,7 +12,6 @@ export const OrderHistoryCard: React.FC<OrderHistoryCardProps> = ({
total, total,
onReorder, onReorder,
onDetails, onDetails,
items,
}) => { }) => {
const { colors } = useAppTheme(); const { colors } = useAppTheme();
const styles = getStyles(colors); const styles = getStyles(colors);
@ -31,20 +30,6 @@ export const OrderHistoryCard: React.FC<OrderHistoryCardProps> = ({
<Text style={styles.statusText}>{status}</Text> <Text style={styles.statusText}>{status}</Text>
</View> </View>
</View> </View>
{items && items.length > 0 && (
<View style={styles.itemsContainer}>
{items.map((item, index) => (
<View key={item.id || index} style={styles.itemRow}>
<View style={styles.itemQuantityBadge}>
<Text style={styles.itemQuantityText}>{item.quantity}x</Text>
</View>
<Text style={styles.itemName} numberOfLines={1}>
{item.name}
</Text>
</View>
))}
</View>
)}
<View style={styles.footer}> <View style={styles.footer}>
<Text style={styles.total}>{total}</Text> <Text style={styles.total}>{total}</Text>
<View style={styles.actions}> <View style={styles.actions}>

View File

@ -1,6 +1,6 @@
export interface PaymentOptionProps { export interface PaymentOptionProps {
type: string;
label: string; label: string;
isSelected: boolean; isSelected: boolean;
icon?: string;
onSelect: () => void; onSelect: () => void;
} }

View File

@ -1,8 +1,7 @@
import { StyleSheet } from 'react-native'; import { StyleSheet } from 'react-native';
import { typography } from '@theme'; import { typography } from '@theme';
export const getStyles = (colors: any) => export const getStyles = (colors: any) => StyleSheet.create({
StyleSheet.create({
container: { container: {
flexDirection: 'row', flexDirection: 'row',
alignItems: 'center', alignItems: 'center',
@ -19,8 +18,7 @@ export const getStyles = (colors: any) =>
backgroundColor: '#E8F5E9', backgroundColor: '#E8F5E9',
}, },
icon: { icon: {
width: 24, fontSize: 24,
height: 24,
marginRight: 12, marginRight: 12,
}, },
label: { label: {

View File

@ -1,18 +1,17 @@
import React from 'react'; import React from 'react';
import { View, Text, TouchableOpacity, Image } from 'react-native'; import { View, Text, TouchableOpacity } from 'react-native';
import { PaymentOptionProps } from './paymentOption.props'; import { PaymentOptionProps } from './paymentOption.props';
import { getStyles } from './paymentOption.styles'; import { getStyles } from './paymentOption.styles';
import { useAppTheme } from '@theme'; import { useAppTheme } from '@theme';
export const PaymentOption: React.FC<PaymentOptionProps> = ({ export const PaymentOption: React.FC<PaymentOptionProps> = ({
type,
label, label,
isSelected, isSelected,
icon,
onSelect, onSelect,
}) => { }) => {
const { colors } = useAppTheme(); const { colors } = useAppTheme();
const styles = getStyles(colors); const styles = getStyles(colors);
// console.log(icon);
return ( return (
<TouchableOpacity <TouchableOpacity
@ -20,7 +19,7 @@ export const PaymentOption: React.FC<PaymentOptionProps> = ({
onPress={onSelect} onPress={onSelect}
activeOpacity={0.7} activeOpacity={0.7}
> >
<Image source={{ uri: icon }} style={styles.icon} /> <Text style={styles.icon}>{type === 'UPI' ? '📱' : type === 'Card' ? '💳' : type === 'Wallet' ? '👛' : '💵'}</Text>
<Text style={styles.label}>{label}</Text> <Text style={styles.label}>{label}</Text>
<View style={[styles.radio, isSelected && styles.radioSelected]}> <View style={[styles.radio, isSelected && styles.radioSelected]}>
{isSelected && <View style={styles.radioInner} />} {isSelected && <View style={styles.radioInner} />}

View File

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

View File

@ -1,109 +0,0 @@
import { StyleSheet, Dimensions, Platform } from 'react-native';
import { typography } from '@theme';
const { width } = Dimensions.get('window');
// Screen padding = 16 * 2 = 32
// Gap between cards = 16
// Total cards width = width - 32 - 16 = width - 48
const CARD_WIDTH = (width - 48) / 2;
export const getStyles = (colors: any) =>
StyleSheet.create({
cardContainer: {
width: CARD_WIDTH,
backgroundColor: colors.surface ?? '#FFFFFF',
borderRadius: 12,
marginBottom: 16,
overflow: 'hidden',
...Platform.select({
ios: {
shadowColor: '#000',
shadowOffset: { width: 0, height: 2 },
shadowOpacity: 0.05,
shadowRadius: 8,
},
android: { elevation: 2 },
}),
},
imageContainer: {
width: '100%',
height: CARD_WIDTH, // Square image
backgroundColor: colors.background ?? '#F8F9FA',
position: 'relative',
},
image: {
width: '100%',
height: '100%',
resizeMode: 'cover',
},
discountBadge: {
position: 'absolute',
top: 8,
left: 8,
backgroundColor: colors.error ?? '#E53935',
paddingHorizontal: 6,
paddingVertical: 2,
borderRadius: 4,
},
discountText: {
color: '#FFFFFF',
fontSize: 10,
fontWeight: typography.fontWeight.bold,
},
favoriteButton: {
position: 'absolute',
top: 8,
right: 8,
backgroundColor: 'rgba(255,255,255,0.9)',
width: 28,
height: 28,
borderRadius: 14,
alignItems: 'center',
justifyContent: 'center',
},
infoContainer: {
padding: 10,
},
brandText: {
fontSize: 10,
color: colors.textSecondary,
textTransform: 'uppercase',
marginBottom: 2,
fontWeight: typography.fontWeight.semibold,
},
titleText: {
fontSize: 13,
color: colors.text,
fontWeight: typography.fontWeight.medium,
marginBottom: 4,
},
priceRow: {
flexDirection: 'row',
alignItems: 'center',
marginBottom: 8,
},
priceText: {
fontSize: 14,
color: colors.text,
fontWeight: typography.fontWeight.bold,
marginRight: 6,
},
comparePriceText: {
fontSize: 11,
color: colors.textSecondary,
textDecorationLine: 'line-through',
},
addButton: {
flexDirection: 'row',
alignItems: 'center',
justifyContent: 'center',
backgroundColor: colors.primaryMuted ?? '#E9F7EF',
paddingVertical: 6,
borderRadius: 6,
},
addButtonText: {
color: colors.primary ?? '#05824C',
fontSize: 12,
fontWeight: typography.fontWeight.bold,
},
});

View File

@ -1,111 +0,0 @@
import React from 'react';
import {
View,
Text,
Image,
TouchableOpacity,
StyleProp,
ViewStyle,
} from 'react-native';
import { getStyles } from './productCard.styles';
import { useAppTheme } from '@theme';
import { getFullUrl } from '@utils';
export interface ProductCardProps {
id: string;
name: string;
imageUrl: string;
price: string;
compareAtPrice?: string;
brand?: string | null;
currency?: string;
onPress?: () => void;
onAddPress?: () => void;
style?: StyleProp<ViewStyle>;
}
export const getDiscountPercentage = (price: string, comparePrice?: string) => {
if (!comparePrice || !price) return null;
const p = parseFloat(price);
const c = parseFloat(comparePrice);
if (c <= p || c <= 0) return null;
const discount = Math.round(((c - p) / c) * 100);
return discount > 0 ? `${discount}% OFF` : null;
};
export const formatPrice = (price: string, currency: string = '₹') => {
const num = parseFloat(price);
return isNaN(num) ? price : `${currency}${num.toFixed(0)}`;
};
export const ProductCard: React.FC<ProductCardProps> = ({
id,
name,
imageUrl,
price,
compareAtPrice,
brand,
currency = '₹',
onPress,
onAddPress,
style,
}) => {
const { colors } = useAppTheme();
const styles = getStyles(colors);
const discountText = getDiscountPercentage(price, compareAtPrice);
return (
<TouchableOpacity
style={[styles.cardContainer, style]}
activeOpacity={0.7}
onPress={onPress}
>
<View style={styles.imageContainer}>
<Image
source={{
uri: getFullUrl(imageUrl),
}}
style={styles.image}
// fallback source can be handled here if needed
/>
{discountText && (
<View style={styles.discountBadge}>
<Text style={styles.discountText}>{discountText}</Text>
</View>
)}
<TouchableOpacity style={styles.favoriteButton}>
<Text style={{ fontSize: 14 }}>🤍</Text>
</TouchableOpacity>
</View>
<View style={styles.infoContainer}>
{brand && (
<Text style={styles.brandText} numberOfLines={1}>
{brand}
</Text>
)}
<Text style={styles.titleText} numberOfLines={2}>
{name}
</Text>
<View style={styles.priceRow}>
<Text style={styles.priceText}>{formatPrice(price, currency)}</Text>
{compareAtPrice && parseFloat(compareAtPrice) > parseFloat(price) && (
<Text style={styles.comparePriceText}>
{formatPrice(compareAtPrice, currency)}
</Text>
)}
</View>
{/* <TouchableOpacity
style={styles.addButton}
onPress={onAddPress || onPress}
activeOpacity={0.7}
>
<Text style={styles.addButtonText}>+ ADD</Text>
</TouchableOpacity> */}
</View>
</TouchableOpacity>
);
};

View File

@ -9,8 +9,7 @@ import {
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 { useLoginScreen } from './hooks/useLoginScreen'; import { useLoginScreen } from '../../../hooks';
import { RootState, useAppSelector } from '@store';
export const LoginScreen: React.FC = () => { export const LoginScreen: React.FC = () => {
const { colors } = useAppTheme(); const { colors } = useAppTheme();
@ -19,8 +18,6 @@ export const LoginScreen: React.FC = () => {
const { mobileNumber, error, onChangeMobileNumber, handleLogin } = const { mobileNumber, error, onChangeMobileNumber, handleLogin } =
useLoginScreen(); useLoginScreen();
const { isLoading } = useAppSelector((state: RootState) => state.auth);
return ( return (
<KeyboardAvoidingView <KeyboardAvoidingView
style={styles.container} style={styles.container}
@ -72,10 +69,9 @@ export const LoginScreen: React.FC = () => {
)} )}
<PrimaryButton <PrimaryButton
title={isLoading ? 'Loading...' : 'Get OTP'} title="Get OTP"
onPress={handleLogin} onPress={handleLogin}
style={{ marginTop: 12 }} style={{ marginTop: 12 }}
disabled={isLoading}
/> />
</View> </View>

View File

@ -1 +1,2 @@
export * from './loginScreen'; export * from './loginScreen';
export * from './loginScreen.styles';

View File

@ -9,10 +9,10 @@ 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 '../../../store';
import { logout } from '../../../store/commonreducers/auth'; import { logout } from '../../../store/commonreducers/auth';
import { AppStackParamList } from '../../../navigation/appStack'; import { AppStackParamList } from '../../../navigation/appStack';
import { MainTabParamList } from '../../../navigation/mainTabNavigator'; import { MainTabParamList } from '../../../navigation/mainTabNavigator';
import { useAppDispatch, useAppSelector } from '@store';
type AccountNavProp = CompositeNavigationProp< type AccountNavProp = CompositeNavigationProp<
BottomTabNavigationProp<MainTabParamList, 'AccountScreen'>, BottomTabNavigationProp<MainTabParamList, 'AccountScreen'>,
@ -121,7 +121,7 @@ export const AccountScreen: React.FC = () => {
<View style={styles.profilePhoneRow}> <View style={styles.profilePhoneRow}>
<Text style={styles.profilePhoneIcon}>📞</Text> <Text style={styles.profilePhoneIcon}>📞</Text>
<Text style={styles.profilePhone}> <Text style={styles.profilePhone}>
{user?.phone || '+91 98765 43210'} {user?.mobileNumber || '+91 98765 43210'}
</Text> </Text>
</View> </View>

View File

@ -1,19 +1,24 @@
import React, { useEffect } from 'react'; import React, { useState } from 'react';
import { View, Text, ScrollView, TouchableOpacity, Image } from 'react-native'; import {
View,
Text,
ScrollView,
TextInput,
TouchableOpacity,
} from 'react-native';
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 './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 '../../../store';
import { import {
removeItem,
applyCoupon,
removeCoupon,
selectCartTotal, selectCartTotal,
getCartThunk,
updateCartItemThunk,
} from '../../../store/commonreducers/cart'; } from '../../../store/commonreducers/cart';
import { AppStackParamList } from '../../../navigation/appStack'; import { AppStackParamList } from '../../../navigation/appStack';
import { useAppDispatch, useAppSelector } from '@store';
import { getFullUrl } from '@utils';
// import { getFullUrl } from '@utils/helperr';
type CartScreenNavProp = StackNavigationProp<AppStackParamList, 'CartScreen'>; type CartScreenNavProp = StackNavigationProp<AppStackParamList, 'CartScreen'>;
@ -22,14 +27,20 @@ export const CartScreen: React.FC = () => {
const styles = getStyles(colors); const styles = getStyles(colors);
const dispatch = useAppDispatch(); const dispatch = useAppDispatch();
const navigation = useNavigation<CartScreenNavProp>(); const navigation = useNavigation<CartScreenNavProp>();
const { items, isLoading } = useAppSelector(state => state.cart); const { items, couponCode } = useAppSelector(state => state.cart);
const totals = useAppSelector(selectCartTotal); const totals = useAppSelector(selectCartTotal);
const [couponInput, setCouponInput] = useState('');
useEffect(() => { const handleCouponPress = () => {
dispatch(getCartThunk()); if (couponCode) {
}, [dispatch]); dispatch(removeCoupon());
setCouponInput('');
} else if (couponInput) {
dispatch(applyCoupon(couponInput));
}
};
if (!isLoading && items.length === 0) { if (items.length === 0) {
return ( return (
<View style={styles.container}> <View style={styles.container}>
<Header title="Cart" onBack={() => navigation.goBack()} /> <Header title="Cart" onBack={() => navigation.goBack()} />
@ -74,36 +85,19 @@ export const CartScreen: React.FC = () => {
index < items.length - 1 && styles.cartItemDivider, index < items.length - 1 && styles.cartItemDivider,
]} ]}
> >
<Image <View style={styles.itemThumb}>
style={styles.itemThumb} <Text style={styles.itemThumbEmoji}>🍕</Text>
source={{ uri: getFullUrl(item.product.imageUrl) }} </View>
/>
<View style={styles.itemInfo}> <View style={styles.itemInfo}>
<Text style={styles.itemTitle} numberOfLines={2}> <Text style={styles.itemTitle} numberOfLines={2}>
{item.product.name} {item.item.title}
</Text> </Text>
<Text style={styles.itemPrice}>{item.product.price}</Text> <Text style={styles.itemPrice}>{item.item.price}</Text>
</View> </View>
<QuantitySelector <QuantitySelector
value={item.quantity} value={item.quantity}
onIncrement={() => onIncrement={() => {}}
dispatch( onDecrement={() => dispatch(removeItem(item.item.id))}
updateCartItemThunk({
productId: item.productId,
quantity: item.quantity + 1,
}),
)
}
onDecrement={() => {
if (item.quantity >= 1) {
dispatch(
updateCartItemThunk({
productId: item.productId,
quantity: item.quantity - 1,
}),
);
}
}}
/> />
</View> </View>
))} ))}
@ -119,12 +113,48 @@ export const CartScreen: React.FC = () => {
</TouchableOpacity> </TouchableOpacity>
<View style={styles.footer}> <View style={styles.footer}>
{/* Coupon logic removed/commented out for now as requested
<Text style={styles.sectionTitle}>Apply Coupon</Text> <Text style={styles.sectionTitle}>Apply Coupon</Text>
<View style={styles.couponCard}> <View style={styles.couponCard}>
... {couponCode ? (
<View style={styles.couponAppliedRow}>
<View style={styles.couponAppliedLeft}>
<View style={styles.couponCheckBadge}>
<Text style={styles.couponCheckIcon}></Text>
</View>
<View>
<Text style={styles.couponAppliedCode}>{couponCode}</Text>
<Text style={styles.couponAppliedSub}>Coupon applied</Text>
</View>
</View>
<TouchableOpacity
onPress={handleCouponPress}
activeOpacity={0.7}
>
<Text style={styles.couponRemoveText}>Remove</Text>
</TouchableOpacity>
</View>
) : (
<View style={styles.couponRow}>
<Text style={styles.couponIcon}>🏷</Text>
<TextInput
style={styles.couponInput}
placeholder="Enter coupon code"
placeholderTextColor={colors.placeholder}
value={couponInput}
onChangeText={setCouponInput}
autoCapitalize="characters"
/>
<TouchableOpacity
style={styles.applyButton}
onPress={handleCouponPress}
activeOpacity={0.7}
disabled={!couponInput}
>
<Text style={styles.applyButtonText}>Apply</Text>
</TouchableOpacity>
</View>
)}
</View> </View>
*/}
<Text style={styles.sectionTitle}>Bill Details</Text> <Text style={styles.sectionTitle}>Bill Details</Text>
<View style={styles.billCard}> <View style={styles.billCard}>

View File

@ -1,15 +1,37 @@
import { StyleSheet } from 'react-native'; import { StyleSheet } from 'react-native';
import { typography } from '@theme'; import { typography } from '@theme';
export const getStyles = (colors: any) => export const getStyles = (colors: any) => StyleSheet.create({
StyleSheet.create({
container: { container: {
flex: 1, flex: 1,
backgroundColor: colors.background, backgroundColor: colors.background,
}, },
content: { content: {
padding: 16, padding: 16,
paddingBottom: 24, },
addressCard: {
backgroundColor: colors.cardBg,
borderRadius: 12,
padding: 16,
borderWidth: 1,
borderColor: colors.border,
marginBottom: 24,
},
addressLabel: {
fontSize: typography.fontSize.xs,
color: colors.textSecondary,
marginBottom: 4,
},
addressText: {
fontSize: typography.fontSize.md,
fontWeight: typography.fontWeight.semibold,
color: colors.text,
marginBottom: 4,
},
addressSubtext: {
fontSize: typography.fontSize.sm,
color: colors.primary,
fontWeight: typography.fontWeight.medium,
}, },
sectionTitle: { sectionTitle: {
fontSize: typography.fontSize.md, fontSize: typography.fontSize.md,
@ -17,61 +39,32 @@ export const getStyles = (colors: any) =>
color: colors.text, color: colors.text,
marginBottom: 12, marginBottom: 12,
}, },
emptyState: { optionCard: {
padding: 24, flexDirection: 'row',
alignItems: 'center', alignItems: 'center',
justifyContent: 'center',
},
emptyStateText: {
fontSize: typography.fontSize.sm,
color: colors.textSecondary,
},
addressCard: {
backgroundColor: colors.cardBg,
borderRadius: 12,
padding: 16, padding: 16,
borderRadius: 12,
borderWidth: 1.5, borderWidth: 1.5,
borderColor: colors.border, borderColor: colors.border,
marginBottom: 12, marginBottom: 10,
backgroundColor: colors.background,
}, },
addressCardSelected: { optionCardSelected: {
borderColor: colors.primary, borderColor: colors.primary,
backgroundColor: '#E8F5E9', backgroundColor: '#E8F5E9',
}, },
addressCardHeader: { optionInfo: {
flexDirection: 'row', flex: 1,
alignItems: 'center',
marginBottom: 8,
}, },
labelBadge: { optionTitle: {
flexDirection: 'row', fontSize: typography.fontSize.md,
alignItems: 'center', fontWeight: typography.fontWeight.medium,
backgroundColor: colors.background,
borderRadius: 20,
paddingVertical: 4,
paddingHorizontal: 10,
marginRight: 8,
},
labelBadgeIcon: {
fontSize: 12,
marginRight: 4,
},
labelBadgeText: {
fontSize: typography.fontSize.xs,
fontWeight: typography.fontWeight.semibold,
color: colors.text, color: colors.text,
marginBottom: 2,
}, },
defaultBadge: { optionSubtext: {
backgroundColor: colors.primary, fontSize: typography.fontSize.sm,
borderRadius: 20, color: colors.textSecondary,
paddingVertical: 3,
paddingHorizontal: 8,
},
defaultBadgeText: {
fontSize: 10,
fontWeight: typography.fontWeight.bold,
color: '#fff',
letterSpacing: 0.5,
}, },
radio: { radio: {
width: 22, width: 22,
@ -81,7 +74,6 @@ export const getStyles = (colors: any) =>
borderColor: colors.border, borderColor: colors.border,
justifyContent: 'center', justifyContent: 'center',
alignItems: 'center', alignItems: 'center',
marginLeft: 'auto',
}, },
radioSelected: { radioSelected: {
borderColor: colors.primary, borderColor: colors.primary,
@ -92,36 +84,6 @@ export const getStyles = (colors: any) =>
borderRadius: 6, borderRadius: 6,
backgroundColor: colors.primary, backgroundColor: colors.primary,
}, },
addressText: {
fontSize: typography.fontSize.md,
fontWeight: typography.fontWeight.medium,
color: colors.text,
marginBottom: 4,
},
addressSubtext: {
fontSize: typography.fontSize.sm,
color: colors.textSecondary,
marginBottom: 2,
},
addressPhone: {
fontSize: typography.fontSize.sm,
color: colors.textSecondary,
marginTop: 4,
},
addAddressButton: {
borderWidth: 1.5,
borderColor: colors.primary,
borderStyle: 'dashed',
borderRadius: 12,
paddingVertical: 14,
alignItems: 'center',
marginTop: 4,
},
addAddressButtonText: {
fontSize: typography.fontSize.md,
fontWeight: typography.fontWeight.semibold,
color: colors.primary,
},
footer: { footer: {
padding: 16, padding: 16,
borderTopWidth: 1, borderTopWidth: 1,

View File

@ -1,18 +1,18 @@
import React, { useState, useMemo } from 'react'; import React, { useState } from 'react';
import { View, Text, TouchableOpacity, ScrollView } from 'react-native'; import {
View,
Text,
TouchableOpacity,
ScrollView,
} from 'react-native';
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 './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 { useAppSelector } from '@store'; import { AppStackParamList } from '../../../navigation/appStack';
import { AddressLabel, CustomerAddress } from '@interfaces';
import { AppStackParamList } from '@navigation/appStack';
type CheckoutAddressNavProp = StackNavigationProp< type CheckoutAddressNavProp = StackNavigationProp<AppStackParamList, 'CheckoutAddressScreen'>;
AppStackParamList,
'CheckoutAddressScreen'
>;
const CHECKOUT_STEPS = [ const CHECKOUT_STEPS = [
{ key: 'address', label: 'Address' }, { key: 'address', label: 'Address' },
@ -20,118 +20,62 @@ const CHECKOUT_STEPS = [
{ key: 'confirm', label: 'Confirm' }, { key: 'confirm', label: 'Confirm' },
]; ];
const LABEL_ICON: Record<AddressLabel, string> = {
Home: '🏠',
Work: '💼',
Other: '📍',
};
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 navigation = useNavigation<CheckoutAddressNavProp>();
const { customerDetails } = useAppSelector(state => state.customerProfile); const [deliveryOption, setDeliveryOption] = useState<'standard' | 'express'>('standard');
const addresses: CustomerAddress[] = customerDetails?.addresses ?? [];
const defaultAddressId = useMemo(() => {
const def = addresses.find(a => a.isDefault);
return def?.id ?? addresses[0]?.id ?? null;
}, [addresses]);
const [selectedAddressId, setSelectedAddressId] = useState<string | null>(
defaultAddressId,
);
const handleContinue = () => {
if (!selectedAddressId) return;
navigation.navigate('CheckoutPaymentScreen', {
selectedAddressId,
});
};
return ( return (
<View style={styles.container}> <View style={styles.container}>
<Header title="Checkout" onBack={() => navigation.goBack()} /> <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}>
<Text style={styles.sectionTitle}>Deliver to</Text> <View style={styles.addressCard}>
<Text style={styles.addressLabel}>Deliver to</Text>
{addresses.length === 0 && ( <Text style={styles.addressText}>
<View style={styles.emptyState}> Koramangala 4th Block, Bengaluru, 560034
<Text style={styles.emptyStateText}>No saved addresses yet.</Text> </Text>
<Text style={styles.addressSubtext}>Home</Text>
</View> </View>
)}
{addresses.map(address => { <Text style={styles.sectionTitle}>Delivery Option</Text>
const isSelected = selectedAddressId === address.id;
return (
<TouchableOpacity <TouchableOpacity
key={address.id}
style={[ style={[
styles.addressCard, styles.optionCard,
isSelected && styles.addressCardSelected, deliveryOption === 'standard' && styles.optionCardSelected,
]} ]}
onPress={() => setDeliveryOption('standard')}
activeOpacity={0.7} activeOpacity={0.7}
onPress={() => setSelectedAddressId(address.id)}
> >
<View style={styles.addressCardHeader}> <View style={styles.optionInfo}>
<View style={styles.labelBadge}> <Text style={styles.optionTitle}>Standard Delivery</Text>
<Text style={styles.labelBadgeIcon}> <Text style={styles.optionSubtext}>Free 25-30 min</Text>
{LABEL_ICON[address.label] ?? '📍'}
</Text>
<Text style={styles.labelBadgeText}>{address.label}</Text>
</View> </View>
<View style={[styles.radio, deliveryOption === 'standard' && styles.radioSelected]}>
{address.isDefault && ( {deliveryOption === 'standard' && <View style={styles.radioInner} />}
<View style={styles.defaultBadge}>
<Text style={styles.defaultBadgeText}>DEFAULT</Text>
</View> </View>
)}
<View
style={[styles.radio, isSelected && styles.radioSelected]}
>
{isSelected && <View style={styles.radioInner} />}
</View>
</View>
<Text style={styles.addressText} numberOfLines={2}>
{address.houseNumber ? `${address.houseNumber}, ` : ''}
{address.addressLine1}
</Text>
{!!address.landmark && (
<Text style={styles.addressSubtext} numberOfLines={1}>
Landmark: {address.landmark}
</Text>
)}
<Text style={styles.addressSubtext} numberOfLines={1}>
{address.city}, {address.state} - {address.postalCode}
</Text>
<Text style={styles.addressPhone}>📞 {address.phone}</Text>
</TouchableOpacity> </TouchableOpacity>
);
})}
<TouchableOpacity <TouchableOpacity
style={styles.addAddressButton} style={[
styles.optionCard,
deliveryOption === 'express' && styles.optionCardSelected,
]}
onPress={() => setDeliveryOption('express')}
activeOpacity={0.7} activeOpacity={0.7}
onPress={() => {
// navigation.navigate('AddAddressScreen');
}}
> >
<Text style={styles.addAddressButtonText}>+ Add New Address</Text> <View style={styles.optionInfo}>
<Text style={styles.optionTitle}>Express Delivery</Text>
<Text style={styles.optionSubtext}>40 10-15 min</Text>
</View>
<View style={[styles.radio, deliveryOption === 'express' && styles.radioSelected]}>
{deliveryOption === 'express' && <View style={styles.radioInner} />}
</View>
</TouchableOpacity> </TouchableOpacity>
</ScrollView> </ScrollView>
<View style={styles.footer}> <View style={styles.footer}>
<PrimaryButton <PrimaryButton title="Continue to Payment" onPress={() => navigation.navigate('CheckoutPaymentScreen')} />
title="Continue to Payment"
onPress={handleContinue}
disabled={!selectedAddressId}
/>
</View> </View>
</View> </View>
); );

View File

@ -1,30 +1,13 @@
import React, { useEffect, useState } from 'react'; import React, { useState } from 'react';
import { View, Text, ScrollView, Alert } from 'react-native'; import { View, Text, ScrollView } from 'react-native';
import { RouteProp, useNavigation, useRoute } 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 './checkoutPaymentScreen.styles'; import { getStyles } from './checkoutPaymentScreen.styles';
import { import { Header, StepProgress, PaymentOption, PrimaryButton } from '@components';
Header,
StepProgress,
PaymentOption,
PrimaryButton,
} from '@components';
import { useAppTheme } from '@theme'; import { useAppTheme } from '@theme';
import { AppStackParamList } from '../../../navigation/appStack'; import { AppStackParamList } from '../../../navigation/appStack';
import { RootState, useAppDispatch, useAppSelector } from '@store';
import { getAllPaymentMethodsThunk, placeOrderThunk } from './thunk';
import { useSelector } from 'react-redux';
import uuid from 'react-native-uuid';
import { v4 } from 'react-native-uuid/dist/v4';
type CheckoutPaymentNavProp = StackNavigationProp< type CheckoutPaymentNavProp = StackNavigationProp<AppStackParamList, 'CheckoutPaymentScreen'>;
AppStackParamList,
'CheckoutPaymentScreen'
>;
type CheckoutPaymentRouteProp = RouteProp<
AppStackParamList,
'CheckoutPaymentScreen'
>;
const CHECKOUT_STEPS = [ const CHECKOUT_STEPS = [
{ key: 'address', label: 'Address' }, { key: 'address', label: 'Address' },
@ -42,59 +25,8 @@ 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 route = useRoute<CheckoutPaymentRouteProp>();
const navigation = useNavigation<CheckoutPaymentNavProp>(); const navigation = useNavigation<CheckoutPaymentNavProp>();
const dispatch = useAppDispatch();
const [selectedMethod, setSelectedMethod] = useState('upi'); const [selectedMethod, setSelectedMethod] = useState('upi');
const selectedAddressId = route.params?.selectedAddressId;
// console.log(selectedAddressId);
const uuid = v4();
// console.log(uuid);
const {
paymentMethods,
placeOrderSuccess,
placeOrderLoading,
placeOrderError,
} = useAppSelector((state: RootState) => state.paymentMethods);
useEffect(() => {
dispatch(getAllPaymentMethodsThunk());
}, [dispatch]);
useEffect(() => {
if (placeOrderSuccess) {
navigation.navigate('OrderConfirmedScreen', {
orderId: 'ORD-' + Math.floor(Math.random() * 900000 + 100000),
});
} else if (placeOrderError) {
Alert.alert('Error', placeOrderError);
}
}, [placeOrderSuccess, placeOrderError]);
// console.log('paymentMethods', paymentMethods);
const selectedPaymentMethod = paymentMethods.find(
item => item.id === selectedMethod,
);
const methodName = selectedPaymentMethod?.code;
// console.log('methodName', methodName);
// useEffect(() => {
// },);
const handlePlaceOrder = () => {
dispatch(
placeOrderThunk({
addressId: selectedAddressId || '',
paymentMethodId: selectedMethod,
paymentMethod: methodName || '',
orderType: 'DELIVERY',
idempotencyKey: `idemp-key-${uuid}`,
}),
);
};
return ( return (
<View style={styles.container}> <View style={styles.container}>
@ -102,22 +34,18 @@ export const CheckoutPaymentScreen: React.FC = () => {
<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>
{paymentMethods?.map(method => ( {PAYMENT_METHODS.map((method) => (
<PaymentOption <PaymentOption
key={method.id} key={method.id}
label={method.code} type={method.type}
icon={method.iconUrl} label={method.label}
isSelected={selectedMethod === method.id} isSelected={selectedMethod === method.id}
onSelect={() => setSelectedMethod(method.id)} onSelect={() => setSelectedMethod(method.id)}
/> />
))} ))}
</ScrollView> </ScrollView>
<View style={styles.footer}> <View style={styles.footer}>
<PrimaryButton <PrimaryButton title="Place Order" onPress={() => navigation.navigate('OrderConfirmedScreen', { orderId: 'ORD-' + Math.floor(Math.random() * 900000 + 100000) })} />
title={placeOrderLoading ? 'Placing...' : 'Place Order'}
onPress={handlePlaceOrder}
disabled={placeOrderLoading}
/>
</View> </View>
</View> </View>
); );

View File

@ -1,3 +1 @@
export * from './checkoutPaymentScreen'; export * from './checkoutPaymentScreen';
export * from './thunk';
export * from './reducer';

View File

@ -1,94 +0,0 @@
import { PaymentMethodsResponse, PlaceOrderResponse } from '@interfaces';
import { createReducer } from '@reduxjs/toolkit';
import {
getAllPaymentMethodsThunk,
getOrderByIdThunk,
getOrderHistoryThunk,
placeOrderThunk,
} from './thunk';
import { Order } from '@interfaces/order';
export interface PaymentMethodsState {
paymentMethods: PaymentMethodsResponse;
isLoading: boolean;
error: string | null;
placeOrderLoading: boolean;
placeOrderSuccess: boolean;
placeOrderError: string | null;
orderHistory: Order[] | null;
placeOrderResponse: PlaceOrderResponse | null;
orderDetails: Order | null;
orderDetailsLoading: boolean;
orderDetailsError: string | null;
}
const initialState: PaymentMethodsState = {
paymentMethods: [],
isLoading: false,
error: null,
placeOrderLoading: false,
placeOrderSuccess: false,
placeOrderError: null,
orderHistory: null,
placeOrderResponse: null,
orderDetails: null,
orderDetailsLoading: false,
orderDetailsError: null,
};
const paymentMethodsReducer = createReducer(initialState, builder => {
builder
.addCase(getAllPaymentMethodsThunk.pending, state => {
state.isLoading = true;
state.error = null;
})
.addCase(getAllPaymentMethodsThunk.fulfilled, (state, action) => {
state.isLoading = false;
state.paymentMethods = action.payload;
})
.addCase(getAllPaymentMethodsThunk.rejected, (state, action) => {
state.isLoading = false;
state.error = action.payload as string;
})
.addCase(placeOrderThunk.pending, state => {
state.placeOrderLoading = true;
state.placeOrderSuccess = false;
state.placeOrderError = null;
})
.addCase(placeOrderThunk.fulfilled, (state, action) => {
state.placeOrderLoading = false;
state.placeOrderSuccess = true;
state.placeOrderResponse = action.payload;
})
.addCase(placeOrderThunk.rejected, (state, action) => {
state.placeOrderLoading = false;
state.placeOrderSuccess = false;
state.placeOrderError = action.payload as string;
})
.addCase(getOrderHistoryThunk.pending, state => {
state.isLoading = true;
state.error = null;
})
.addCase(getOrderHistoryThunk.fulfilled, (state, action) => {
state.isLoading = false;
state.orderHistory = action.payload;
})
.addCase(getOrderHistoryThunk.rejected, (state, action) => {
state.isLoading = false;
state.error = action.payload as string;
})
.addCase(getOrderByIdThunk.pending, state => {
state.orderDetailsLoading = true;
state.orderDetailsError = null;
})
.addCase(getOrderByIdThunk.fulfilled, (state, action) => {
state.orderDetailsLoading = false;
state.orderDetails = action.payload;
})
.addCase(getOrderByIdThunk.rejected, (state, action) => {
state.orderDetailsLoading = false;
state.orderDetailsError = action.payload as string;
});
});
export default paymentMethodsReducer;

View File

@ -1,74 +0,0 @@
import {
OrderRequest,
PaymentMethodsResponse,
PlaceOrderResponse,
Order,
} from '@interfaces';
import { createAsyncThunk } from '@reduxjs/toolkit';
import {
getAllPaymentMethodsApi,
getOrderByIdApi,
getOrderHistoryApi,
placeOrderApi,
} from '@api';
export const getAllPaymentMethodsThunk = createAsyncThunk<
PaymentMethodsResponse,
void,
{ rejectValue: string }
>('paymentMethods/getAll', async (_, { rejectWithValue }) => {
try {
const response = await getAllPaymentMethodsApi();
// console.log(response.data);
return response;
} catch (error: any) {
return rejectWithValue(
error.response?.data?.error || 'Failed to get payment methods',
);
}
});
export const placeOrderThunk = createAsyncThunk<
PlaceOrderResponse,
OrderRequest,
{ rejectValue: string }
>('orders/placeOrder', async (payload: OrderRequest, { rejectWithValue }) => {
try {
const response = await placeOrderApi(payload);
return response;
} catch (error: any) {
return rejectWithValue(
error.response?.data?.error || 'Failed to place order',
);
}
});
export const getOrderHistoryThunk = createAsyncThunk<
Order[],
void,
{ rejectValue: string }
>('orders/getOrderHistory', async (_, { rejectWithValue }) => {
try {
const response = await getOrderHistoryApi();
return response;
} catch (error: any) {
return rejectWithValue(
error.response?.data?.error || 'Failed to get order history',
);
}
});
export const getOrderByIdThunk = createAsyncThunk<
Order,
string,
{ rejectValue: string }
>('orders/getOrderById', async (orderId: string, { rejectWithValue }) => {
try {
const response = await getOrderByIdApi(orderId);
return response;
} catch (error: any) {
return rejectWithValue(
error.response?.data?.error || 'Failed to get order',
);
}
});

View File

@ -12,15 +12,11 @@ import { StackNavigationProp } from '@react-navigation/stack';
import { getStyles } from './completeProfileScreen.styles'; 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 '../../../store';
import { completeProfile } from '../../../store/commonreducers/auth';
import { AuthStackParamList } from '../../../navigation/authStack'; import { AuthStackParamList } from '../../../navigation/authStack';
import { useAppDispatch } from '@store';
import { saveProfileData } from './reducer';
import { OnboardingStackParamList } from '@navigation/onboardingStack';
type NavProp = StackNavigationProp< type NavProp = StackNavigationProp<AuthStackParamList, 'CompleteProfileScreen'>;
OnboardingStackParamList,
'CompleteProfileScreen'
>;
const LOCATION_LABELS = ['Home', 'Work', 'Other']; const LOCATION_LABELS = ['Home', 'Work', 'Other'];
@ -32,34 +28,10 @@ export const CompleteProfileScreen: React.FC = () => {
const [name, setName] = useState(''); const [name, setName] = useState('');
const [email, setEmail] = useState(''); const [email, setEmail] = useState('');
const [gender, setGender] = useState<'MALE' | 'FEMALE' | 'OTHER'>('MALE');
const [addressLine1, setAddressLine1] = useState('');
const [houseNumber, setHouseNumber] = useState('');
const [landmark, setLandmark] = useState('');
const [city, setCity] = useState('');
const [state, setState] = useState('');
const [postalCode, setPostalCode] = useState('');
const [addressPhone, setAddressPhone] = useState('');
const [selectedLabel, setSelectedLabel] = useState('Home'); const [selectedLabel, setSelectedLabel] = useState('Home');
const handleSave = () => { const handleSave = () => {
dispatch( dispatch(completeProfile({ name, email, locationLabels: [selectedLabel] }));
saveProfileData({
name,
email,
gender,
addressLine1,
houseNumber,
landmark,
city,
state,
postalCode,
addressPhone,
addressLabel: selectedLabel,
}),
);
navigation.navigate('PreferencesScreen'); navigation.navigate('PreferencesScreen');
}; };
@ -68,117 +40,28 @@ export const CompleteProfileScreen: React.FC = () => {
style={styles.container} style={styles.container}
behavior={Platform.OS === 'ios' ? 'padding' : 'height'} behavior={Platform.OS === 'ios' ? 'padding' : 'height'}
> >
<ScrollView <ScrollView contentContainerStyle={styles.scrollContainer}>
contentContainerStyle={styles.scrollContainer}
showsVerticalScrollIndicator={false}
>
<Text style={styles.title}>Complete Profile</Text> <Text style={styles.title}>Complete Profile</Text>
<Text style={styles.subtitle}>Tell us about yourself</Text> <Text style={styles.subtitle}>Tell us about yourself</Text>
<View style={styles.form}> <View style={styles.form}>
{/* Personal Information */}
<CustomInput <CustomInput
label="Full Name" label="Name"
placeholder="John Doe" placeholder="John Doe"
value={name} value={name}
onChangeText={setName} onChangeText={setName}
/> />
<CustomInput <CustomInput
label="Email" label="Email"
placeholder="john@example.com" placeholder="john@example.com"
keyboardType="email-address" keyboardType="email-address"
autoCapitalize="none"
value={email} value={email}
onChangeText={setEmail} onChangeText={setEmail}
/> />
{/* Gender */}
<Text style={styles.labelText}>Gender</Text>
<View style={styles.labelRow}>
{['MALE', 'FEMALE', 'OTHER'].map(item => (
<TouchableOpacity
key={item}
style={[
styles.labelChip,
gender === item && styles.labelChipSelected,
]}
onPress={() => setGender(item as 'MALE' | 'FEMALE' | 'OTHER')}
>
<Text
style={[
styles.labelChipText,
gender === item && styles.labelChipTextSelected,
]}
>
{item.charAt(0) + item.slice(1).toLowerCase()}
</Text>
</TouchableOpacity>
))}
</View>
{/* Address */}
<CustomInput
label="Address Line 1"
placeholder="123 Main Street"
value={addressLine1}
onChangeText={setAddressLine1}
/>
<CustomInput
label="House / Flat Number"
placeholder="Apt 4B"
value={houseNumber}
onChangeText={setHouseNumber}
/>
<CustomInput
label="Landmark"
placeholder="Near Central Park"
value={landmark}
onChangeText={setLandmark}
/>
<CustomInput
label="City"
placeholder="New York"
value={city}
onChangeText={setCity}
/>
<CustomInput
label="State"
placeholder="NY"
value={state}
onChangeText={setState}
/>
<CustomInput
label="Postal Code"
placeholder="10001"
keyboardType="number-pad"
value={postalCode}
onChangeText={setPostalCode}
/>
<CustomInput
label="Phone Number"
placeholder="+1 9999999999"
keyboardType="phone-pad"
value={addressPhone}
onChangeText={setAddressPhone}
/>
{/* Location Label */}
<Text style={styles.labelText}>Location Label</Text> <Text style={styles.labelText}>Location Label</Text>
<View style={styles.labelRow}> <View style={styles.labelRow}>
{LOCATION_LABELS.map(label => ( {LOCATION_LABELS.map((label) => (
<TouchableOpacity <TouchableOpacity
key={label} key={label}
style={[ style={[
@ -186,6 +69,7 @@ export const CompleteProfileScreen: React.FC = () => {
selectedLabel === label && styles.labelChipSelected, selectedLabel === label && styles.labelChipSelected,
]} ]}
onPress={() => setSelectedLabel(label)} onPress={() => setSelectedLabel(label)}
activeOpacity={0.7}
> >
<Text <Text
style={[ style={[

View File

@ -1,2 +1 @@
export * from './completeProfileScreen'; export * from './completeProfileScreen';
export * from './reducer';

View File

@ -1,71 +0,0 @@
import { createReducer, createAction } from '@reduxjs/toolkit';
// ─── Actions ──────────────────────────────────────────────────────────────────
export const saveProfileData = createAction<{
name: string;
email: string;
gender: 'MALE' | 'FEMALE' | 'OTHER';
addressLine1: string;
houseNumber: string;
landmark: string;
city: string;
state: string;
postalCode: string;
addressPhone: string;
addressLabel: string;
}>('completeProfile/saveProfileData');
export const clearProfileData = createAction('completeProfile/clearProfileData');
// ─── State ────────────────────────────────────────────────────────────────────
export interface CompleteProfileState {
name: string;
email: string;
gender: 'MALE' | 'FEMALE' | 'OTHER';
addressLine1: string;
houseNumber: string;
landmark: string;
city: string;
state: string;
postalCode: string;
addressPhone: string;
addressLabel: string;
}
const initialState: CompleteProfileState = {
name: '',
email: '',
gender: 'MALE',
addressLine1: '',
houseNumber: '',
landmark: '',
city: '',
state: '',
postalCode: '',
addressPhone: '',
addressLabel: 'Home',
};
// ─── Reducer ──────────────────────────────────────────────────────────────────
const completeProfileReducer = createReducer(initialState, builder => {
builder
.addCase(saveProfileData, (state, action) => {
state.name = action.payload.name;
state.email = action.payload.email;
state.gender = action.payload.gender;
state.addressLine1 = action.payload.addressLine1;
state.houseNumber = action.payload.houseNumber;
state.landmark = action.payload.landmark;
state.city = action.payload.city;
state.state = action.payload.state;
state.postalCode = action.payload.postalCode;
state.addressPhone = action.payload.addressPhone;
state.addressLabel = action.payload.addressLabel;
})
.addCase(clearProfileData, () => initialState);
});
export default completeProfileReducer;

View File

@ -250,12 +250,9 @@ export const getStyles = (colors: any) =>
color: colors.primary ?? '#05824C', color: colors.primary ?? '#05824C',
}, },
// ---------- Product Grid ---------- // ---------- Provider cards ----------
providerList: { providerList: {
paddingHorizontal: 16, paddingHorizontal: 16,
flexDirection: 'row',
flexWrap: 'wrap',
justifyContent: 'space-between',
}, },
providerCardWrap: { providerCardWrap: {
backgroundColor: colors.surface ?? '#FFFFFF', backgroundColor: colors.surface ?? '#FFFFFF',

View File

@ -17,13 +17,12 @@ import {
import { BottomTabNavigationProp } from '@react-navigation/bottom-tabs'; import { BottomTabNavigationProp } from '@react-navigation/bottom-tabs';
import { StackNavigationProp } from '@react-navigation/stack'; import { StackNavigationProp } from '@react-navigation/stack';
import { getStyles } from './homeScreen.styles'; import { getStyles } from './homeScreen.styles';
import { ProductCard, SearchBar } from '@components'; import { ProviderCard, SearchBar } from '@components';
import { useAppTheme } from '@theme'; import { useAppTheme } from '@theme';
import { Product } from '../../../interfaces'; import { getProvidersApi } from '../../../api/deliveryApi';
import { Provider } from '../../../interfaces';
import { AppStackParamList } from '../../../navigation/appStack'; import { AppStackParamList } from '../../../navigation/appStack';
import { MainTabParamList } from '../../../navigation/mainTabNavigator'; import { MainTabParamList } from '../../../navigation/mainTabNavigator';
import { fetchCustomerDetails, useAppDispatch, useAppSelector } from '@store';
import { getAllProductsThunk } from './thunk';
type NavProp = CompositeNavigationProp< type NavProp = CompositeNavigationProp<
BottomTabNavigationProp<MainTabParamList, 'HomeScreen'>, BottomTabNavigationProp<MainTabParamList, 'HomeScreen'>,
@ -75,21 +74,18 @@ export const HomeScreen: React.FC = () => {
const styles = getStyles(colors); const styles = getStyles(colors);
const navigation = useNavigation<NavProp>(); const navigation = useNavigation<NavProp>();
const dispatch = useAppDispatch(); const [providers, setProviders] = useState<Provider[]>([]);
const { products, isLoading } = useAppSelector(state => state.home);
const [selectedCategory, setSelectedCategory] = useState('all'); const [selectedCategory, setSelectedCategory] = useState('all');
const [activeBanner, setActiveBanner] = useState(0); const [activeBanner, setActiveBanner] = useState(0);
useEffect(() => { useEffect(() => {
dispatch(getAllProductsThunk()); getProvidersApi().then(setProviders);
dispatch(fetchCustomerDetails()); }, []);
}, [dispatch]);
const filteredProducts = const filteredProviders =
selectedCategory === 'all' selectedCategory === 'all'
? products ? providers
: products.filter(p => p.category?.name === selectedCategory); : providers.filter(p => p.tag === selectedCategory);
const handleSearchFocus = useCallback(() => { const handleSearchFocus = useCallback(() => {
navigation.navigate('SearchScreen'); navigation.navigate('SearchScreen');
@ -122,12 +118,8 @@ export const HomeScreen: React.FC = () => {
</View> </View>
</TouchableOpacity> </TouchableOpacity>
<TouchableOpacity <TouchableOpacity style={styles.avatarButton} activeOpacity={0.7}>
style={styles.avatarButton} <Text style={styles.avatarEmoji}>🔔</Text>
activeOpacity={0.7}
onPress={() => navigation.navigate('CartScreen')}
>
<Text style={styles.avatarEmoji}>🧺</Text>
<View style={styles.notifDot} /> <View style={styles.notifDot} />
</TouchableOpacity> </TouchableOpacity>
</View> </View>
@ -232,8 +224,8 @@ export const HomeScreen: React.FC = () => {
{selectedCategory === 'all' ? 'Popular near you' : selectedCategory} {selectedCategory === 'all' ? 'Popular near you' : selectedCategory}
</Text> </Text>
<Text style={styles.sectionSubtitle}> <Text style={styles.sectionSubtitle}>
{filteredProducts.length} product {filteredProviders.length} place
{filteredProducts.length === 1 ? '' : 's'} available {filteredProviders.length === 1 ? '' : 's'} delivering to you
</Text> </Text>
</View> </View>
<TouchableOpacity activeOpacity={0.7}> <TouchableOpacity activeOpacity={0.7}>
@ -241,31 +233,31 @@ export const HomeScreen: React.FC = () => {
</TouchableOpacity> </TouchableOpacity>
</View> </View>
{/* Product list */} {/* Provider list */}
<View style={styles.providerList}> <View style={styles.providerList}>
{filteredProducts.length === 0 && ( {filteredProviders.length === 0 && (
<View style={[styles.emptyWrap, { width: '100%' }]}> <View style={styles.emptyWrap}>
<Text style={styles.emptyEmoji}>🛍</Text> <Text style={styles.emptyEmoji}>🍽</Text>
<Text style={styles.emptyText}>No products found</Text> <Text style={styles.emptyText}>
No providers in this category yet
</Text>
</View> </View>
)} )}
{filteredProducts.map(product => ( {filteredProviders.map(provider => (
<ProductCard <ProviderCard
key={product.id} key={provider.id}
id={product.id} imageUrl={provider.imageUrl}
imageUrl={product.imageUrl} name={provider.name}
name={product.name} rating={provider.rating}
price={product.price} deliveryTime={provider.deliveryTime}
compareAtPrice={product.compareAtPrice} tag={provider.tag}
brand={product.brand || product.merchant?.name} discountText={provider.discountText}
currency={product.currency === 'INR' ? '₹' : product.currency} onPress={() =>
onPress={() => {
navigation.navigate('ProviderDetailsScreen', { navigation.navigate('ProviderDetailsScreen', {
providerId: product.id, providerId: provider.id,
providerName: product.name, })
}); }
}}
/> />
))} ))}
</View> </View>

View File

@ -1,3 +1 @@
export * from './homeScreen'; export * from './homeScreen';
export { default as homeReducer } from './reducer';
export * from './thunk';

View File

@ -1,36 +0,0 @@
import { createReducer } from '@reduxjs/toolkit';
import { Product, Products } from '@interfaces';
import { getAllProductsThunk } from './thunk';
export interface HomeState {
products: Products[];
isLoading: boolean;
error: string | null;
}
const initialState: HomeState = {
products: [],
isLoading: false,
error: null,
};
const homeReducer = createReducer(initialState, builder => {
builder
.addCase(getAllProductsThunk.pending, state => {
state.isLoading = true;
state.error = null;
})
.addCase(getAllProductsThunk.fulfilled, (state, action) => {
// Handle the API returning { products, meta }
state.products =
action.payload?.products ||
(Array.isArray(action.payload) ? action.payload : []);
state.isLoading = false;
})
.addCase(getAllProductsThunk.rejected, (state, action) => {
state.isLoading = false;
state.error = action.payload || 'Failed to fetch products';
});
});
export default homeReducer;

View File

@ -1,17 +0,0 @@
import { createAsyncThunk } from '@reduxjs/toolkit';
import { getProductsApi, GetProductsResponse } from '@api';
export const getAllProductsThunk = createAsyncThunk<
GetProductsResponse,
void,
{ rejectValue: string }
>('home/getAllProducts', async (_, { rejectWithValue }) => {
try {
const response = await getProductsApi();
return response;
} catch (error: any) {
return rejectWithValue(
error.response?.data?.message || 'Failed to fetch products',
);
}
});

View File

@ -19,5 +19,3 @@ export * from './myOrdersScreen';
export * from './offersScreen'; export * from './offersScreen';
export * from './helpSupportScreen'; export * from './helpSupportScreen';
export * from './accountScreen'; export * from './accountScreen';
export * from './writeReviewScreen';
export * from './orderDetailsScreen';

View File

@ -1,9 +1,11 @@
import React, { useEffect, useState } from 'react'; import React, { useState } from 'react';
import { View, Text, FlatList, TouchableOpacity } from 'react-native';
import { import {
useNavigation, View,
CompositeNavigationProp, Text,
} from '@react-navigation/native'; FlatList,
TouchableOpacity,
} from 'react-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 { StackNavigationProp } from '@react-navigation/stack';
import { getStyles } from './myOrdersScreen.styles'; import { getStyles } from './myOrdersScreen.styles';
@ -11,9 +13,6 @@ import { Header, OrderHistoryCard } from '@components';
import { useAppTheme } from '@theme'; import { useAppTheme } from '@theme';
import { AppStackParamList } from '../../../navigation/appStack'; import { AppStackParamList } from '../../../navigation/appStack';
import { MainTabParamList } from '../../../navigation/mainTabNavigator'; import { MainTabParamList } from '../../../navigation/mainTabNavigator';
import { RootState, useAppDispatch, useAppSelector } from '@store';
import { getOrderHistoryThunk } from '../checkoutPaymentScreen';
import { formatDate } from '@utils/helper';
type MyOrdersNavProp = CompositeNavigationProp< type MyOrdersNavProp = CompositeNavigationProp<
BottomTabNavigationProp<MainTabParamList, 'MyOrdersScreen'>, BottomTabNavigationProp<MainTabParamList, 'MyOrdersScreen'>,
@ -22,30 +21,9 @@ type MyOrdersNavProp = CompositeNavigationProp<
const TABS = ['All', 'Ongoing', 'Completed']; const TABS = ['All', 'Ongoing', 'Completed'];
const mockOrders = [ const mockOrders = [
{ { id: 'ORD-591283', providerName: 'Pizza Planet', providerImage: '', orderDate: '29 Jun 2026', status: 'Delivered', total: 599 },
id: 'ORD-591283', { id: 'ORD-771239', providerName: 'Fresh Mart', providerImage: '', orderDate: '28 Jun 2026', status: 'Ongoing', total: 349 },
providerName: 'Pizza Planet', { id: 'ORD-108239', providerName: 'Burger Barn', providerImage: '', orderDate: '27 Jun 2026', status: 'Delivered', total: 449 },
providerImage: '',
orderDate: '29 Jun 2026',
status: 'Delivered',
total: 599,
},
{
id: 'ORD-771239',
providerName: 'Fresh Mart',
providerImage: '',
orderDate: '28 Jun 2026',
status: 'Ongoing',
total: 349,
},
{
id: 'ORD-108239',
providerName: 'Burger Barn',
providerImage: '',
orderDate: '27 Jun 2026',
status: 'Delivered',
total: 449,
},
]; ];
export const MyOrdersScreen: React.FC = () => { export const MyOrdersScreen: React.FC = () => {
@ -53,42 +31,24 @@ export const MyOrdersScreen: React.FC = () => {
const styles = getStyles(colors); const styles = getStyles(colors);
const navigation = useNavigation<MyOrdersNavProp>(); const navigation = useNavigation<MyOrdersNavProp>();
const [activeTab, setActiveTab] = useState('All'); const [activeTab, setActiveTab] = useState('All');
const dispatch = useAppDispatch();
const { orderHistory } = useAppSelector(
(state: RootState) => state.paymentMethods,
);
console.log(orderHistory);
useEffect(() => { const filteredOrders = activeTab === 'All'
dispatch(getOrderHistoryThunk()); ? mockOrders
}, []);
const ONGOING_STATUSES = [
'PENDING',
'CONFIRMED',
'PREPARING',
'READY_FOR_PICKUP',
'OUT_FOR_DELIVERY',
];
const COMPLETED_STATUSES = ['DELIVERED', 'CANCELLED'];
const orders = Array.isArray(orderHistory) ? orderHistory : [];
const filteredOrders =
activeTab === 'All'
? orders
: activeTab === 'Ongoing' : activeTab === 'Ongoing'
? orders.filter(o => ONGOING_STATUSES.includes(o.status)) ? mockOrders.filter((o) => o.status === 'Ongoing')
: orders.filter(o => COMPLETED_STATUSES.includes(o.status)); : mockOrders.filter((o) => o.status === 'Delivered');
return ( return (
<View style={styles.container}> <View style={styles.container}>
<Header title="My Orders" /> <Header title="My Orders" />
<View style={styles.tabRow}> <View style={styles.tabRow}>
{TABS.map(tab => ( {TABS.map((tab) => (
<TouchableOpacity <TouchableOpacity
key={tab} key={tab}
style={[styles.tab, activeTab === tab && styles.tabActive]} style={[
styles.tab,
activeTab === tab && styles.tabActive,
]}
onPress={() => setActiveTab(tab)} onPress={() => setActiveTab(tab)}
activeOpacity={0.7} activeOpacity={0.7}
> >
@ -105,36 +65,25 @@ export const MyOrdersScreen: React.FC = () => {
</View> </View>
<FlatList <FlatList
data={filteredOrders} data={filteredOrders}
keyExtractor={item => item.id} keyExtractor={(item) => item.id}
renderItem={({ item }) => ( renderItem={({ item }) => (
<OrderHistoryCard <OrderHistoryCard
providerName={item?.merchant?.name || ''} providerName={item.providerName}
providerImage={item?.merchant?.imageUrl || ''} providerImage={item.providerImage}
orderDate={formatDate(item?.createdAt)} orderDate={item.orderDate}
status={item.status} status={item.status}
total={Number(item?.totalAmount) || 0} total={item.total}
items={item.orderItems}
onReorder={() => { onReorder={() => {
navigation.navigate('ProviderDetailsScreen', { navigation.navigate('ProviderDetailsScreen', {
providerId: 'p1', providerId: 'p1',
providerName: item?.merchant?.name || '', providerName: item.providerName,
}); });
}} }}
onDetails={() => { onDetails={() => {
if ( if (item.status === 'Ongoing') {
item?.status === 'PENDING' || navigation.navigate('OrderTrackingScreen', { orderId: item.id });
item?.status === 'CONFIRMED' ||
item?.status === 'PREPARING' ||
item?.status === 'READY_FOR_PICKUP' ||
item?.status === 'OUT_FOR_DELIVERY'
) {
navigation.navigate('OrderDetailsScreen', {
orderId: item.id,
});
} else { } else {
navigation.navigate('OrderDeliveredScreen', { navigation.navigate('OrderDeliveredScreen', { orderId: item.id });
orderId: item.id,
});
} }
}} }}
/> />

View File

@ -3,9 +3,8 @@ import { View, Text } from 'react-native';
import { getStyles } from './onboardingCompleteScreen.styles'; import { getStyles } from './onboardingCompleteScreen.styles';
import { PrimaryButton } from '@components'; import { PrimaryButton } from '@components';
import { useAppTheme } from '@theme'; import { useAppTheme } from '@theme';
import { useAppDispatch } from '../../../store';
import { useAppDispatch } from '@store'; import { completeOnboarding } from '../../../store/commonreducers/auth';
import { completeOnboarding } from '@store/commonreducers/auth';
export const OnboardingCompleteScreen: React.FC = () => { export const OnboardingCompleteScreen: React.FC = () => {
const { colors } = useAppTheme(); const { colors } = useAppTheme();
@ -13,9 +12,6 @@ export const OnboardingCompleteScreen: React.FC = () => {
const dispatch = useAppDispatch(); const dispatch = useAppDispatch();
const handleExplore = () => { const handleExplore = () => {
// Dispatching completeOnboarding sets user.status = 'ACTIVE' in Redux.
// RootNavigator watches this value and automatically swaps OnboardingStack
// → AppStack. No direct navigation call needed or possible here.
dispatch(completeOnboarding()); dispatch(completeOnboarding());
}; };
@ -29,7 +25,10 @@ export const OnboardingCompleteScreen: React.FC = () => {
</Text> </Text>
</View> </View>
<View style={styles.footer}> <View style={styles.footer}>
<PrimaryButton title="Explore Now" onPress={handleExplore} /> <PrimaryButton
title="Explore Now"
onPress={handleExplore}
/>
</View> </View>
</View> </View>
); );

View File

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

View File

@ -1,194 +0,0 @@
import { StyleSheet } from 'react-native';
import { typography } from '@theme';
export const getStyles = (colors: any) =>
StyleSheet.create({
container: {
flex: 1,
backgroundColor: colors.background,
},
loadingContainer: {
flex: 1,
justifyContent: 'center',
alignItems: 'center',
},
loadingText: {
marginTop: 12,
fontSize: typography.fontSize.md,
color: colors.textSecondary,
},
errorContainer: {
flex: 1,
justifyContent: 'center',
alignItems: 'center',
padding: 20,
},
errorText: {
fontSize: typography.fontSize.md,
color: colors.error,
textAlign: 'center',
},
content: {
padding: 16,
paddingBottom: 40,
},
section: {
backgroundColor: colors.cardBg,
borderRadius: 12,
padding: 16,
marginBottom: 16,
borderWidth: 1,
borderColor: colors.border,
},
// Used for the OrderStatusTimeline section — no inner padding as the
// component renders its own self-contained card with rounded borders.
timelineSection: {
marginBottom: 16,
},
sectionTitle: {
fontSize: typography.fontSize.md,
fontWeight: typography.fontWeight.bold,
color: colors.text,
marginBottom: 12,
},
headerRow: {
flexDirection: 'row',
justifyContent: 'space-between',
alignItems: 'center',
marginBottom: 12,
},
orderNumber: {
fontSize: typography.fontSize.lg,
fontWeight: typography.fontWeight.bold,
color: colors.text,
},
statusBadge: {
paddingHorizontal: 10,
paddingVertical: 4,
borderRadius: 12,
backgroundColor: '#E8F5E9',
},
statusText: {
fontSize: typography.fontSize.xs,
fontWeight: typography.fontWeight.bold,
color: colors.primary,
},
dateText: {
fontSize: typography.fontSize.sm,
color: colors.textSecondary,
marginBottom: 4,
},
merchantRow: {
flexDirection: 'row',
alignItems: 'center',
marginTop: 8,
paddingTop: 12,
borderTopWidth: 1,
borderTopColor: colors.border,
},
merchantIcon: {
width: 32,
height: 32,
borderRadius: 16,
backgroundColor: colors.surface,
justifyContent: 'center',
alignItems: 'center',
marginRight: 12,
},
merchantName: {
fontSize: typography.fontSize.md,
fontWeight: typography.fontWeight.semibold,
color: colors.text,
},
itemRow: {
flexDirection: 'row',
justifyContent: 'space-between',
alignItems: 'flex-start',
marginBottom: 12,
},
itemInfo: {
flex: 1,
flexDirection: 'row',
alignItems: 'flex-start',
marginRight: 16,
},
itemQuantityBadge: {
backgroundColor: colors.surface,
borderWidth: 1,
borderColor: colors.border,
borderRadius: 4,
paddingHorizontal: 6,
paddingVertical: 2,
marginRight: 10,
marginTop: 2,
},
itemQuantityText: {
fontSize: typography.fontSize.xs,
fontWeight: typography.fontWeight.bold,
color: colors.text,
},
itemName: {
fontSize: typography.fontSize.sm,
color: colors.text,
lineHeight: 20,
},
itemPrice: {
fontSize: typography.fontSize.sm,
fontWeight: typography.fontWeight.semibold,
color: colors.text,
marginTop: 2,
},
billRow: {
flexDirection: 'row',
justifyContent: 'space-between',
marginBottom: 8,
},
billLabel: {
fontSize: typography.fontSize.sm,
color: colors.textSecondary,
},
billValue: {
fontSize: typography.fontSize.sm,
color: colors.text,
fontWeight: typography.fontWeight.medium,
},
billDivider: {
height: 1,
backgroundColor: colors.border,
marginVertical: 12,
},
billTotalLabel: {
fontSize: typography.fontSize.md,
fontWeight: typography.fontWeight.bold,
color: colors.text,
},
billTotalValue: {
fontSize: typography.fontSize.md,
fontWeight: typography.fontWeight.bold,
color: colors.primary,
},
addressType: {
fontSize: typography.fontSize.sm,
fontWeight: typography.fontWeight.semibold,
color: colors.text,
marginBottom: 4,
},
addressText: {
fontSize: typography.fontSize.sm,
color: colors.textSecondary,
lineHeight: 20,
},
paymentRow: {
flexDirection: 'row',
alignItems: 'center',
},
paymentIcon: {
marginRight: 8,
fontSize: 20,
},
paymentText: {
fontSize: typography.fontSize.sm,
color: colors.text,
fontWeight: typography.fontWeight.medium,
},
});

View File

@ -1,191 +0,0 @@
import React, { useEffect } from 'react';
import { View, Text, ScrollView, ActivityIndicator } from 'react-native';
import { useRoute, useNavigation } from '@react-navigation/native';
import { StackNavigationProp } from '@react-navigation/stack';
import { RouteProp } from '@react-navigation/native';
import { Header, OrderStatusTimeline } from '@components';
import { useAppTheme } from '@theme';
import { getStyles } from './orderDetailsScreen.styles';
import { RootState, useAppDispatch, useAppSelector } from '@store';
import { getOrderByIdThunk } from '../checkoutPaymentScreen';
import { formatDate, formatTime } from '@utils/helper';
import { AppStackParamList } from '../../../navigation/appStack';
type OrderDetailsRouteProp = RouteProp<AppStackParamList, 'OrderDetailsScreen'>;
type OrderDetailsNavProp = StackNavigationProp<AppStackParamList>;
export const OrderDetailsScreen: React.FC = () => {
const { colors } = useAppTheme();
const styles = getStyles(colors);
const route = useRoute<OrderDetailsRouteProp>();
const navigation = useNavigation<OrderDetailsNavProp>();
const dispatch = useAppDispatch();
const orderId = route.params?.orderId;
const { orderDetails, orderDetailsLoading, orderDetailsError } =
useAppSelector((state: RootState) => state.paymentMethods);
useEffect(() => {
if (orderId) {
dispatch(getOrderByIdThunk(orderId));
}
}, [dispatch, orderId]);
if (orderDetailsLoading) {
return (
<View style={styles.container}>
<Header title="Order Details" onBack={() => navigation.goBack()} />
<View style={styles.loadingContainer}>
<ActivityIndicator size="large" color={colors.primary} />
<Text style={styles.loadingText}>Fetching order details...</Text>
</View>
</View>
);
}
if (orderDetailsError || !orderDetails) {
return (
<View style={styles.container}>
<Header title="Order Details" onBack={() => navigation.goBack()} />
<View style={styles.errorContainer}>
<Text style={styles.errorText}>
{orderDetailsError || 'Could not load order details.'}
</Text>
</View>
</View>
);
}
const orderDate = orderDetails.createdAt
? `${formatDate(orderDetails.createdAt)} at ${formatTime(
orderDetails.createdAt,
)}`
: '';
return (
<View style={styles.container}>
<Header title="Order Details" onBack={() => navigation.goBack()} />
<ScrollView contentContainerStyle={styles.content}>
{/* Top Info Section */}
<View style={styles.section}>
<View style={styles.headerRow}>
<Text style={styles.orderNumber}>
Order {orderDetails.orderNumber?.split('-').pop() || ''}
</Text>
<View style={styles.statusBadge}>
<Text style={styles.statusText}>{orderDetails.status}</Text>
</View>
</View>
<Text style={styles.dateText}>Placed on {orderDate}</Text>
<Text style={styles.dateText}>
Payment: {orderDetails.paymentMethod}
</Text>
{/* Merchant Info */}
<View style={styles.merchantRow}>
<View style={styles.merchantIcon}>
<Text>🏪</Text>
</View>
<Text style={styles.merchantName}>
{orderDetails.merchant?.name || 'Store'}
</Text>
</View>
</View>
{/* Order Status Timeline Section */}
<View style={styles.timelineSection}>
<Text style={styles.sectionTitle}>Order Status</Text>
<OrderStatusTimeline
tracking={orderDetails.tracking}
currentStatus={orderDetails.status}
/>
</View>
{/* Order Items Section */}
<View style={styles.section}>
<Text style={styles.sectionTitle}>Order Items</Text>
{orderDetails.orderItems?.map((item, index) => (
<View key={item.id || index} style={styles.itemRow}>
<View style={styles.itemInfo}>
<View style={styles.itemQuantityBadge}>
<Text style={styles.itemQuantityText}>{item.quantity}x</Text>
</View>
<Text style={styles.itemName}>{item.name}</Text>
</View>
<Text style={styles.itemPrice}>{item.totalAmount}</Text>
</View>
))}
</View>
{/* Bill Summary Section */}
<View style={styles.section}>
<Text style={styles.sectionTitle}>Bill Summary</Text>
<View style={styles.billRow}>
<Text style={styles.billLabel}>Item Total</Text>
<Text style={styles.billValue}>{orderDetails.subtotal}</Text>
</View>
<View style={styles.billRow}>
<Text style={styles.billLabel}>Delivery Fee</Text>
<Text style={styles.billValue}>{orderDetails.deliveryFee}</Text>
</View>
<View style={styles.billRow}>
<Text style={styles.billLabel}>Platform Fee</Text>
<Text style={styles.billValue}>{orderDetails.platformFee}</Text>
</View>
<View style={styles.billRow}>
<Text style={styles.billLabel}>Taxes</Text>
<Text style={styles.billValue}>{orderDetails.taxAmount}</Text>
</View>
<View style={styles.billDivider} />
<View style={styles.billRow}>
<Text style={styles.billTotalLabel}>Grand Total</Text>
<Text style={styles.billTotalValue}>
{orderDetails.totalAmount}
</Text>
</View>
</View>
{/* Delivery Address Section */}
<View style={styles.section}>
<Text style={styles.sectionTitle}>Delivery Details</Text>
<Text style={styles.addressType}>
{orderDetails.dropAddress?.label || 'Home'}
</Text>
<Text style={styles.addressText}>
{orderDetails.dropAddress?.houseNumber
? `${orderDetails.dropAddress.houseNumber}, `
: ''}
{orderDetails.dropAddress?.addressLine1}
</Text>
<Text style={styles.addressText}>
{orderDetails.dropAddress?.landmark
? `Landmark: ${orderDetails.dropAddress.landmark}`
: ''}
</Text>
<Text style={styles.addressText}>
Phone: {orderDetails.dropAddress?.phone}
</Text>
</View>
{/* Payment Info */}
{orderDetails.payments && orderDetails.payments.length > 0 && (
<View style={styles.section}>
<Text style={styles.sectionTitle}>Payment Details</Text>
<View style={styles.paymentRow}>
<Text style={styles.paymentIcon}>💳</Text>
<Text style={styles.paymentText}>
Paid via {orderDetails.payments[0].method} (
{orderDetails.payments[0].status})
</Text>
</View>
</View>
)}
</ScrollView>
</View>
);
};
export default OrderDetailsScreen;

View File

@ -1,108 +0,0 @@
import { useState, useEffect, useCallback } from 'react';
import { Alert } from 'react-native';
import { useNavigation, useRoute, RouteProp } from '@react-navigation/native';
import { StackNavigationProp } from '@react-navigation/stack';
import { RootState, useAppDispatch, useAppSelector, verifyOtp } from '@store';
import { AuthStackParamList } from '@navigation/authStack';
type OtpNavProp = StackNavigationProp<AuthStackParamList, 'OtpScreen'>;
type OtpRouteProp = RouteProp<AuthStackParamList, 'OtpScreen'>;
const OTP_LENGTH = 6;
const RESEND_TIMER = 30;
export const useOtpScreen = () => {
const navigation = useNavigation<OtpNavProp>();
const route = useRoute<OtpRouteProp>();
const dispatch = useAppDispatch();
const { mobileNumber } = route.params;
const { loginData, user } = useAppSelector((state: RootState) => state.auth);
const [otp, setOtp] = useState<string[]>(Array(OTP_LENGTH).fill(''));
const [activeIndex, setActiveIndex] = useState(0);
const [timer, setTimer] = useState(RESEND_TIMER);
const [isLoading, setIsLoading] = useState(false);
useEffect(() => {
if (timer > 0) {
const interval = setInterval(() => setTimer(t => t - 1), 1000);
return () => clearInterval(interval);
}
}, [timer]);
const resetOtp = useCallback(() => {
setOtp(Array(OTP_LENGTH).fill(''));
setActiveIndex(0);
}, []);
const handleKeyPress = useCallback(
(key: string) => {
if (key === 'backspace') {
if (otp[activeIndex] || activeIndex > 0) {
const newOtp = [...otp];
if (otp[activeIndex]) {
newOtp[activeIndex] = '';
} else if (activeIndex > 0) {
newOtp[activeIndex - 1] = '';
setActiveIndex(activeIndex - 1);
}
setOtp(newOtp);
}
} else if (key >= '0' && key <= '9' && activeIndex < OTP_LENGTH) {
const newOtp = [...otp];
newOtp[activeIndex] = key;
setOtp(newOtp);
if (activeIndex < OTP_LENGTH - 1) {
setActiveIndex(activeIndex + 1);
}
}
},
[otp, activeIndex],
);
const handleResend = useCallback(() => {
setTimer(RESEND_TIMER);
resetOtp();
// TODO: dispatch resend/loginWithPhone thunk here if needed
}, [resetOtp]);
const handleVerify = useCallback(async () => {
const otpString = otp.join('');
if (otpString.length !== OTP_LENGTH) return;
setIsLoading(true);
try {
await dispatch(
verifyOtp({ phone: mobileNumber, code: otpString, role: 'CUSTOMER' }),
).unwrap();
navigation.navigate('SetLocationScreen');
} catch (error) {
const message =
typeof error === 'string' ? error : 'Invalid OTP. Please try again.';
Alert.alert('Verification Failed', message);
resetOtp();
} finally {
setIsLoading(false);
}
}, [dispatch, mobileNumber, navigation, otp, resetOtp]);
return {
// data
mobileNumber,
loginData,
user,
otp,
activeIndex,
timer,
isLoading,
otpLength: OTP_LENGTH,
isOtpComplete: otp.join('').length === OTP_LENGTH,
// actions
handleKeyPress,
handleVerify,
handleResend,
};
};

View File

@ -1,4 +1,4 @@
import React from 'react'; import React, { useState, useEffect } from 'react';
import { import {
View, View,
Text, Text,
@ -6,27 +6,71 @@ import {
KeyboardAvoidingView, KeyboardAvoidingView,
Platform, Platform,
} from 'react-native'; } from 'react-native';
import { useNavigation, useRoute, RouteProp } from '@react-navigation/native';
import { StackNavigationProp } from '@react-navigation/stack';
import { getStyles } from './otpScreen.styles'; import { getStyles } from './otpScreen.styles';
import { PrimaryButton } from '@components'; import { PrimaryButton } from '@components';
import { useAppTheme } from '@theme'; import { useAppTheme } from '@theme';
import { useOtpScreen } from './hooks/useOtpScreen'; import { useAppDispatch } from '../../../store';
import { verifyOtp } from '../../../store/commonreducers/auth';
import { AuthStackParamList } from '../../../navigation/authStack';
type OtpNavProp = StackNavigationProp<AuthStackParamList, 'OtpScreen'>;
type OtpRouteProp = RouteProp<AuthStackParamList, 'OtpScreen'>;
const OTP_LENGTH = 6;
const RESEND_TIMER = 30;
export const OtpScreen: React.FC = () => { export const OtpScreen: React.FC = () => {
const { colors } = useAppTheme(); const { colors } = useAppTheme();
const styles = getStyles(colors); const styles = getStyles(colors);
const navigation = useNavigation<OtpNavProp>();
const route = useRoute<OtpRouteProp>();
const dispatch = useAppDispatch();
const { mobileNumber } = route.params;
const { const [otp, setOtp] = useState<string[]>(Array(OTP_LENGTH).fill(''));
mobileNumber, const [activeIndex, setActiveIndex] = useState(0);
otp, const [timer, setTimer] = useState(RESEND_TIMER);
activeIndex, const [isLoading, setIsLoading] = useState(false);
timer,
isLoading, useEffect(() => {
otpLength, if (timer > 0) {
isOtpComplete, const interval = setInterval(() => setTimer((t) => t - 1), 1000);
handleKeyPress, return () => clearInterval(interval);
handleVerify, }
handleResend, }, [timer]);
} = useOtpScreen();
const handleKeyPress = (key: string) => {
if (key === 'backspace') {
if (otp[activeIndex] || activeIndex > 0) {
const newOtp = [...otp];
if (otp[activeIndex]) {
newOtp[activeIndex] = '';
} else if (activeIndex > 0) {
newOtp[activeIndex - 1] = '';
setActiveIndex(activeIndex - 1);
}
setOtp(newOtp);
}
} else if (key >= '0' && key <= '9' && activeIndex < OTP_LENGTH) {
const newOtp = [...otp];
newOtp[activeIndex] = key;
setOtp(newOtp);
if (activeIndex < OTP_LENGTH - 1) {
setActiveIndex(activeIndex + 1);
}
}
};
const handleVerify = async () => {
const otpString = otp.join('');
if (otpString.length !== OTP_LENGTH) return;
setIsLoading(true);
await dispatch(verifyOtp({ mobileNumber, otp: otpString }));
setIsLoading(false);
navigation.navigate('SetLocationScreen');
};
return ( return (
<KeyboardAvoidingView <KeyboardAvoidingView
@ -35,40 +79,12 @@ export const OtpScreen: React.FC = () => {
> >
<View style={styles.content}> <View style={styles.content}>
<Text style={styles.title}>Verify OTP</Text> <Text style={styles.title}>Verify OTP</Text>
<Text style={styles.subtitle}>Code sent to {mobileNumber}</Text> <Text style={styles.subtitle}>
Code sent to {mobileNumber}
</Text>
<View style={styles.otpContainer}> <View style={styles.otpContainer}>
{otp.map( {otp.map((digit, index) => (
(
digit:
| string
| number
| bigint
| boolean
| React.ReactElement<
unknown,
string | React.JSXElementConstructor<any>
>
| Iterable<React.ReactNode>
| React.ReactPortal
| Promise<
| string
| number
| bigint
| boolean
| React.ReactPortal
| React.ReactElement<
unknown,
string | React.JSXElementConstructor<any>
>
| Iterable<React.ReactNode>
| null
| undefined
>
| null
| undefined,
index: React.Key | null | undefined,
) => (
<View <View
key={index} key={index}
style={[ style={[
@ -79,12 +95,11 @@ export const OtpScreen: React.FC = () => {
> >
<Text style={styles.otpCellText}>{digit}</Text> <Text style={styles.otpCellText}>{digit}</Text>
</View> </View>
), ))}
)}
</View> </View>
<View style={styles.keypad}> <View style={styles.keypad}>
{[1, 2, 3, 4, 5, 6, 7, 8, 9].map(num => ( {[1, 2, 3, 4, 5, 6, 7, 8, 9].map((num) => (
<TouchableOpacity <TouchableOpacity
key={num} key={num}
style={styles.key} style={styles.key}
@ -114,7 +129,14 @@ export const OtpScreen: React.FC = () => {
{timer > 0 ? ( {timer > 0 ? (
<Text style={styles.timerText}>Resend code in {timer}s</Text> <Text style={styles.timerText}>Resend code in {timer}s</Text>
) : ( ) : (
<TouchableOpacity onPress={handleResend} activeOpacity={0.7}> <TouchableOpacity
onPress={() => {
setTimer(RESEND_TIMER);
setOtp(Array(OTP_LENGTH).fill(''));
setActiveIndex(0);
}}
activeOpacity={0.7}
>
<Text style={styles.resendText}>Resend OTP</Text> <Text style={styles.resendText}>Resend OTP</Text>
</TouchableOpacity> </TouchableOpacity>
)} )}
@ -123,7 +145,7 @@ export const OtpScreen: React.FC = () => {
title="Verify" title="Verify"
onPress={handleVerify} onPress={handleVerify}
isLoading={isLoading} isLoading={isLoading}
disabled={!isOtpComplete} disabled={otp.join('').length !== OTP_LENGTH}
style={{ marginTop: 24 }} style={{ marginTop: 24 }}
/> />
</View> </View>

View File

@ -1,3 +1 @@
export * from './preferencesScreen'; export * from './preferencesScreen';
export * from './thunk';
export * from './reducer';

View File

@ -1,12 +1,10 @@
import React, { useEffect, useState } from 'react'; import React, { useState } from 'react';
import { import {
View, View,
Text, Text,
TouchableOpacity, TouchableOpacity,
Switch, Switch,
ScrollView, ScrollView,
ActivityIndicator,
Alert,
} from 'react-native'; } from 'react-native';
import { useNavigation } from '@react-navigation/native'; import { useNavigation } from '@react-navigation/native';
import { StackNavigationProp } from '@react-navigation/stack'; import { StackNavigationProp } from '@react-navigation/stack';
@ -14,138 +12,60 @@ import { getStyles } from './preferencesScreen.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 { useAppDispatch, useAppSelector } from '@store';
import { fetchCategories, completeOnboard } from './thunk';
import { OnboardingStackParamList } from '@navigation/onboardingStack';
type NavProp = StackNavigationProp< type NavProp = StackNavigationProp<AuthStackParamList, 'PreferencesScreen'>;
OnboardingStackParamList,
'PreferencesScreen' const CATEGORIES = [
>; { key: 'food', icon: '🍔', label: 'Food' },
{ key: 'groceries', icon: '🛒', label: 'Groceries' },
{ key: 'pharmacy', icon: '💊', label: 'Pharmacy' },
{ key: 'others', icon: '📦', label: 'Others' },
];
export const PreferencesScreen: React.FC = () => { export const PreferencesScreen: 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 dispatch = useAppDispatch();
// ─── Redux state ────────────────────────────────────────────────────────── const [selectedCategories, setSelectedCategories] = useState<string[]>(['food']);
const { categories, isLoading, error, isSubmitting, submitError } =
useAppSelector(state => state.preferences);
const locationData = useAppSelector(state => state.setLocation);
const profileData = useAppSelector(state => state.completeProfile);
// ─── Local state ──────────────────────────────────────────────────────────
const [selectedCategories, setSelectedCategories] = useState<string[]>([]);
const [notificationsEnabled, setNotificationsEnabled] = useState(true); const [notificationsEnabled, setNotificationsEnabled] = useState(true);
// ─── Fetch categories on mount ──────────────────────────────────────────── const toggleCategory = (key: string) => {
useEffect(() => { setSelectedCategories((prev) =>
dispatch(fetchCategories()); prev.includes(key)
}, [dispatch]); ? prev.filter((c) => c !== key)
: [...prev, key],
const toggleCategory = (id: string) => {
setSelectedCategories(prev =>
prev.includes(id) ? prev.filter(c => c !== id) : [...prev, id],
); );
}; };
// ─── Continue → call onboard API ─────────────────────────────────────────
const handleContinue = async () => {
if (!locationData.latitude || !locationData.longitude) {
Alert.alert('Location missing', 'Please set your location first.');
return;
}
if (!profileData.name || !profileData.email) {
Alert.alert('Profile incomplete', 'Please complete your profile first.');
return;
}
const payload = {
name: profileData.name,
email: profileData.email,
gender: profileData.gender,
latitude: locationData.latitude,
longitude: locationData.longitude,
addressLabel: profileData.addressLabel,
addressLine1: profileData.addressLine1,
mapAddress: locationData.mapAddress,
houseNumber: profileData.houseNumber,
landmark: profileData.landmark,
city: profileData.city,
state: profileData.state,
postalCode: profileData.postalCode,
addressPhone: profileData.addressPhone,
categoryPreferences: selectedCategories,
};
const result = await dispatch(completeOnboard(payload));
if (completeOnboard.fulfilled.match(result)) {
navigation.navigate('OnboardingCompleteScreen');
} else {
Alert.alert(
'Onboarding failed',
(result.payload as string) ?? 'Something went wrong. Please try again.',
);
}
};
return ( return (
<ScrollView style={styles.container} contentContainerStyle={styles.content}> <ScrollView style={styles.container} contentContainerStyle={styles.content}>
<Text style={styles.title}>Preferences</Text> <Text style={styles.title}>Preferences</Text>
<Text style={styles.subtitle}>Select your interests</Text> <Text style={styles.subtitle}>Select your interests</Text>
{isLoading && (
<ActivityIndicator
size="large"
color={colors.primary}
style={{ marginVertical: 32 }}
/>
)}
{!!error && (
<Text
style={{
color: colors.error ?? 'red',
textAlign: 'center',
marginBottom: 16,
}}
>
{error}
</Text>
)}
{!!submitError && (
<Text
style={{
color: colors.error ?? 'red',
textAlign: 'center',
marginBottom: 16,
}}
>
{submitError}
</Text>
)}
{!isLoading && (
<View style={styles.grid}> <View style={styles.grid}>
{(categories || []).map(cat => ( {CATEGORIES.map((cat) => (
<TouchableOpacity <TouchableOpacity
key={cat.id} key={cat.key}
style={[ style={[
styles.gridItem, styles.gridItem,
selectedCategories.includes(cat.id) && styles.gridItemSelected, selectedCategories.includes(cat.key) && styles.gridItemSelected,
]} ]}
onPress={() => toggleCategory(cat.id)} onPress={() => toggleCategory(cat.key)}
activeOpacity={0.7} activeOpacity={0.7}
> >
<Text style={styles.gridLabel}>{cat.name}</Text> <Text style={styles.gridIcon}>{cat.icon}</Text>
<Text
style={[
styles.gridLabel,
selectedCategories.includes(cat.key) && styles.gridLabelSelected,
]}
>
{cat.label}
</Text>
</TouchableOpacity> </TouchableOpacity>
))} ))}
</View> </View>
)}
<View style={styles.toggleRow}> <View style={styles.toggleRow}>
<View> <View>
@ -161,9 +81,8 @@ export const PreferencesScreen: React.FC = () => {
</View> </View>
<PrimaryButton <PrimaryButton
title={isSubmitting ? 'Please wait…' : 'Continue'} title="Continue"
onPress={handleContinue} onPress={() => navigation.navigate('OnboardingCompleteScreen')}
disabled={isSubmitting}
style={{ marginTop: 32 }} style={{ marginTop: 32 }}
/> />
</ScrollView> </ScrollView>

View File

@ -1,59 +0,0 @@
import { createReducer } from '@reduxjs/toolkit';
import { Category } from '@interfaces';
import { fetchCategories, completeOnboard } from './thunk';
// ─── State ────────────────────────────────────────────────────────────────────
export interface PreferencesState {
categories: Category[];
selectedCategories: string[];
isLoading: boolean;
isSubmitting: boolean;
error: string | null;
submitError: string | null;
}
const initialState: PreferencesState = {
categories: [],
selectedCategories: [],
isLoading: false,
isSubmitting: false,
error: null,
submitError: null,
};
// ─── Reducer ──────────────────────────────────────────────────────────────────
const preferencesReducer = createReducer(initialState, builder => {
builder
// Fetch categories
.addCase(fetchCategories.pending, state => {
state.isLoading = true;
state.error = null;
})
.addCase(fetchCategories.fulfilled, (state, action) => {
state.categories = action.payload;
state.isLoading = false;
state.error = null;
})
.addCase(fetchCategories.rejected, (state, action) => {
state.isLoading = false;
state.error = action.payload as string;
})
// Complete onboard
.addCase(completeOnboard.pending, state => {
state.isSubmitting = true;
state.submitError = null;
})
.addCase(completeOnboard.fulfilled, state => {
state.isSubmitting = false;
state.submitError = null;
})
.addCase(completeOnboard.rejected, (state, action) => {
state.isSubmitting = false;
state.submitError = action.payload as string;
});
});
export default preferencesReducer;

View File

@ -1,33 +0,0 @@
import { createAsyncThunk } from '@reduxjs/toolkit';
import { getCatagories, onBoardComplete } from '../../../api/onboardApi';
import { onBoardPayload } from '@interfaces/onboard';
// ─── Fetch Categories ─────────────────────────────────────────────────────────
export const fetchCategories = createAsyncThunk(
'preferences/fetchCategories',
async (_, { rejectWithValue }) => {
try {
const response = await getCatagories();
return response;
} catch (error: unknown) {
const message =
error instanceof Error ? error.message : 'Failed to fetch categories';
return rejectWithValue(message);
}
},
);
export const completeOnboard = createAsyncThunk(
'preferences/completeOnboard',
async (payload: onBoardPayload, { rejectWithValue }) => {
try {
const response = await onBoardComplete(payload);
return response;
} catch (error: unknown) {
const message =
error instanceof Error ? error.message : 'Failed to complete onboard';
return rejectWithValue(message);
}
},
);

View File

@ -1,3 +1 @@
export * from './providerDetailsScreen'; export * from './providerDetailsScreen';
export { default as providerDetailsReducer } from './reducer';
export * from './thunk';

View File

@ -1,8 +1,8 @@
import { StyleSheet, Dimensions, Platform } from 'react-native'; import { StyleSheet, Dimensions, Platform } from 'react-native';
import { typography } from '@theme'; import { typography } from '@theme';
const { width, height } = Dimensions.get('window'); const { width } = Dimensions.get('window');
const HERO_HEIGHT = height * 0.45; const HERO_HEIGHT = 220;
export const getStyles = (colors: any) => export const getStyles = (colors: any) =>
StyleSheet.create({ StyleSheet.create({
@ -14,49 +14,36 @@ export const getStyles = (colors: any) =>
flex: 1, flex: 1,
}, },
contentBody: { contentBody: {
paddingBottom: 100, // Space for sticky bottom bar paddingBottom: 32,
}, },
// ---------- Image Carousel ---------- // ---------- Hero ----------
carouselWrap: { heroWrap: {
width, width,
height: HERO_HEIGHT, height: HERO_HEIGHT,
backgroundColor: colors.background, backgroundColor: colors.inputBg,
}, },
carouselImage: { heroImage: {
width, width: '100%',
height: HERO_HEIGHT, height: '100%',
resizeMode: 'contain',
}, },
paginationDots: { heroFallback: {
position: 'absolute', width: '100%',
bottom: 20, height: '100%',
alignSelf: 'center',
flexDirection: 'row',
backgroundColor: 'rgba(255,255,255,0.7)',
paddingHorizontal: 8,
paddingVertical: 4,
borderRadius: 12,
},
dot: {
width: 6,
height: 6,
borderRadius: 3,
backgroundColor: colors.border,
marginHorizontal: 3,
},
dotActive: {
width: 14,
backgroundColor: colors.primary,
},
fallbackIconWrap: {
flex: 1,
alignItems: 'center', alignItems: 'center',
justifyContent: 'center', justifyContent: 'center',
backgroundColor: colors.surface, backgroundColor: colors.inputBg,
}, },
fallbackIcon: { heroFallbackEmoji: {
fontSize: 60, 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) // Floating top icon row (back / share / favorite)
@ -68,42 +55,40 @@ export const getStyles = (colors: any) =>
flexDirection: 'row', flexDirection: 'row',
justifyContent: 'space-between', justifyContent: 'space-between',
alignItems: 'center', alignItems: 'center',
zIndex: 10,
}, },
iconButton: { iconButton: {
width: 42, width: 38,
height: 42, height: 38,
borderRadius: 21, borderRadius: 19,
backgroundColor: 'rgba(255,255,255,0.95)', backgroundColor: 'rgba(255,255,255,0.92)',
alignItems: 'center', alignItems: 'center',
justifyContent: 'center', justifyContent: 'center',
...Platform.select({ ...Platform.select({
ios: { ios: {
shadowColor: '#000', shadowColor: '#000',
shadowOffset: { width: 0, height: 3 }, shadowOffset: { width: 0, height: 2 },
shadowOpacity: 0.15, shadowOpacity: 0.15,
shadowRadius: 5, shadowRadius: 4,
}, },
android: { elevation: 4 }, android: { elevation: 3 },
}), }),
}, },
iconButtonText: { iconButtonText: {
fontSize: 18, fontSize: 16,
color: colors.text,
}, },
iconButtonGroup: { iconButtonGroup: {
flexDirection: 'row', flexDirection: 'row',
}, },
// ---------- Product Info Box ---------- // ---------- Info card ----------
infoBox: { infoCard: {
backgroundColor: colors.background, backgroundColor: colors.background,
marginTop: -20, marginTop: -20,
borderTopLeftRadius: 24, borderTopLeftRadius: 24,
borderTopRightRadius: 24, borderTopRightRadius: 24,
paddingTop: 24, paddingTop: 20,
paddingHorizontal: 20, paddingHorizontal: 20,
paddingBottom: 20, paddingBottom: 16,
...Platform.select({ ...Platform.select({
ios: { ios: {
shadowColor: '#000', shadowColor: '#000',
@ -114,389 +99,247 @@ export const getStyles = (colors: any) =>
android: { elevation: 4 }, android: { elevation: 4 },
}), }),
}, },
brandRow: { infoTopRow: {
flexDirection: 'row', flexDirection: 'row',
justifyContent: 'space-between', justifyContent: 'space-between',
alignItems: 'center', alignItems: 'flex-start',
marginBottom: 6,
}, },
brandText: { heroName: {
fontSize: typography.fontSize.xs, fontSize: typography.fontSize.xl,
fontWeight: typography.fontWeight.bold, fontWeight: typography.fontWeight.bold,
color: colors.textSecondary, color: colors.text,
textTransform: 'uppercase', flex: 1,
letterSpacing: 0.5, marginRight: 12,
}, },
ratingBadge: { ratingBadge: {
flexDirection: 'row', flexDirection: 'row',
alignItems: 'center', alignItems: 'center',
backgroundColor: colors.primaryMuted ?? '#E9F7EF', backgroundColor: colors.primary,
borderRadius: 6, borderRadius: 8,
paddingHorizontal: 6, paddingHorizontal: 8,
paddingVertical: 3, paddingVertical: 5,
}, },
ratingBadgeText: { ratingBadgeText: {
fontSize: typography.fontSize.xs, fontSize: typography.fontSize.sm,
fontWeight: typography.fontWeight.bold, fontWeight: typography.fontWeight.bold,
color: colors.primary,
marginLeft: 4,
},
titleText: {
fontSize: 24,
fontWeight: typography.fontWeight.bold,
color: colors.text,
marginBottom: 12,
lineHeight: 32,
},
priceRow: {
flexDirection: 'row',
alignItems: 'flex-end',
marginBottom: 8,
},
priceText: {
fontSize: 28,
fontWeight: typography.fontWeight.bold,
color: colors.text,
marginRight: 10,
},
comparePriceText: {
fontSize: 16,
color: colors.textSecondary,
textDecorationLine: 'line-through',
marginBottom: 4,
},
discountBadgeWrap: {
marginLeft: 10,
marginBottom: 6,
backgroundColor: colors.error ?? '#E53935',
paddingHorizontal: 8,
paddingVertical: 3,
borderRadius: 6,
},
discountBadgeText: {
color: '#FFFFFF', color: '#FFFFFF',
fontSize: typography.fontSize.xs, marginLeft: 3,
fontWeight: typography.fontWeight.bold,
}, },
taxText: { cuisineText: {
fontSize: typography.fontSize.sm, fontSize: typography.fontSize.sm,
color: colors.textSecondary, color: colors.textSecondary,
marginTop: 4,
}, },
// ---------- Section Divider ---------- metaRow: {
sectionDivider: {
height: 8,
backgroundColor: colors.surface ?? '#F5F6F8',
},
// ---------- Details Section ----------
detailsSection: {
padding: 20,
backgroundColor: colors.background,
},
sectionTitle: {
fontSize: 18,
fontWeight: typography.fontWeight.bold,
color: colors.text,
marginBottom: 12,
},
descriptionText: {
fontSize: 15,
lineHeight: 24,
color: colors.textSecondary,
marginBottom: 20,
},
metaGrid: {
flexDirection: 'row', flexDirection: 'row',
flexWrap: 'wrap', alignItems: 'center',
backgroundColor: colors.surface ?? '#F5F6F8', marginTop: 12,
borderRadius: 12,
padding: 16,
}, },
metaItem: { metaItem: {
width: '50%',
marginBottom: 12,
},
metaLabel: {
fontSize: 12,
color: colors.textSecondary,
marginBottom: 2,
},
metaValue: {
fontSize: 14,
fontWeight: typography.fontWeight.semibold,
color: colors.text,
},
// ---------- Sticky Bottom Bar ----------
bottomBarWrap: {
position: 'absolute',
left: 0,
right: 0,
bottom: 0,
backgroundColor: colors.background,
paddingHorizontal: 20,
paddingTop: 16,
paddingBottom: Platform.OS === 'ios' ? 34 : 20,
borderTopWidth: 1,
borderTopColor: colors.border ?? '#ECECEC',
flexDirection: 'row', flexDirection: 'row',
alignItems: 'center', alignItems: 'center',
}, },
qtySelector: { 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', flexDirection: 'row',
alignItems: 'center', alignItems: 'center',
borderWidth: 1, borderWidth: 1,
borderColor: colors.border, borderStyle: 'dashed',
borderRadius: 12, borderColor: colors.primary,
marginRight: 16, backgroundColor: colors.primaryMuted ?? '#E9F7EF',
borderRadius: 10,
paddingVertical: 8,
paddingHorizontal: 12,
marginRight: 10,
}, },
qtyBtn: { offerIcon: {
width: 44, fontSize: 15,
height: 44, 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', alignItems: 'center',
justifyContent: 'center', justifyContent: 'space-between',
paddingHorizontal: 20,
paddingTop: 18,
paddingBottom: 4,
}, },
qtyBtnText: { menuTitle: {
fontSize: 20, fontSize: typography.fontSize.lg,
color: colors.primary,
fontWeight: typography.fontWeight.medium,
},
qtyValue: {
fontSize: 16,
fontWeight: typography.fontWeight.bold, fontWeight: typography.fontWeight.bold,
color: colors.text, color: colors.text,
width: 30,
textAlign: 'center',
}, },
addBtn: { menuCount: {
flex: 1, fontSize: typography.fontSize.sm,
backgroundColor: colors.primary, color: colors.textSecondary,
height: 48,
borderRadius: 14,
alignItems: 'center',
justifyContent: 'center',
...Platform.select({
ios: {
shadowColor: colors.primary,
shadowOffset: { width: 0, height: 4 },
shadowOpacity: 0.3,
shadowRadius: 8,
},
android: { elevation: 6 },
}),
},
addBtnText: {
fontSize: 16,
fontWeight: typography.fontWeight.bold,
color: '#FFFFFF',
}, },
// Skeleton loaders // ---------- Category tabs ----------
skeletonTitle: { categoryRow: {
width: '80%', paddingVertical: 14,
height: 32,
backgroundColor: colors.surface ?? '#EEEEEE',
borderRadius: 8,
marginBottom: 12,
},
skeletonPrice: {
width: '40%',
height: 28,
backgroundColor: colors.surface ?? '#EEEEEE',
borderRadius: 6,
marginBottom: 10,
},
skeletonText: {
width: '100%',
height: 16,
backgroundColor: colors.surface ?? '#EEEEEE',
borderRadius: 4,
marginBottom: 8,
},
// ================= Ratings & Reviews =================
ratingsSection: {
paddingHorizontal: 20, paddingHorizontal: 20,
paddingTop: 20, flexGrow: 0,
paddingBottom: 24, },
categoryTab: {
paddingHorizontal: 18,
paddingVertical: 9,
borderRadius: 22,
borderWidth: 1.5,
borderColor: colors.border,
marginRight: 10,
backgroundColor: colors.background, backgroundColor: colors.background,
}, },
ratingsHeaderRow: { categoryTabActive: {
flexDirection: 'row', borderColor: colors.primary,
justifyContent: 'space-between', backgroundColor: colors.primary,
alignItems: 'center',
marginBottom: 16,
}, },
seeAllText: { categoryTabText: {
fontSize: typography.fontSize.sm, fontSize: typography.fontSize.sm,
fontWeight: typography.fontWeight.semibold, color: colors.textSecondary,
color: colors.primary, fontWeight: typography.fontWeight.medium,
},
categoryTabTextActive: {
color: '#FFFFFF',
fontWeight: typography.fontWeight.bold,
}, },
// Summary card (big number + stars + optional breakdown bars) // ---------- Menu list ----------
ratingsSummaryCard: { menuList: {
paddingHorizontal: 20,
},
emptyMenuWrap: {
alignItems: 'center',
paddingVertical: 40,
},
emptyMenuEmoji: {
fontSize: 34,
marginBottom: 8,
},
emptyMenuText: {
color: colors.textSecondary,
fontSize: typography.fontSize.sm,
},
// ---------- Cart strip ----------
cartStripWrap: {
position: 'absolute',
left: 16,
right: 16,
bottom: Platform.OS === 'ios' ? 28 : 16,
},
cartStrip: {
flexDirection: 'row', flexDirection: 'row',
backgroundColor: colors.surface ?? '#F5F6F8', alignItems: 'center',
justifyContent: 'space-between',
backgroundColor: colors.primary,
paddingHorizontal: 18,
paddingVertical: 14,
borderRadius: 16, borderRadius: 16,
padding: 18,
marginBottom: 16,
...Platform.select({ ...Platform.select({
ios: { ios: {
shadowColor: '#000', shadowColor: '#000',
shadowOffset: { width: 0, height: 2 }, shadowOffset: { width: 0, height: 6 },
shadowOpacity: 0.04, shadowOpacity: 0.25,
shadowRadius: 6, shadowRadius: 12,
}, },
android: { elevation: 1 }, android: { elevation: 8 },
}), }),
}, },
ratingsSummaryLeft: { cartStripLeft: {
alignItems: 'center',
justifyContent: 'center',
minWidth: 76,
},
ratingsSummaryLeftWithDivider: {
marginRight: 20,
paddingRight: 20,
borderRightWidth: StyleSheet.hairlineWidth,
borderRightColor: colors.border ?? '#E0E0E0',
},
avgRatingNumber: {
fontSize: 34,
fontWeight: typography.fontWeight.bold,
color: colors.text,
marginBottom: 4,
},
starsRow: {
flexDirection: 'row',
marginBottom: 6,
},
starIcon: {
marginRight: 1,
},
totalRatingsText: {
fontSize: typography.fontSize.xs,
color: colors.textSecondary,
},
ratingsBarsWrap: {
flex: 1,
justifyContent: 'center',
},
barRow: {
flexDirection: 'row', flexDirection: 'row',
alignItems: 'center', alignItems: 'center',
marginBottom: 6,
}, },
barLabel: { cartBagIcon: {
width: 26, fontSize: 18,
fontSize: typography.fontSize.xs,
color: colors.textSecondary,
textAlign: 'right',
marginRight: 8, marginRight: 8,
}, },
barTrack: { cartStripTextWrap: {},
flex: 1, cartStripCount: {
height: 6, fontSize: typography.fontSize.xs,
borderRadius: 3, color: 'rgba(255,255,255,0.85)',
backgroundColor: colors.border ?? '#E5E5EA',
overflow: 'hidden',
}, },
barFill: { cartStripPrice: {
height: '100%', fontSize: typography.fontSize.md,
borderRadius: 3, fontWeight: typography.fontWeight.bold,
backgroundColor: colors.warning ?? '#F5A623', color: '#FFFFFF',
}, },
viewCartButton: {
// Empty state — styled as a card with a CTA, not bare text
emptyRatingsCard: {
backgroundColor: colors.surface ?? '#F5F6F8',
borderRadius: 16,
paddingVertical: 28,
paddingHorizontal: 20,
alignItems: 'center',
},
emptyRatingsIcon: {
fontSize: 30,
marginBottom: 10,
},
emptyRatingsTitle: {
fontSize: 15,
fontWeight: typography.fontWeight.semibold,
color: colors.text,
marginBottom: 4,
},
emptyRatingsSubtitle: {
fontSize: 13,
lineHeight: 18,
color: colors.textSecondary,
textAlign: 'center',
marginBottom: 16,
},
rateProductBtn: {
borderWidth: 1.5,
borderColor: colors.primary,
borderRadius: 10,
paddingVertical: 10,
paddingHorizontal: 24,
},
rateProductBtnText: {
fontSize: 14,
fontWeight: typography.fontWeight.semibold,
color: colors.primary,
},
// Recent reviews list
reviewsList: {
marginTop: 4,
},
reviewCard: {
backgroundColor: colors.background,
borderWidth: 1,
borderColor: colors.border ?? '#ECECEC',
borderRadius: 14,
padding: 14,
marginBottom: 12,
},
reviewCardHeader: {
flexDirection: 'row', flexDirection: 'row',
alignItems: 'center', alignItems: 'center',
marginBottom: 10, backgroundColor: '#FFFFFF',
paddingHorizontal: 16,
paddingVertical: 9,
borderRadius: 10,
}, },
reviewerAvatar: { viewCartText: {
width: 36, fontSize: typography.fontSize.sm,
height: 36,
borderRadius: 18,
backgroundColor: colors.primaryMuted ?? '#E9F7EF',
alignItems: 'center',
justifyContent: 'center',
marginRight: 10,
},
reviewerAvatarText: {
color: colors.primary,
fontSize: 15,
fontWeight: typography.fontWeight.bold, fontWeight: typography.fontWeight.bold,
color: colors.primary,
marginRight: 4,
}, },
reviewerName: { viewCartArrow: {
fontSize: 13, fontSize: 13,
fontWeight: typography.fontWeight.semibold, color: colors.primary,
color: colors.text,
marginBottom: 3,
},
reviewDate: {
fontSize: 11,
color: colors.textSecondary,
},
reviewComment: {
fontSize: 13,
lineHeight: 19,
color: colors.textSecondary,
},
skeletonBlock: {
height: 90,
borderRadius: 16,
backgroundColor: colors.surface ?? '#EEEEEE',
marginBottom: 12,
}, },
}); });

View File

@ -1,27 +1,22 @@
import React, { useEffect, useState, useRef } from 'react'; import React, { useEffect, useMemo, useState } from 'react';
import { import {
View, View,
Text, Text,
TouchableOpacity, TouchableOpacity,
ScrollView, ScrollView,
Image, Image,
Dimensions, ImageBackground,
NativeSyntheticEvent,
NativeScrollEvent,
} from 'react-native'; } from 'react-native';
import { useNavigation, useRoute, RouteProp } from '@react-navigation/native'; import { useNavigation, useRoute, RouteProp } from '@react-navigation/native';
import { StackNavigationProp } from '@react-navigation/stack'; import { StackNavigationProp } from '@react-navigation/stack';
import { getStyles } from './providerDetailsScreen.styles'; import { getStyles } from './providerDetailsScreen.styles';
import { CatalogItemRow } from '@components';
import { useAppTheme } from '@theme'; import { useAppTheme } from '@theme';
import { import { useAppDispatch, useAppSelector } from '../../../store';
addToCartThunk, import { addItem } from '../../../store/commonreducers/cart';
updateCartItemThunk, import { getProviderCatalogApi, getProvidersApi } from '../../../api/deliveryApi';
} from '../../../store/commonreducers/cart'; import { CatalogItem, Provider } from '../../../interfaces';
import { AppStackParamList } from '../../../navigation/appStack'; import { AppStackParamList } from '../../../navigation/appStack';
import { useAppDispatch, useAppSelector } from '@store';
import { getProductDetailsThunk } from './thunk';
import { getDiscountPercentage, formatPrice } from '@components';
import { getFullUrl } from '@utils';
type ProviderDetailsNavProp = StackNavigationProp< type ProviderDetailsNavProp = StackNavigationProp<
AppStackParamList, AppStackParamList,
@ -32,19 +27,215 @@ type ProviderDetailsRouteProp = RouteProp<
'ProviderDetailsScreen' 'ProviderDetailsScreen'
>; >;
const { width } = Dimensions.get('window'); const FALLBACK_CATEGORIES = ['Pizza', 'Sides', 'Beverages'];
// TODO: move these into your shared Product type once the backend const OFFERS = [
// response includes them. { key: 'o1', icon: '🏷️', label: '50% OFF up to ₹100' },
interface ProductReview { { key: 'o2', icon: '🚚', label: 'Free delivery above ₹299' },
id: string; { key: 'o3', icon: '🎁', label: 'Buy 1 Get 1 on combos' },
userName?: string; ];
rating: number;
comment?: string; // ---------------------------------------------------------------------------
createdAt?: string; // 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;
} }
type RatingBreakdown = Partial<Record<1 | 2 | 3 | 4 | 5, number>>; 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();
@ -52,314 +243,65 @@ export const ProviderDetailsScreen: React.FC = () => {
const dispatch = useAppDispatch(); const dispatch = useAppDispatch();
const navigation = useNavigation<ProviderDetailsNavProp>(); const navigation = useNavigation<ProviderDetailsNavProp>();
const route = useRoute<ProviderDetailsRouteProp>(); const route = useRoute<ProviderDetailsRouteProp>();
const { providerId, providerName } = route.params;
// Notice we still use providerId param name to avoid breaking routing everywhere
const { providerId } = route.params;
const cartItems = useAppSelector(state => state.cart.items); const cartItems = useAppSelector(state => state.cart.items);
const { product, isLoading } = useAppSelector(state => state.providerDetails);
// Find this product in the cart (if already added) const [provider, setProvider] = useState<Provider | null>(null);
const cartItem = product const [catalog, setCatalog] = useState<CatalogItem[]>([]);
? cartItems.find(i => i.productId === product.id) const [activeCategory, setActiveCategory] = useState(FALLBACK_CATEGORIES[0]);
: undefined;
const isInCart = !!cartItem;
const [activeImageIndex, setActiveImageIndex] = useState(0); const resolvedProviderId = providerId || 'p1';
const [localQty, setLocalQty] = useState(0); const resolvedProviderName = providerName || 'Pizza Planet';
useEffect(() => { useEffect(() => {
if (providerId) { getProviderCatalogApi(resolvedProviderId).then(setCatalog);
dispatch(getProductDetailsThunk(providerId)); }, [resolvedProviderId]);
}
}, [providerId, dispatch]);
// Sync localQty with cart quantity when cart loads or product changes
useEffect(() => { useEffect(() => {
if (cartItem) { getProvidersApi().then(providers => {
setLocalQty(cartItem.quantity); const match = providers.find(p => p.id === resolvedProviderId);
} else { if (match) setProvider(match);
setLocalQty(0); });
} }, [resolvedProviderId]);
}, [cartItem?.quantity, cartItem?.productId]);
const handleScroll = (e: NativeSyntheticEvent<NativeScrollEvent>) => { const categories = useMemo(() => {
const slide = Math.round(e.nativeEvent.contentOffset.x / width); const fromCatalog = Array.from(new Set(catalog.map(item => item.category)));
setActiveImageIndex(slide); 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 match = cartItems.find(i => i.item.id === itemId);
return match ? match.quantity : 0;
}; };
const handleAddToCart = () => { const totalItems = cartItems.reduce((sum, i) => sum + i.quantity, 0);
if (!product) return; const totalPrice = cartItems.reduce(
(sum, i) => sum + i.item.price * i.quantity,
0,
);
const handleAddItem = (item: CatalogItem) => {
dispatch( dispatch(
addToCartThunk({ addItem({
productId: product.id, providerId: resolvedProviderId,
quantity: Math.max(1, localQty), // always add at least 1 providerName: resolvedProviderName,
item,
}), }),
); );
}; };
const handleIncrement = () => {
if (!product) return;
const newQty = localQty + 1;
setLocalQty(newQty);
dispatch(updateCartItemThunk({ productId: product.id, quantity: newQty }));
};
const handleDecrement = () => {
if (!product || localQty <= 1) return;
const newQty = localQty - 1;
setLocalQty(newQty);
dispatch(updateCartItemThunk({ productId: product.id, quantity: newQty }));
};
const handleRateProduct = () => {
navigation.navigate('WriteReviewScreen', { productId: product?.id ?? '' });
};
const renderStars = (value: number, size = 14) => {
const rounded = Math.round(value);
const starColor = (colors as any).warning ?? '#F5A623';
return (
<View style={styles.starsRow}>
{[1, 2, 3, 4, 5].map(i => (
<Text
key={i}
style={[styles.starIcon, { fontSize: size, color: starColor }]}
>
{i <= rounded ? '★' : '☆'}
</Text>
))}
</View>
);
};
const renderImages = () => {
if (isLoading) {
return (
<View style={styles.carouselWrap}>
<View
style={[
styles.fallbackIconWrap,
{ backgroundColor: (colors as any).surface ?? colors.cardBg },
]}
/>
</View>
);
}
const images = product?.media?.length
? product.media
: [
{
id: 'fallback',
url: product?.imageUrl || '',
mediaType: 'IMAGE',
sortOrder: 0,
productId: '',
createdAt: '',
},
];
return (
<View style={styles.carouselWrap}>
<ScrollView
horizontal
pagingEnabled
showsHorizontalScrollIndicator={false}
onMomentumScrollEnd={handleScroll}
>
{images.map(img => (
<Image
key={img.id}
source={{ uri: getFullUrl(img.url) }}
style={styles.carouselImage}
/>
))}
</ScrollView>
{images.length > 1 && (
<View style={styles.paginationDots}>
{images.map((_, i) => (
<View
key={i}
style={[styles.dot, i === activeImageIndex && styles.dotActive]}
/>
))}
</View>
)}
</View>
);
};
const renderProductInfo = () => {
if (isLoading || !product) {
return (
<View style={styles.infoBox}>
<View style={styles.skeletonTitle} />
<View style={styles.skeletonPrice} />
<View style={styles.skeletonText} />
<View style={styles.skeletonText} />
</View>
);
}
const discount = getDiscountPercentage(
product.price,
product.compareAtPrice,
);
const currency = product.currency === 'INR' ? '₹' : product.currency;
return (
<View style={styles.infoBox}>
<View style={styles.brandRow}>
<Text style={styles.brandText}>
{product.brand || product.merchant?.name || 'BRAND'}
</Text>
<View style={styles.ratingBadge}>
<Text style={{ fontSize: 11 }}></Text>
<Text style={styles.ratingBadgeText}>
{parseFloat(product.avgRating).toFixed(1)}
</Text>
</View>
</View>
<Text style={styles.titleText}>{product.name}</Text>
<View style={styles.priceRow}>
<Text style={styles.priceText}>
{formatPrice(product.price, currency)}
</Text>
{product.compareAtPrice &&
parseFloat(product.compareAtPrice) > parseFloat(product.price) && (
<Text style={styles.comparePriceText}>
{formatPrice(product.compareAtPrice, currency)}
</Text>
)}
{discount && (
<View style={styles.discountBadgeWrap}>
<Text style={styles.discountBadgeText}>{discount}</Text>
</View>
)}
</View>
<Text style={styles.taxText}>Inclusive of all taxes</Text>
</View>
);
};
const renderRatingsSection = () => {
if (isLoading) {
return (
<View style={styles.ratingsSection}>
<View style={styles.skeletonBlock} />
<View style={[styles.skeletonBlock, { height: 80 }]} />
</View>
);
}
const avgRating = product?.avgRating ? parseFloat(product.avgRating) : 0;
const reviews: ProductReview[] = (product as any)?.reviews ?? [];
const totalRatings: number =
(product as any)?.ratingsCount ?? reviews.length ?? 0;
const breakdown: RatingBreakdown | undefined = (product as any)
?.ratingBreakdown;
const hasRatings = totalRatings > 0;
return (
<View style={styles.ratingsSection}>
<View style={styles.ratingsHeaderRow}>
<Text style={styles.sectionTitle}>Ratings & Reviews</Text>
{reviews.length > 3 && (
<TouchableOpacity activeOpacity={0.7}>
<Text style={styles.seeAllText}>See all</Text>
</TouchableOpacity>
)}
</View>
{hasRatings ? (
<View style={styles.ratingsSummaryCard}>
<View
style={[
styles.ratingsSummaryLeft,
breakdown && styles.ratingsSummaryLeftWithDivider,
]}
>
<Text style={styles.avgRatingNumber}>{avgRating.toFixed(1)}</Text>
{renderStars(avgRating, 16)}
<Text style={styles.totalRatingsText}>
{totalRatings} {totalRatings === 1 ? 'rating' : 'ratings'}
</Text>
</View>
{breakdown && (
<View style={styles.ratingsBarsWrap}>
{[5, 4, 3, 2, 1].map(star => {
const count = breakdown[star as 1 | 2 | 3 | 4 | 5] ?? 0;
const pct =
totalRatings > 0 ? (count / totalRatings) * 100 : 0;
return (
<View key={star} style={styles.barRow}>
<Text style={styles.barLabel}>{star}</Text>
<View style={styles.barTrack}>
<View style={[styles.barFill, { width: `${pct}%` }]} />
</View>
</View>
);
})}
</View>
)}
</View>
) : (
<View style={styles.emptyRatingsCard}>
<Text style={styles.emptyRatingsIcon}></Text>
<Text style={styles.emptyRatingsTitle}>No ratings yet</Text>
<Text style={styles.emptyRatingsSubtitle}>
Be the first to share what you think of this product.
</Text>
<TouchableOpacity
style={styles.rateProductBtn}
activeOpacity={0.8}
onPress={handleRateProduct}
>
<Text style={styles.rateProductBtnText}>Rate this product</Text>
</TouchableOpacity>
</View>
)}
{reviews.length > 0 && (
<View style={styles.reviewsList}>
{reviews.slice(0, 3).map(review => (
<View key={review.id} style={styles.reviewCard}>
<View style={styles.reviewCardHeader}>
<View style={styles.reviewerAvatar}>
<Text style={styles.reviewerAvatarText}>
{(review.userName || 'U').charAt(0).toUpperCase()}
</Text>
</View>
<View style={{ flex: 1 }}>
<Text style={styles.reviewerName}>
{review.userName || 'Anonymous'}
</Text>
{renderStars(review.rating, 12)}
</View>
<Text style={styles.reviewDate}>
{review.createdAt
? new Date(review.createdAt).toLocaleDateString('en-IN', {
day: 'numeric',
month: 'short',
})
: ''}
</Text>
</View>
{!!review.comment && (
<Text style={styles.reviewComment}>{review.comment}</Text>
)}
</View>
))}
</View>
)}
</View>
);
};
return ( return (
<View style={styles.container}> <View style={styles.container}>
<ScrollView <ScrollView
@ -367,147 +309,69 @@ export const ProviderDetailsScreen: React.FC = () => {
contentContainerStyle={styles.contentBody} contentContainerStyle={styles.contentBody}
showsVerticalScrollIndicator={false} showsVerticalScrollIndicator={false}
> >
<View style={{ position: 'relative' }}> <HeroSection
{renderImages()} styles={styles}
imageUrl={provider?.imageUrl}
onBack={() => navigation.goBack()}
/>
<View style={styles.topIconRow}> <InfoCard
<TouchableOpacity styles={styles}
style={styles.iconButton} name={resolvedProviderName}
activeOpacity={0.8} rating={provider?.rating ?? 4.5}
onPress={() => navigation.goBack()} deliveryTime={provider?.deliveryTime ?? '25 min'}
> tag={provider?.tag ?? 'Italian'}
<Text style={styles.iconButtonText}></Text> />
</TouchableOpacity>
<View style={styles.iconButtonGroup}>
<TouchableOpacity
style={[styles.iconButton, { marginRight: 12 }]}
activeOpacity={0.8}
>
<Text style={styles.iconButtonText}>🔗</Text>
</TouchableOpacity>
<TouchableOpacity style={styles.iconButton} activeOpacity={0.8}>
<Text style={{ fontSize: 17 }}>🤍</Text>
</TouchableOpacity>
</View>
</View>
</View>
{renderProductInfo()} <OffersRow styles={styles} />
<View style={styles.sectionDivider} /> <View style={styles.sectionDivider} />
<View style={styles.detailsSection}> <View style={styles.menuHeaderRow}>
<Text style={styles.sectionTitle}>Product Details</Text> <Text style={styles.menuTitle}>Menu</Text>
<Text style={styles.descriptionText}> <Text style={styles.menuCount}>{catalog.length} items</Text>
{product?.description ||
'No description available for this product.'}
</Text>
<View style={styles.metaGrid}>
<View style={styles.metaItem}>
<Text style={styles.metaLabel}>Category</Text>
<Text style={styles.metaValue}>
{product?.category?.name || 'N/A'}
</Text>
</View>
<View style={styles.metaItem}>
<Text style={styles.metaLabel}>SKU</Text>
<Text style={styles.metaValue}>{product?.sku || 'N/A'}</Text>
</View>
<View style={styles.metaItem}>
<Text style={styles.metaLabel}>Stock</Text>
<Text style={styles.metaValue}>
{product?.stockQuantity ? 'In Stock' : 'Out of Stock'}
</Text>
</View>
<View style={styles.metaItem}>
<Text style={styles.metaLabel}>Merchant</Text>
<Text style={styles.metaValue}>
{product?.merchant?.name || 'N/A'}
</Text>
</View>
</View>
</View> </View>
<View style={styles.sectionDivider} /> <CategoryTabs
styles={styles}
categories={categories}
activeCategory={activeCategory}
onSelect={setActiveCategory}
/>
{renderRatingsSection()} <View style={styles.menuList}>
{filteredItems.length === 0 && (
<View style={styles.emptyMenuWrap}>
<Text style={styles.emptyMenuEmoji}>🍽</Text>
<Text style={styles.emptyMenuText}>
No items in this category yet
</Text>
</View>
)}
{filteredItems.map(item => (
<CatalogItemRow
key={item.id}
imageUrl={item.imageUrl}
title={item.title}
description={item.description}
price={item.price}
quantity={getItemQuantity(item.id)}
onAdd={() => handleAddItem(item)}
onRemove={() => {}}
/>
))}
</View>
</ScrollView> </ScrollView>
{/* Sticky Bottom Bar */} {totalItems > 0 && (
<View style={styles.bottomBarWrap}> <CartStrip
{isInCart ? ( styles={styles}
// ── Already in cart: show inline qty stepper + Go to Cart ── totalItems={totalItems}
<> totalPrice={totalPrice}
<View style={styles.qtySelector}>
<TouchableOpacity
style={styles.qtyBtn}
onPress={handleDecrement}
activeOpacity={0.7}
disabled={localQty <= 1}
>
<Text style={styles.qtyBtnText}>-</Text>
</TouchableOpacity>
<Text style={styles.qtyValue}>{localQty}</Text>
<TouchableOpacity
style={styles.qtyBtn}
onPress={handleIncrement}
activeOpacity={0.7}
>
<Text style={styles.qtyBtnText}>+</Text>
</TouchableOpacity>
</View>
<TouchableOpacity
style={styles.addBtn}
activeOpacity={0.8}
onPress={() => navigation.navigate('CartScreen')} onPress={() => navigation.navigate('CartScreen')}
> />
<Text style={styles.addBtnText}>Go to Cart</Text>
</TouchableOpacity>
</>
) : (
// ── Not in cart: qty picker + Add to Cart ──
<>
<View style={styles.qtySelector}>
<TouchableOpacity
style={styles.qtyBtn}
onPress={() => setLocalQty(q => Math.max(0, q - 1))}
activeOpacity={0.7}
disabled={localQty <= 0}
>
<Text style={styles.qtyBtnText}>-</Text>
</TouchableOpacity>
<Text style={styles.qtyValue}>{localQty}</Text>
<TouchableOpacity
style={styles.qtyBtn}
onPress={() => setLocalQty(q => q + 1)}
activeOpacity={0.7}
>
<Text style={styles.qtyBtnText}>+</Text>
</TouchableOpacity>
</View>
<TouchableOpacity
style={styles.addBtn}
activeOpacity={0.8}
onPress={handleAddToCart}
disabled={
!product ||
(product.isTrackStock && product.stockQuantity === 0)
}
>
<Text style={styles.addBtnText}>
{!product
? 'Loading...'
: product.isTrackStock && product.stockQuantity === 0
? 'Out of Stock'
: 'Add to Cart'}
</Text>
</TouchableOpacity>
</>
)} )}
</View> </View>
</View>
); );
}; };

View File

@ -1,33 +0,0 @@
import { createReducer } from '@reduxjs/toolkit';
import { Product } from '@interfaces';
import { getProductDetailsThunk } from './thunk';
export interface ProviderDetailsState {
product: Product | null;
isLoading: boolean;
error: string | null;
}
const initialState: ProviderDetailsState = {
product: null,
isLoading: false,
error: null,
};
const providerDetailsReducer = createReducer(initialState, builder => {
builder
.addCase(getProductDetailsThunk.pending, state => {
state.isLoading = true;
state.error = null;
})
.addCase(getProductDetailsThunk.fulfilled, (state, action) => {
state.product = action.payload;
state.isLoading = false;
})
.addCase(getProductDetailsThunk.rejected, (state, action) => {
state.isLoading = false;
state.error = action.payload || 'Failed to fetch product details';
});
});
export default providerDetailsReducer;

View File

@ -1,18 +0,0 @@
import { createAsyncThunk } from '@reduxjs/toolkit';
import { getProductDetailsApi } from '@api';
import { Product } from '@interfaces';
export const getProductDetailsThunk = createAsyncThunk<
Product,
string,
{ rejectValue: string }
>('providerDetails/getProductDetails', async (productId, { rejectWithValue }) => {
try {
const response = await getProductDetailsApi(productId);
return response;
} catch (error: any) {
return rejectWithValue(
error.response?.data?.message || 'Failed to fetch product details',
);
}
});

View File

@ -1,2 +1 @@
export * from './setLocationScreen'; export * from './setLocationScreen';
export * from './reducer';

View File

@ -1,43 +0,0 @@
import { createReducer, createAction } from '@reduxjs/toolkit';
// ─── Actions ──────────────────────────────────────────────────────────────────
export const setLocationData = createAction<{
latitude: number;
longitude: number;
mapAddress: string;
}>('setLocation/setLocationData');
export const clearLocationData = createAction('setLocation/clearLocationData');
// ─── State ────────────────────────────────────────────────────────────────────
export interface SetLocationState {
latitude: number | null;
longitude: number | null;
mapAddress: string;
}
const initialState: SetLocationState = {
latitude: null,
longitude: null,
mapAddress: '',
};
// ─── Reducer ──────────────────────────────────────────────────────────────────
const setLocationReducer = createReducer(initialState, builder => {
builder
.addCase(setLocationData, (state, action) => {
state.latitude = action.payload.latitude;
state.longitude = action.payload.longitude;
state.mapAddress = action.payload.mapAddress;
})
.addCase(clearLocationData, state => {
state.latitude = null;
state.longitude = null;
state.mapAddress = '';
});
});
export default setLocationReducer;

View File

@ -20,14 +20,8 @@ import {
showPermissionDeniedAlert, showPermissionDeniedAlert,
LatLng, LatLng,
} from '../../../services/locationServices'; } from '../../../services/locationServices';
import { useAppDispatch } from '@store';
import { setLocationData } from './reducer';
import { OnboardingStackParamList } from '@navigation/onboardingStack';
type NavProp = StackNavigationProp< type NavProp = StackNavigationProp<AuthStackParamList, 'SetLocationScreen'>;
OnboardingStackParamList,
'SetLocationScreen'
>;
const DELTA = { latitudeDelta: 0.006, longitudeDelta: 0.006 }; const DELTA = { latitudeDelta: 0.006, longitudeDelta: 0.006 };
@ -35,7 +29,6 @@ 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 dispatch = useAppDispatch();
const mapRef = useRef<MapView>(null); const mapRef = useRef<MapView>(null);
// Guard: never call setState after the component has unmounted // Guard: never call setState after the component has unmounted
const isMounted = useRef(true); const isMounted = useRef(true);
@ -156,16 +149,7 @@ export const SetLocationScreen: React.FC = () => {
<PrimaryButton <PrimaryButton
title="Confirm Location" title="Confirm Location"
onPress={() => { onPress={() => navigation.navigate('CompleteProfileScreen')}
dispatch(
setLocationData({
latitude: coords.latitude,
longitude: coords.longitude,
mapAddress: address,
}),
);
navigation.navigate('CompleteProfileScreen');
}}
style={styles.confirmButton} style={styles.confirmButton}
/> />

View File

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

View File

@ -1,31 +0,0 @@
import { StyleSheet } from 'react-native';
import { typography } from '@theme';
export const getStyles = (colors: any) => StyleSheet.create({
container: {
flex: 1,
backgroundColor: colors.background,
},
content: {
flex: 1,
justifyContent: 'center',
alignItems: 'center',
paddingHorizontal: 24,
},
icon: {
fontSize: 64,
marginBottom: 16,
},
title: {
fontSize: typography.fontSize.xl,
fontWeight: typography.fontWeight.bold,
color: colors.text,
textAlign: 'center',
marginBottom: 8,
},
subtitle: {
fontSize: typography.fontSize.md,
color: colors.textSecondary,
marginBottom: 32,
},
});

View File

@ -1,43 +0,0 @@
import React, { useState } from 'react';
import { View, Text } from 'react-native';
import { useNavigation } from '@react-navigation/native';
import { StackNavigationProp } from '@react-navigation/stack';
import { getStyles } from './writeReviewScreen.styles';
import { Header, RatingStars, PrimaryButton } from '@components';
import { useAppTheme } from '@theme';
import { AppStackParamList } from '../../../navigation/appStack';
type WriteReviewNavProp = StackNavigationProp<
AppStackParamList,
'WriteReviewScreen'
>;
export const WriteReviewScreen: React.FC = () => {
const { colors } = useAppTheme();
const styles = getStyles(colors);
const navigation = useNavigation<WriteReviewNavProp>();
const [rating, setRating] = useState(0);
return (
<View style={styles.container}>
<Header
title="Order Delivered"
onBack={() => navigation.navigate('MainTabs')}
/>
<View style={styles.content}>
<Text style={styles.icon}>🎉</Text>
<Text style={styles.title}>Your order has been delivered!</Text>
<Text style={styles.subtitle}>How was your experience?</Text>
<RatingStars rating={rating} onRate={setRating} size={40} />
<PrimaryButton
title="Rate & Tip"
onPress={() => navigation.navigate('MainTabs')}
disabled={rating === 0}
style={{ marginTop: 32 }}
/>
</View>
</View>
);
};

View File

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

View File

@ -2,7 +2,7 @@ import { useState, useCallback } from 'react';
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 { AuthStackParamList } from 'app/navigation/authStack'; import { AuthStackParamList } from 'app/navigation/authStack';
import { loginWithPhone, useAppDispatch } from '@store'; import { loginWithPhone, useAppDispatch } from '../store';
type LoginNavProp = StackNavigationProp<AuthStackParamList, 'LoginScreen'>; type LoginNavProp = StackNavigationProp<AuthStackParamList, 'LoginScreen'>;

View File

@ -1,61 +0,0 @@
export interface LoginResponse {
message: string;
phone: string;
code: string;
}
export interface VerifyOtpResponse {
message: string;
isNewUser: boolean;
accessToken: string;
refreshToken: string;
user: User;
}
export interface User {
id: string;
roleId: string;
roleEnum: RoleEnum;
name: string | null;
email: string | null;
phone: string;
profileImage: string | null;
status: UserStatus;
mfaEnabled: boolean;
createdAt: string;
updatedAt: string;
deletedAt: string | null;
isDeleted: boolean;
lastLogoutAt: string | null;
loginAt: string;
refreshTokenId: string;
refreshTokenHash: string;
refreshTokenExpiresAt: string;
categoryPreferences: CategoryPreference[];
gender: Gender | null;
dateOfBirth: string | null;
role: Role;
// merchants: Merchant[];
}
export interface Role {
id: string;
code: string;
scope: string;
isSystem: boolean;
}
export interface CategoryPreference {
// Extend when backend starts returning data
}
export interface Merchant {
name: string;
imageUrl: string | null;
}
export type RoleEnum = 'CUSTOMER';
export type UserStatus = 'ONBOARDING' | 'ACTIVE' | 'INACTIVE' | 'BLOCKED';
export type Gender = 'MALE' | 'FEMALE' | 'OTHER';

View File

@ -1,51 +0,0 @@
export interface AddToCartRequest {
productId: string;
quantity: number;
attributes?: CartItemAttributes;
}
export interface CartItemAttributes {
size?: string;
extraToppings?: string[];
}
export interface CartProduct {
id: string;
name: string;
price: number;
compareAtPrice: number;
imageUrl: string;
stockQuantity: number;
isTrackStock: boolean;
merchantId: string;
merchantName: string;
}
export interface CartItem {
id: string;
productId: string;
quantity: number;
attributes: Record<string, any>;
createdAt: string;
updatedAt: string;
product: CartProduct;
}
export interface MerchantCartGroup {
merchantId: string;
merchantName: string;
items: CartItem[];
subtotal: number;
}
export interface CartResponse {
[x: string]: any;
id: string;
customerId: string;
items: CartItem[];
groupedByMerchant: MerchantCartGroup[];
subtotal: number;
totalItems: number;
createdAt: string;
updatedAt: string;
}

View File

@ -1,49 +0,0 @@
import { Gender, UserStatus } from './auth';
export interface CustomerResponse {
id: string;
userId: string;
customerCode: string;
createdAt: string;
deletedAt: string | null;
user: CustomerUser;
addresses: CustomerAddress[];
}
export interface CustomerUser {
id: string;
name: string;
email: string;
phone: string;
profileImage: string | null;
status: UserStatus;
gender: Gender;
dateOfBirth: string | null;
categoryPreferences: string[];
}
export interface CustomerAddress {
id: string;
customerId: string;
merchantId: string | null;
deliveryPartnerId: string | null;
latitude: string;
longitude: string;
mapAddress: string;
houseNumber: string;
landmark: string;
addressLine1: string;
city: string;
state: string;
postalCode: string;
phone: string;
label: AddressLabel;
isDefault: boolean;
createdAt: string;
updatedAt: string;
deletedAt: string | null;
}
// export type UserStatus = 'ACTIVE' | 'INACTIVE' | 'SUSPENDED';
export type AddressLabel = 'Home' | 'Work' | 'Other';

View File

@ -43,37 +43,32 @@ export interface CartItem {
quantity: number; quantity: number;
} }
// export type OrderStatus = export type OrderStatus = 'placed' | 'confirmed' | 'dispatched' | 'delivered' | 'cancelled';
// | 'placed'
// | 'confirmed'
// | 'dispatched'
// | 'delivered'
// | 'cancelled';
// export interface Order { export interface Order {
// id: string; id: string;
// providerId: string; providerId: string;
// providerName: string; providerName: string;
// providerImage: string; providerImage: string;
// items: CartItem[]; items: CartItem[];
// status: OrderStatus; status: OrderStatus;
// total: number; total: number;
// deliveryFee: number; deliveryFee: number;
// platformFee: number; platformFee: number;
// discount: number; discount: number;
// couponCode?: string; couponCode?: string;
// createdAt: string; createdAt: string;
// estimatedDelivery: string; estimatedDelivery: string;
// deliveryAddress: Location; deliveryAddress: Location;
// } }
// export interface TrackingStep { export interface TrackingStep {
// key: OrderStatus; key: OrderStatus;
// label: string; label: string;
// timestamp?: string; timestamp?: string;
// isCompleted: boolean; isCompleted: boolean;
// isActive: boolean; isActive: boolean;
// } }
export interface DeliveryAgent { export interface DeliveryAgent {
name: string; name: string;
@ -88,11 +83,3 @@ export interface PaymentMethod {
label: string; label: string;
icon?: string; icon?: string;
} }
export * from './auth';
export * from './onboard';
export * from './product';
export * from './cart';
export * from './paymentMethods';
export * from './customerProfile';
export * from './order';

View File

@ -1,55 +0,0 @@
import { Role } from './auth';
export interface Category {
id: string;
parentId: string | null;
name: string;
slug: string;
imageUrl: string | null;
isActive: boolean;
children: Category[];
}
export interface onBoardPayload {
name: string;
email: string;
latitude: number;
longitude: number;
addressLabel: string;
addressLine1: string;
mapAddress: string;
houseNumber: string;
landmark: string;
city: string;
state: string;
postalCode: string;
addressPhone: string;
categoryPreferences: string[];
gender: string;
}
export interface UserProfile {
id: string;
roleId: string;
roleEnum: 'CUSTOMER' | 'MERCHANT' | 'BUSINESS_ADMIN' | 'SUPER_ADMIN';
name: string;
email: string;
phone: string;
profileImage: string | null;
status: 'ACTIVE' | 'INACTIVE' | 'SUSPENDED';
mfaEnabled: boolean;
createdAt: string;
updatedAt: string;
deletedAt: string | null;
isDeleted: boolean;
lastLogoutAt: string | null;
loginAt: string;
refreshTokenId: string;
refreshTokenHash: string;
refreshTokenExpiresAt: string;
categoryPreferences: string[];
gender: 'MALE' | 'FEMALE' | 'OTHER';
dateOfBirth: string;
role: Role;
}

View File

@ -1,187 +0,0 @@
import { Merchant } from './auth';
export interface OrderRequest {
addressId: string;
paymentMethodId: string;
paymentMethod: string;
orderType: string;
idempotencyKey: string;
}
export interface PlaceOrderResponse {
success: boolean;
message: string;
orders: Order[];
}
export interface Order {
id: string;
orderNumber: string;
customerId: string;
merchantId: string;
couponCode: string | null;
appliedPromotionId: string | null;
status: OrderStatus;
orderType: OrderType;
paymentMethodId: string;
paymentMethod: PaymentMethod;
currency: string;
subtotal: string;
deliveryFee: string;
platformFee: string;
serviceFee: string;
taxAmount: string;
discountAmount: string;
totalAmount: string;
pickupAddress: PickupAddress;
dropAddress: DropAddress;
metadata: OrderMetadata;
version: number;
createdAt: string;
updatedAt: string;
deletedAt: string | null;
orderItems?: OrderItem[];
payments?: Payment[];
delivery?: Delivery;
merchant?: Merchant;
customer?: OrderCustomer;
tracking?: TrackingEntry[];
}
// Add these new interfaces
export interface OrderCustomerUser {
name: string;
phone: string;
}
export interface OrderCustomer {
id: string;
userId: string;
customerCode: string;
createdAt: string;
deletedAt: string | null;
user: OrderCustomerUser;
}
export interface TrackingEntry {
status: OrderStatus;
time: string;
}
export interface PickupAddress {
city: string;
label: string | null;
phone: string;
state: string;
landmark: string;
postalCode: string;
houseNumber: string;
addressLine1: string;
}
export interface DropAddress {
city: string;
label: string;
phone: string;
state: string;
landmark: string;
latitude: number;
longitude: number;
postalCode: string;
houseNumber: string;
addressLine1: string;
}
export interface OrderMetadata {
idempotencyKey: string;
trackingHistory: TrackingHistory;
}
export interface TrackingHistory {
PLACED: string;
CONFIRMED: string;
// Future statuses can be added here:
// PREPARING?: string;
// OUT_FOR_DELIVERY?: string;
// DELIVERED?: string;
// CANCELLED?: string;
}
export interface OrderItem {
id: string;
orderId: string;
productId: string;
itemType: 'PRODUCT' | 'SERVICE';
name: string;
quantity: string;
unitPrice: string;
totalAmount: string;
attributes: Record<string, any>;
}
export interface Payment {
id: string;
orderId: string;
paymentReference: string;
paymentMethodId: string;
method: PaymentMethod;
provider: PaymentMethod;
providerPaymentId: string | null;
status: PaymentStatus;
amount: string;
currency: string;
idempotencyKey: string;
metadata: Record<string, any>;
createdAt: string;
updatedAt: string;
}
export interface Delivery {
id: string;
orderId: string;
deliveryPartnerId: string | null;
deliveryNumber: string;
status: DeliveryStatus;
estimatedDistanceKm: number | null;
actualDistanceKm: number | null;
proofOfPickupKey: string | null;
proofOfDeliveryKey: string | null;
pickedUpAt: string | null;
deliveredAt: string | null;
}
// export interface Merchant {
// name: string;
// imageUrl: string | null;
// }
export type PaymentStatus =
| 'PENDING'
| 'PROCESSING'
| 'COMPLETED'
| 'FAILED'
| 'REFUNDED';
export type DeliveryStatus =
| 'PENDING_ASSIGNMENT'
| 'ASSIGNED'
| 'PICKED_UP'
| 'IN_TRANSIT'
| 'DELIVERED'
| 'CANCELLED';
export type OrderStatus =
| 'PENDING'
| 'CONFIRMED'
| 'PREPARING'
| 'READY_FOR_PICKUP'
| 'OUT_FOR_DELIVERY'
| 'DELIVERED'
| 'CANCELLED';
export type OrderType = 'DELIVERY' | 'PICKUP';
export type PaymentMethod = 'CARD' | 'CASH' | 'UPI' | 'WALLET' | 'NET_BANKING';

View File

@ -1,12 +0,0 @@
export interface PaymentMethod {
id: string;
code: 'CARD' | 'COD' | 'UPI' | 'WALLET';
displayName: string;
iconUrl: string;
isActive: boolean;
createdAt: string;
updatedAt: string;
deletedAt: string | null;
}
export type PaymentMethodsResponse = PaymentMethod[];

View File

@ -1,143 +0,0 @@
export interface ProductMedia {
id: string;
productId: string;
url: string;
mediaType: 'IMAGE' | 'VIDEO';
sortOrder: number;
createdAt: string;
}
export interface ProductCategory {
name: string;
}
export interface ProductMerchant {
name: string;
}
export interface ProductAttributes {
featured?: boolean;
[key: string]: any;
}
export interface ProductDimensions {
[key: string]: any;
}
export interface Products {
id: string;
merchantId: string;
categoryId: string;
sku: string;
barcode: string | null;
name: string;
brand: string | null;
description: string;
imageUrl: string;
productType: string;
price: string;
compareAtPrice: string;
currency: string;
weightKg: string | null;
weightGm: string | null;
dimensions: ProductDimensions;
attributes: ProductAttributes;
stockQuantity: number;
isTrackStock: boolean;
isActive: boolean;
createdAt: string;
deletedAt: string | null;
category: ProductCategory;
merchant: ProductMerchant;
media: ProductMedia[];
avgRating: string;
totalRatings: number;
}
export interface PaginationMeta {
total: number;
page: number;
limit: number;
totalPages: number;
}
export interface Product {
id: string;
merchantId: string;
categoryId: string;
sku: string;
barcode: string | null;
name: string;
brand: string | null;
description: string;
imageUrl: string;
productType: ProductType;
price: string;
compareAtPrice: string;
currency: string;
weightKg: string | null;
weightGm: string | null;
dimensions: ProductDimensions;
attributes: ProductAttributes;
stockQuantity: number;
isTrackStock: boolean;
isActive: boolean;
createdAt: string;
deletedAt: string | null;
category: ProductCategory;
merchant: ProductMerchant;
media: ProductMedia[];
avgRating: string;
totalRatings: number;
recentRatings: ProductRating[];
}
export interface ProductCategory {
id: string;
name: string;
slug: string;
}
export interface ProductMerchant {
id: string;
name: string;
merchantCode: string;
rating: string;
}
// export interface ProductMedia {
// id: string;
// productId: string;
// url: string;
// mediaType: MediaType;
// sortOrder: number;
// createdAt: string;
// }
// export interface ProductAttributes {
// featured: boolean;
// }
// export interface ProductDimensions {
// [key: string]: unknown;
// }
export interface ProductRating {
id: string;
userId: string;
rating: number;
review: string;
createdAt: string;
}
export enum ProductType {
FOOD = 'FOOD',
GROCERIES = 'GROCERIES',
PHARMACY = 'PHARMACY',
ELECTRONICS = 'ELECTRONICS',
}
export enum MediaType {
IMAGE = 'IMAGE',
VIDEO = 'VIDEO',
}

View File

@ -13,8 +13,6 @@ import {
LiveTrackingScreen, LiveTrackingScreen,
OrderDeliveredScreen, OrderDeliveredScreen,
HelpSupportScreen, HelpSupportScreen,
WriteReviewScreen,
OrderDetailsScreen,
} from '@features/screens'; } from '@features/screens';
export type AppStackParamList = { export type AppStackParamList = {
@ -23,14 +21,12 @@ export type AppStackParamList = {
ProviderDetailsScreen: { providerId: string; providerName?: string }; ProviderDetailsScreen: { providerId: string; providerName?: string };
CartScreen: undefined; CartScreen: undefined;
CheckoutAddressScreen: undefined; CheckoutAddressScreen: undefined;
CheckoutPaymentScreen: { selectedAddressId?: string } | undefined; CheckoutPaymentScreen: undefined;
OrderConfirmedScreen: { orderId?: string } | undefined; OrderConfirmedScreen: { orderId?: string } | undefined;
OrderTrackingScreen: { orderId?: string } | undefined; OrderTrackingScreen: { orderId?: string } | undefined;
LiveTrackingScreen: { orderId?: string } | undefined; LiveTrackingScreen: { orderId?: string } | undefined;
OrderDeliveredScreen: { orderId?: string } | undefined; OrderDeliveredScreen: { orderId?: string } | undefined;
HelpSupportScreen: undefined; HelpSupportScreen: undefined;
WriteReviewScreen: { productId: string } | undefined;
OrderDetailsScreen: { orderId: string } | undefined;
}; };
const Stack = createStackNavigator<AppStackParamList>(); const Stack = createStackNavigator<AppStackParamList>();
@ -40,38 +36,15 @@ export const AppStack: React.FC = () => {
<Stack.Navigator screenOptions={{ headerShown: false }}> <Stack.Navigator screenOptions={{ headerShown: false }}>
<Stack.Screen name="MainTabs" component={MainTabNavigator} /> <Stack.Screen name="MainTabs" component={MainTabNavigator} />
<Stack.Screen name="ProviderListScreen" component={ProviderListScreen} /> <Stack.Screen name="ProviderListScreen" component={ProviderListScreen} />
<Stack.Screen <Stack.Screen name="ProviderDetailsScreen" component={ProviderDetailsScreen} />
name="ProviderDetailsScreen"
component={ProviderDetailsScreen}
/>
<Stack.Screen name="CartScreen" component={CartScreen} /> <Stack.Screen name="CartScreen" component={CartScreen} />
<Stack.Screen <Stack.Screen name="CheckoutAddressScreen" component={CheckoutAddressScreen} />
name="CheckoutAddressScreen" <Stack.Screen name="CheckoutPaymentScreen" component={CheckoutPaymentScreen} />
component={CheckoutAddressScreen} <Stack.Screen name="OrderConfirmedScreen" component={OrderConfirmedScreen} />
/> <Stack.Screen name="OrderTrackingScreen" component={OrderTrackingScreen} />
<Stack.Screen
name="CheckoutPaymentScreen"
component={CheckoutPaymentScreen}
/>
<Stack.Screen
name="OrderConfirmedScreen"
component={OrderConfirmedScreen}
/>
<Stack.Screen
name="OrderTrackingScreen"
component={OrderTrackingScreen}
/>
<Stack.Screen name="LiveTrackingScreen" component={LiveTrackingScreen} /> <Stack.Screen name="LiveTrackingScreen" component={LiveTrackingScreen} />
<Stack.Screen <Stack.Screen name="OrderDeliveredScreen" component={OrderDeliveredScreen} />
name="OrderDeliveredScreen"
component={OrderDeliveredScreen}
/>
<Stack.Screen name="HelpSupportScreen" component={HelpSupportScreen} /> <Stack.Screen name="HelpSupportScreen" component={HelpSupportScreen} />
<Stack.Screen name="WriteReviewScreen" component={WriteReviewScreen} />
<Stack.Screen
name="OrderDetailsScreen"
component={OrderDetailsScreen}
/>
</Stack.Navigator> </Stack.Navigator>
); );
}; };

View File

@ -12,10 +12,10 @@ import {
export type AuthStackParamList = { export type AuthStackParamList = {
LoginScreen: undefined; LoginScreen: undefined;
OtpScreen: { mobileNumber: string }; OtpScreen: { mobileNumber: string };
// SetLocationScreen: undefined; SetLocationScreen: undefined;
// CompleteProfileScreen: undefined; CompleteProfileScreen: undefined;
// PreferencesScreen: undefined; PreferencesScreen: undefined;
// OnboardingCompleteScreen: undefined; OnboardingCompleteScreen: undefined;
}; };
const Stack = createStackNavigator<AuthStackParamList>(); const Stack = createStackNavigator<AuthStackParamList>();
@ -25,6 +25,10 @@ export const AuthStack: React.FC = () => {
<Stack.Navigator screenOptions={{ headerShown: false }}> <Stack.Navigator screenOptions={{ headerShown: false }}>
<Stack.Screen name="LoginScreen" component={LoginScreen} /> <Stack.Screen name="LoginScreen" component={LoginScreen} />
<Stack.Screen name="OtpScreen" component={OtpScreen} /> <Stack.Screen name="OtpScreen" component={OtpScreen} />
<Stack.Screen name="SetLocationScreen" component={SetLocationScreen} />
<Stack.Screen name="CompleteProfileScreen" component={CompleteProfileScreen} />
<Stack.Screen name="PreferencesScreen" component={PreferencesScreen} />
<Stack.Screen name="OnboardingCompleteScreen" component={OnboardingCompleteScreen} />
</Stack.Navigator> </Stack.Navigator>
); );
}; };

View File

@ -1,33 +0,0 @@
import {
CompleteProfileScreen,
OnboardingCompleteScreen,
PreferencesScreen,
SetLocationScreen,
} from '@features/screens';
import { createStackNavigator } from '@react-navigation/stack';
export type OnboardingStackParamList = {
SetLocationScreen: undefined;
CompleteProfileScreen: undefined;
PreferencesScreen: undefined;
OnboardingCompleteScreen: undefined;
};
const Stack = createStackNavigator<OnboardingStackParamList>();
export const OnboardingStack: React.FC = () => {
return (
<Stack.Navigator screenOptions={{ headerShown: false }}>
<Stack.Screen name="SetLocationScreen" component={SetLocationScreen} />
<Stack.Screen
name="CompleteProfileScreen"
component={CompleteProfileScreen}
/>
<Stack.Screen name="PreferencesScreen" component={PreferencesScreen} />
<Stack.Screen
name="OnboardingCompleteScreen"
component={OnboardingCompleteScreen}
/>
</Stack.Navigator>
);
};

View File

@ -1,23 +1,17 @@
import React from 'react'; import React from 'react';
import { useSelector } from 'react-redux'; import { useSelector } from 'react-redux';
import { RootState, useAppSelector } from '../store'; import { RootState } from '../store';
import { AuthStack } from './authStack'; import { AuthStack } from './authStack';
import { AppStack } from './appStack'; import { AppStack } from './appStack';
import { OnboardingStack } from './onboardingStack';
export const RootNavigator: React.FC = () => { export const RootNavigator: React.FC = () => {
const accessToken = useSelector((state: RootState) => state.auth.accessToken); const { isAuthenticated, onboardingComplete } = useSelector(
const isOnboarding = useAppSelector( (state: RootState) => state.auth,
state => state.auth.user?.status === 'ONBOARDING',
); );
if (!accessToken) { if (!isAuthenticated || !onboardingComplete) {
return <AuthStack />; return <AuthStack />;
} }
if (isOnboarding) {
return <OnboardingStack />;
}
return <AppStack />; return <AppStack />;
}; };

View File

@ -15,7 +15,7 @@ const STORAGE_KEYS = {
} as const; } as const;
// ─── Config ────────────────────────────────────────────────────────────────── // ─── Config ──────────────────────────────────────────────────────────────────
const BASE_URL = 'https://db1d-202-8-116-13.ngrok-free.app'; // TODO: replace with your actual base URL const BASE_URL = 'https://api.example.com/v1'; // TODO: replace with your actual base URL
// ─── Token Helpers ─────────────────────────────────────────────────────────── // ─── Token Helpers ───────────────────────────────────────────────────────────
export const tokenManager = { export const tokenManager = {
@ -53,8 +53,8 @@ axiosInstance.interceptors.request.use(
async (config: InternalAxiosRequestConfig) => { async (config: InternalAxiosRequestConfig) => {
// Skip attaching token for auth endpoints (login, register, refresh) // Skip attaching token for auth endpoints (login, register, refresh)
const publicPaths = [ const publicPaths = [
'/auth/otp/request', '/auth/login',
'/auth/otp/verify', '/auth/register',
'/auth/refresh-token', '/auth/refresh-token',
]; ];
const isPublic = publicPaths.some(path => config.url?.includes(path)); const isPublic = publicPaths.some(path => config.url?.includes(path));
@ -68,7 +68,7 @@ axiosInstance.interceptors.request.use(
// Debug logging (remove in production) // Debug logging (remove in production)
if (__DEV__) { if (__DEV__) {
// console.log(`🚀 [API] ${config.method?.toUpperCase()} ${config.url}`);s console.log(`🚀 [API] ${config.method?.toUpperCase()} ${config.url}`);
} }
return config; return config;

View File

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

View File

@ -1,106 +1,129 @@
import { createAction, createReducer } from '@reduxjs/toolkit'; import { createAction, createReducer } from '@reduxjs/toolkit';
import { VerifyOtpResponse } from '../../../interfaces'; import { User, Location } from '../../../interfaces';
import { loginWithPhone, verifyOtp, logoutUser, updateProfile } from './thunk'; import { loginWithPhone, verifyOtp, updateProfile, logoutUser } from './thunk';
import { LoginResponse, User } from '@interfaces/auth';
// ─── Sync Actions ─────────────────────────────────────────────────────────────
export const completeOnboarding = createAction('auth/completeOnboarding');
// ─── State ─────────────────────────────────────────────────────────────────── // ─── State ───────────────────────────────────────────────────────────────────
export interface AuthState { export interface AuthState {
loginData: LoginResponse | null;
user: User | null; user: User | null;
accessToken: string | null; isAuthenticated: boolean;
refreshToken: string | null;
isNewUser: boolean | null;
error: string | null;
isLoading: boolean; isLoading: boolean;
onboardingComplete: boolean;
error: string | null;
} }
const initialState: AuthState = { const initialState: AuthState = {
loginData: null,
user: null, user: null,
accessToken: null, isAuthenticated: false,
refreshToken: null,
isNewUser: null,
isLoading: false, isLoading: false,
onboardingComplete: false,
error: null, 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 ───────────────────────────────────────────────────────────────── // ─── Reducer ─────────────────────────────────────────────────────────────────
const authReducer = createReducer(initialState, builder => { const authReducer = createReducer(initialState, builder => {
builder builder
// Login // ── 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 => { .addCase(loginWithPhone.pending, state => {
state.isLoading = true; state.isLoading = true;
state.error = null; state.error = null;
}) })
.addCase(loginWithPhone.fulfilled, (state, action) => { .addCase(loginWithPhone.fulfilled, state => {
state.loginData = action.payload;
state.isLoading = false; state.isLoading = false;
state.error = null;
}) })
.addCase(loginWithPhone.rejected, (state, action) => { .addCase(loginWithPhone.rejected, (state, action) => {
state.isLoading = false; state.isLoading = false;
state.error = action.payload as string; state.error = (action.payload as string) || 'Login failed';
}) })
// Verify OTP // ── verifyOtp ──────────────────────────────────────────────────────────
.addCase(verifyOtp.pending, state => { .addCase(verifyOtp.pending, state => {
state.isLoading = true; state.isLoading = true;
state.error = null; state.error = null;
}) })
.addCase(verifyOtp.fulfilled, (state, action) => { .addCase(verifyOtp.fulfilled, (state, action) => {
const { user, accessToken, refreshToken, isNewUser } = action.payload;
state.user = user;
state.accessToken = accessToken;
state.refreshToken = refreshToken;
state.isNewUser = isNewUser;
state.isLoading = false; state.isLoading = false;
state.error = null; state.isAuthenticated = true;
state.user = action.payload.user;
}) })
.addCase(verifyOtp.rejected, (state, action) => { .addCase(verifyOtp.rejected, (state, action) => {
state.isLoading = false; state.isLoading = false;
state.error = action.payload as string; state.error = (action.payload as string) || 'OTP verification failed';
}) })
// Complete Onboarding — flips user.status so RootNavigator swaps to AppStack // ── updateProfile ──────────────────────────────────────────────────────
.addCase(completeOnboarding, state => { .addCase(updateProfile.pending, state => {
if (state.user) { state.isLoading = true;
state.user = { ...state.user, status: 'ACTIVE' };
}
})
// Update Profile
.addCase(updateProfile.fulfilled, (state, action) => {
if (state.user && action.payload?.status) {
state.user = { ...state.user, status: action.payload.status };
}
state.isLoading = false;
state.error = null; 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';
})
// Logout — clear all auth state // ── logoutUser ─────────────────────────────────────────────────────────
.addCase(logoutUser.fulfilled, state => { .addCase(logoutUser.fulfilled, state => {
state.user = null; state.user = null;
state.accessToken = null; state.isAuthenticated = false;
state.refreshToken = null; state.onboardingComplete = false;
state.loginData = null;
state.isNewUser = null;
state.isLoading = false;
state.error = null; state.error = null;
}) })
.addCase(logoutUser.rejected, state => { .addCase(logoutUser.rejected, state => {
// Even on failure, clear local auth state (tokens already cleared by thunk) // Still clear state even if API fails (tokens already cleared in thunk)
state.user = null; state.user = null;
state.accessToken = null; state.isAuthenticated = false;
state.refreshToken = null; state.onboardingComplete = false;
state.loginData = null; state.error = null;
state.isNewUser = null;
state.isLoading = false;
}); });
}); });

View File

@ -1,23 +1,18 @@
import { createAsyncThunk } from '@reduxjs/toolkit'; import { createAsyncThunk } from '@reduxjs/toolkit';
import { import { loginApi, verifyOtpApi, updateProfileApi, logoutApi } from '../../../api/authApi';
loginApi,
verifyOtpApi,
updateProfileApi,
logoutApi,
} from '../../../api/authApi';
import { tokenManager } from '../../../services/apiClient'; import { tokenManager } from '../../../services/apiClient';
// ─── Login ─────────────────────────────────────────────────────────────────── // ─── Login ───────────────────────────────────────────────────────────────────
export const loginWithPhone = createAsyncThunk( export const loginWithPhone = createAsyncThunk(
'auth/loginWithPhone', 'auth/loginWithPhone',
async (phone: string, { rejectWithValue }) => { async (mobileNumber: string, { rejectWithValue }) => {
try { try {
const response = await loginApi(phone); const response = await loginApi(mobileNumber);
console.log(response);
return response; return response;
} catch (error: unknown) { } catch (error: unknown) {
const message = error instanceof Error ? error.message : 'Login failed'; const message =
error instanceof Error ? error.message : 'Login failed';
return rejectWithValue(message); return rejectWithValue(message);
} }
}, },
@ -28,11 +23,11 @@ export const loginWithPhone = createAsyncThunk(
export const verifyOtp = createAsyncThunk( export const verifyOtp = createAsyncThunk(
'auth/verifyOtp', 'auth/verifyOtp',
async ( async (
{ phone, code, role }: { phone: string; code: string; role: string }, { mobileNumber, otp }: { mobileNumber: string; otp: string },
{ rejectWithValue }, { rejectWithValue },
) => { ) => {
try { try {
const response = await verifyOtpApi(phone, code, role); const response = await verifyOtpApi(mobileNumber, otp);
// Persist tokens on successful verification // Persist tokens on successful verification
if (response.accessToken && response.refreshToken) { if (response.accessToken && response.refreshToken) {
@ -82,7 +77,8 @@ export const logoutUser = createAsyncThunk(
} catch (error: unknown) { } catch (error: unknown) {
// Clear tokens even if API call fails // Clear tokens even if API call fails
await tokenManager.clearTokens(); await tokenManager.clearTokens();
const message = error instanceof Error ? error.message : 'Logout failed'; const message =
error instanceof Error ? error.message : 'Logout failed';
return rejectWithValue(message); return rejectWithValue(message);
} }
}, },

View File

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

View File

@ -1,45 +1,54 @@
import { createReducer } from '@reduxjs/toolkit'; import { createAction, createReducer } from '@reduxjs/toolkit';
import { CartItem, MerchantCartGroup } from '@interfaces/cart'; import { CartItem, CatalogItem } from '../../../interfaces';
import { addToCartThunk, getCartThunk, updateCartItemThunk } from './thunk';
// ─── State ─────────────────────────────────────────────────────────────────── // ─── State ───────────────────────────────────────────────────────────────────
export interface CartState { export interface CartState {
id: string | null;
customerId: string | null;
items: CartItem[]; items: CartItem[];
groupedByMerchant: MerchantCartGroup[]; couponCode: string | null;
subtotal: number; discount: number;
totalItems: number; providerId: string | null;
isLoading: boolean; providerName: string | null;
error: string | null;
} }
const initialState: CartState = { const initialState: CartState = {
id: null,
customerId: null,
items: [], items: [],
groupedByMerchant: [], couponCode: null,
subtotal: 0, discount: 0,
totalItems: 0, providerId: null,
isLoading: false, providerName: null,
error: 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 ──────────────────────────────────────────────────────────────── // ─── Selector ────────────────────────────────────────────────────────────────
export const selectCartTotal = (state: { cart: CartState }) => { export const selectCartTotal = (state: { cart: CartState }) => {
const subtotal = state.cart.subtotal || 0; const subtotal = state.cart.items.reduce(
const deliveryFee = 40; // Flat fee for now (sum, item) => sum + item.item.price * item.quantity,
const platformFee = 10; // Flat fee for now 0,
const discount = 0; // Removing discount logic for now as requested );
const deliveryFee = 40;
const platformFee = 10;
const discount = state.cart.discount;
return { return {
subtotal, subtotal,
deliveryFee, deliveryFee,
platformFee, platformFee,
discount, discount,
total: Math.max(0, subtotal + deliveryFee + platformFee - discount), total: Math.max(0, subtotal + deliveryFee + platformFee - discount),
itemCount: state.cart.totalItems || 0, itemCount: state.cart.items.reduce((sum, item) => sum + item.quantity, 0),
}; };
}; };
@ -47,64 +56,58 @@ export const selectCartTotal = (state: { cart: CartState }) => {
const cartReducer = createReducer(initialState, builder => { const cartReducer = createReducer(initialState, builder => {
builder builder
// getCartThunk .addCase(addItem, (state, action) => {
.addCase(getCartThunk.pending, state => { const { providerId, providerName, item } = action.payload;
state.isLoading = true;
state.error = null;
})
.addCase(getCartThunk.fulfilled, (state, action) => {
state.isLoading = false;
const cart = action.payload;
state.id = cart.id;
state.customerId = cart.customerId;
state.items = cart.items || [];
state.groupedByMerchant = cart.groupedByMerchant || [];
state.subtotal = cart.subtotal || 0;
state.totalItems = cart.totalItems || 0;
})
.addCase(getCartThunk.rejected, (state, action) => {
state.isLoading = false;
state.error = (action.payload as string) || 'Failed to get cart';
})
// addToCartThunk // Clear cart if switching providers
.addCase(addToCartThunk.pending, state => { if (state.providerId && state.providerId !== providerId) {
state.isLoading = true; state.items = [];
state.error = null; }
})
.addCase(addToCartThunk.fulfilled, (state, action) => {
state.isLoading = false;
const cart = action.payload;
state.id = cart.id;
state.customerId = cart.customerId;
state.items = cart.items || [];
state.groupedByMerchant = cart.groupedByMerchant || [];
state.subtotal = cart.subtotal || 0;
state.totalItems = cart.totalItems || 0;
})
.addCase(addToCartThunk.rejected, (state, action) => {
state.isLoading = false;
state.error = (action.payload as string) || 'Failed to add to cart';
})
// updateCartItemThunk state.providerId = providerId;
.addCase(updateCartItemThunk.pending, state => { state.providerName = providerName;
state.isLoading = true;
state.error = null; 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(updateCartItemThunk.fulfilled, (state, action) => { .addCase(removeItem, (state, action) => {
state.isLoading = false; const existing = state.items.find(i => i.item.id === action.payload);
const cart = action.payload; if (existing) {
state.id = cart.id; if (existing.quantity > 1) {
state.customerId = cart.customerId; existing.quantity -= 1;
state.items = cart.items || []; } else {
state.groupedByMerchant = cart.groupedByMerchant || []; state.items = state.items.filter(i => i.item.id !== action.payload);
state.subtotal = cart.subtotal || 0; }
state.totalItems = cart.totalItems || 0; }
if (state.items.length === 0) {
state.providerId = null;
state.providerName = null;
}
}) })
.addCase(updateCartItemThunk.rejected, (state, action) => { .addCase(clearCart, state => {
state.isLoading = false; state.items = [];
state.error = (action.payload as string) || 'Failed to update cart item'; 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;
}); });
}); });

View File

@ -1,48 +1,2 @@
import { addToCartApi, getCartApi, updateCartItem } from '@api'; // Cart thunks — placeholder for future async cart operations
import { AddToCartRequest, CartResponse } from '@interfaces/cart'; // (e.g., syncing cart with backend, applying server-side coupons)
import { createAsyncThunk } from '@reduxjs/toolkit';
export const addToCartThunk = createAsyncThunk<CartResponse, AddToCartRequest>(
'cart/addToCart',
async (addToCartRequest, { rejectWithValue }) => {
try {
const response = await addToCartApi(addToCartRequest);
return response;
} catch (error: any) {
const message =
error instanceof Error ? error.message : 'Failed to add to cart';
return rejectWithValue(message);
}
},
);
export const getCartThunk = createAsyncThunk<CartResponse>(
'cart/getCart',
async (_, { rejectWithValue }) => {
try {
const response = await getCartApi();
return response;
} catch (error: any) {
const message =
error instanceof Error ? error.message : 'Failed to get cart';
return rejectWithValue(message);
}
},
);
export const updateCartItemThunk = createAsyncThunk<
CartResponse,
{ productId: string; quantity: number }
>(
'cart/updateCartItem',
async ({ productId, quantity }, { rejectWithValue }) => {
try {
const response = await updateCartItem(productId, quantity);
return response;
} catch (error: any) {
const message =
error instanceof Error ? error.message : 'Failed to update cart item';
return rejectWithValue(message);
}
},
);

View File

@ -1,2 +0,0 @@
export * from './thunk';
export * from './reducer';

View File

@ -1,32 +0,0 @@
import { CustomerResponse } from '@interfaces';
import { createReducer } from '@reduxjs/toolkit';
import { fetchCustomerDetails } from './thunk';
export interface CustomerProfileState {
customerDetails: CustomerResponse | null;
error: string | null;
isLoading: boolean;
}
const initialState: CustomerProfileState = {
customerDetails: null,
error: null,
isLoading: false,
};
const customerProfileReducer = createReducer(initialState, builder => {
builder
.addCase(fetchCustomerDetails.pending, state => {
state.isLoading = true;
state.error = null;
})
.addCase(fetchCustomerDetails.fulfilled, (state, action) => {
state.isLoading = false;
state.customerDetails = action.payload;
})
.addCase(fetchCustomerDetails.rejected, (state, action) => {
state.isLoading = false;
state.error = action.payload as string;
});
});
export default customerProfileReducer;

View File

@ -1,17 +0,0 @@
import { getCustomerDetails } from '@api';
import { CustomerResponse } from '@interfaces';
import { createAsyncThunk } from '@reduxjs/toolkit';
export const fetchCustomerDetails = createAsyncThunk<CustomerResponse>(
'customer/fetchCustomerDetails',
async (_, { rejectWithValue }) => {
try {
const response = await getCustomerDetails();
console.log(response);
return response;
} catch (error: any) {
console.log(error);
return rejectWithValue(error?.message);
}
},
);

View File

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

View File

@ -1,27 +1,12 @@
import { combineReducers } from '@reduxjs/toolkit'; import { combineReducers } from '@reduxjs/toolkit';
import { authReducer } from './commonreducers/auth';
import { cartReducer } from './commonreducers/cart'; import { cartReducer } from './commonreducers/cart';
import { orderReducer } from './commonreducers/order'; import { orderReducer } from './commonreducers/order';
import preferencesReducer from '@features/screens/preferencesScreen/reducer';
import setLocationReducer from '@features/screens/setLocationScreen/reducer';
import completeProfileReducer from '@features/screens/completeProfileScreen/reducer';
import authReducer from './commonreducers/auth/reducer';
import homeReducer from '@features/screens/homeScreen/reducer';
import providerDetailsReducer from '@features/screens/providerDetailsScreen/reducer';
import paymentMethodsReducer from '@features/screens/checkoutPaymentScreen/reducer';
import customerProfileReducer from './commonreducers/customerProfile/reducer';
const rootReducer = combineReducers({ const rootReducer = combineReducers({
auth: authReducer, auth: authReducer,
cart: cartReducer, cart: cartReducer,
order: orderReducer, order: orderReducer,
preferences: preferencesReducer,
setLocation: setLocationReducer,
completeProfile: completeProfileReducer,
home: homeReducer,
providerDetails: providerDetailsReducer,
paymentMethods: paymentMethodsReducer,
customerProfile: customerProfileReducer,
}); });
export type RootState = ReturnType<typeof rootReducer>; export type RootState = ReturnType<typeof rootReducer>;

View File

@ -1,31 +0,0 @@
export const getFullUrl = (url?: string) => {
const BASE_URL = 'https://db1d-202-8-116-13.ngrok-free.app';
if (!url) return '';
return url.startsWith('/') ? `${BASE_URL}${url}` : url;
};
export const formatTime = (dateString?: string) => {
if (!dateString) return '';
const dateObj = new Date(dateString);
// Example output: 11:57 AM
return dateObj.toLocaleTimeString('en-US', {
hour: 'numeric',
minute: '2-digit',
hour12: true,
});
};
export const formatDate = (dateString?: string) => {
if (!dateString) return '';
const dateObj = new Date(dateString);
// Example output: July 8, 2026
return dateObj.toLocaleDateString('en-US', {
month: 'long',
day: 'numeric',
year: 'numeric',
});
};

View File

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

View File

@ -9,12 +9,6 @@ module.exports = {
'@components': './app/components', '@components': './app/components',
'@features/screens': './app/features/screens', '@features/screens': './app/features/screens',
'@theme': './app/theme', '@theme': './app/theme',
'@hooks': './app/hooks',
'@navigation': './app/navigation',
'@services': './app/services',
'@store': './app/store',
'@api': './app/api',
'@utils': './app/utils',
}, },
}, },
], ],

View File

@ -2,8 +2,4 @@ import { AppRegistry } from 'react-native';
import App from './app/App'; import App from './app/App';
import { name as appName } from './app.json'; import { name as appName } from './app.json';
if (__DEV__) {
require('./ReactotronConfig');
}
AppRegistry.registerComponent(appName, () => App); AppRegistry.registerComponent(appName, () => App);

View File

@ -27,10 +27,8 @@
"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-uuid": "^2.0.4",
"react-native-worklets": "^0.10.0", "react-native-worklets": "^0.10.0",
"react-redux": "^9.3.0", "react-redux": "^9.3.0",
"reactotron-react-native": "^5.2.0",
"redux-persist": "^6.0.0", "redux-persist": "^6.0.0",
"redux-thunk": "^3.1.0" "redux-thunk": "^3.1.0"
}, },

View File

@ -9,21 +9,7 @@
"@features/screens": ["./app/features/screens"], "@features/screens": ["./app/features/screens"],
"@features/screens/*": ["./app/features/screens/*"], "@features/screens/*": ["./app/features/screens/*"],
"@theme": ["./app/theme"], "@theme": ["./app/theme"],
"@theme/*": ["./app/theme/*"], "@theme/*": ["./app/theme/*"]
"@api": ["./app/api"],
"@api/*": ["./app/api/*"],
"@store": ["./app/store"],
"@store/*": ["./app/store/*"],
"@interfaces": ["./app/interfaces"],
"@interfaces/*": ["./app/interfaces/*"],
"@navigation": ["./app/navigation"],
"@navigation/*": ["./app/navigation/*"],
"@hooks": ["./app/hooks"],
"@hooks/*": ["./app/hooks/*"],
"@utils": ["./app/utils"],
"@utils/*": ["./app/utils/*"],
"@services": ["./app/services"],
"@services/*": ["./app/services/*"]
} }
}, },
"include": ["**/*.ts", "**/*.tsx"], "include": ["**/*.ts", "**/*.tsx"],

Some files were not shown because too many files have changed in this diff Show More