feat: implement checkout address selection and customer profile data fetching

This commit is contained in:
Tamojit Biswas 2026-07-08 18:00:00 +05:30
parent 0806289a81
commit d7c2254d2e
44 changed files with 2178 additions and 269 deletions

View File

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

View File

@ -1,5 +1,11 @@
import { apiClient } from '../services/apiClient'; import { apiClient } from '../services/apiClient';
import { Provider, CatalogItem, Order, DeliveryAgent, Location } from '../interfaces'; import {
Provider,
CatalogItem,
Order,
DeliveryAgent,
Location,
} from '../interfaces';
// ─── Types ─────────────────────────────────────────────────────────────────── // ─── Types ───────────────────────────────────────────────────────────────────
@ -10,8 +16,7 @@ export interface PlaceOrderResponse {
// ─── Endpoints ─────────────────────────────────────────────────────────────── // ─── Endpoints ───────────────────────────────────────────────────────────────
export const getProvidersApi = () => export const getProvidersApi = () => apiClient.get<Provider[]>('/providers');
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`);
@ -19,8 +24,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

@ -3,3 +3,6 @@ export * from './deliveryApi';
export * from './onboardApi'; export * from './onboardApi';
export * from './productApi'; export * from './productApi';
export * from './cartApi'; export * from './cartApi';
export * from './paymentMethodsApi';
export * from './customerDetailsApi';
export * from './orderApi';

14
app/api/orderApi.ts Normal file
View File

@ -0,0 +1,14 @@
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

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

View File

@ -0,0 +1,7 @@
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

@ -0,0 +1,317 @@
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

@ -0,0 +1,377 @@
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

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

View File

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

View File

@ -53,12 +53,42 @@ 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',
marginTop: 10, paddingTop: 12,
paddingTop: 10,
borderTopWidth: 1, borderTopWidth: 1,
borderTopColor: colors.border, borderTopColor: colors.border,
}, },

View File

@ -12,6 +12,7 @@ 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);
@ -30,6 +31,20 @@ 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,48 +1,50 @@
import { StyleSheet } from 'react-native'; import { StyleSheet } from 'react-native';
import { typography } from '@theme'; import { typography } from '@theme';
export const getStyles = (colors: any) => StyleSheet.create({ export const getStyles = (colors: any) =>
container: { StyleSheet.create({
flexDirection: 'row', container: {
alignItems: 'center', flexDirection: 'row',
paddingVertical: 14, alignItems: 'center',
paddingHorizontal: 16, paddingVertical: 14,
borderWidth: 1.5, paddingHorizontal: 16,
borderColor: colors.border, borderWidth: 1.5,
borderRadius: 12, borderColor: colors.border,
marginBottom: 10, borderRadius: 12,
backgroundColor: colors.background, marginBottom: 10,
}, backgroundColor: colors.background,
selected: { },
borderColor: colors.primary, selected: {
backgroundColor: '#E8F5E9', borderColor: colors.primary,
}, backgroundColor: '#E8F5E9',
icon: { },
fontSize: 24, icon: {
marginRight: 12, width: 24,
}, height: 24,
label: { marginRight: 12,
flex: 1, },
fontSize: typography.fontSize.md, label: {
fontWeight: typography.fontWeight.medium, flex: 1,
color: colors.text, fontSize: typography.fontSize.md,
}, fontWeight: typography.fontWeight.medium,
radio: { color: colors.text,
width: 22, },
height: 22, radio: {
borderRadius: 11, width: 22,
borderWidth: 2, height: 22,
borderColor: colors.border, borderRadius: 11,
justifyContent: 'center', borderWidth: 2,
alignItems: 'center', borderColor: colors.border,
}, justifyContent: 'center',
radioSelected: { alignItems: 'center',
borderColor: colors.primary, },
}, radioSelected: {
radioInner: { borderColor: colors.primary,
width: 12, },
height: 12, radioInner: {
borderRadius: 6, width: 12,
backgroundColor: colors.primary, height: 12,
}, borderRadius: 6,
}); backgroundColor: colors.primary,
},
});

View File

@ -1,17 +1,18 @@
import React from 'react'; import React from 'react';
import { View, Text, TouchableOpacity } from 'react-native'; import { View, Text, TouchableOpacity, Image } 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
@ -19,7 +20,7 @@ export const PaymentOption: React.FC<PaymentOptionProps> = ({
onPress={onSelect} onPress={onSelect}
activeOpacity={0.7} activeOpacity={0.7}
> >
<Text style={styles.icon}>{type === 'UPI' ? '📱' : type === 'Card' ? '💳' : type === 'Wallet' ? '👛' : '💵'}</Text> <Image source={{ uri: icon }} style={styles.icon} />
<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

@ -9,6 +9,7 @@ import {
} from 'react-native'; } from 'react-native';
import { getStyles } from './productCard.styles'; import { getStyles } from './productCard.styles';
import { useAppTheme } from '@theme'; import { useAppTheme } from '@theme';
import { getFullUrl } from '@utils/helper';
export interface ProductCardProps { export interface ProductCardProps {
id: string; id: string;
@ -63,9 +64,7 @@ export const ProductCard: React.FC<ProductCardProps> = ({
<View style={styles.imageContainer}> <View style={styles.imageContainer}>
<Image <Image
source={{ source={{
uri: imageUrl?.startsWith('/') uri: getFullUrl(imageUrl),
? `https://f7ee-202-8-116-13.ngrok-free.app${imageUrl}`
: imageUrl,
}} }}
style={styles.image} style={styles.image}
// fallback source can be handled here if needed // fallback source can be handled here if needed
@ -99,13 +98,13 @@ export const ProductCard: React.FC<ProductCardProps> = ({
)} )}
</View> </View>
<TouchableOpacity {/* <TouchableOpacity
style={styles.addButton} style={styles.addButton}
onPress={onAddPress || onPress} onPress={onAddPress || onPress}
activeOpacity={0.7} activeOpacity={0.7}
> >
<Text style={styles.addButtonText}>+ ADD</Text> <Text style={styles.addButtonText}>+ ADD</Text>
</TouchableOpacity> </TouchableOpacity> */}
</View> </View>
</TouchableOpacity> </TouchableOpacity>
); );

View File

@ -1,92 +1,130 @@
import { StyleSheet } from 'react-native'; import { StyleSheet } from 'react-native';
import { typography } from '@theme'; import { typography } from '@theme';
export const getStyles = (colors: any) => StyleSheet.create({ export const getStyles = (colors: any) =>
container: { StyleSheet.create({
flex: 1, container: {
backgroundColor: colors.background, flex: 1,
}, backgroundColor: colors.background,
content: { },
padding: 16, content: {
}, padding: 16,
addressCard: { paddingBottom: 24,
backgroundColor: colors.cardBg, },
borderRadius: 12, sectionTitle: {
padding: 16, fontSize: typography.fontSize.md,
borderWidth: 1, fontWeight: typography.fontWeight.semibold,
borderColor: colors.border, color: colors.text,
marginBottom: 24, marginBottom: 12,
}, },
addressLabel: { emptyState: {
fontSize: typography.fontSize.xs, padding: 24,
color: colors.textSecondary, alignItems: 'center',
marginBottom: 4, justifyContent: 'center',
}, },
addressText: { emptyStateText: {
fontSize: typography.fontSize.md, fontSize: typography.fontSize.sm,
fontWeight: typography.fontWeight.semibold, color: colors.textSecondary,
color: colors.text, },
marginBottom: 4, addressCard: {
}, backgroundColor: colors.cardBg,
addressSubtext: { borderRadius: 12,
fontSize: typography.fontSize.sm, padding: 16,
color: colors.primary, borderWidth: 1.5,
fontWeight: typography.fontWeight.medium, borderColor: colors.border,
}, marginBottom: 12,
sectionTitle: { },
fontSize: typography.fontSize.md, addressCardSelected: {
fontWeight: typography.fontWeight.semibold, borderColor: colors.primary,
color: colors.text, backgroundColor: '#E8F5E9',
marginBottom: 12, },
}, addressCardHeader: {
optionCard: { flexDirection: 'row',
flexDirection: 'row', alignItems: 'center',
alignItems: 'center', marginBottom: 8,
padding: 16, },
borderRadius: 12, labelBadge: {
borderWidth: 1.5, flexDirection: 'row',
borderColor: colors.border, alignItems: 'center',
marginBottom: 10, backgroundColor: colors.background,
backgroundColor: colors.background, borderRadius: 20,
}, paddingVertical: 4,
optionCardSelected: { paddingHorizontal: 10,
borderColor: colors.primary, marginRight: 8,
backgroundColor: '#E8F5E9', },
}, labelBadgeIcon: {
optionInfo: { fontSize: 12,
flex: 1, marginRight: 4,
}, },
optionTitle: { labelBadgeText: {
fontSize: typography.fontSize.md, fontSize: typography.fontSize.xs,
fontWeight: typography.fontWeight.medium, 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,
radio: { },
width: 22, defaultBadgeText: {
height: 22, fontSize: 10,
borderRadius: 11, fontWeight: typography.fontWeight.bold,
borderWidth: 2, color: '#fff',
borderColor: colors.border, letterSpacing: 0.5,
justifyContent: 'center', },
alignItems: 'center', radio: {
}, width: 22,
radioSelected: { height: 22,
borderColor: colors.primary, borderRadius: 11,
}, borderWidth: 2,
radioInner: { borderColor: colors.border,
width: 12, justifyContent: 'center',
height: 12, alignItems: 'center',
borderRadius: 6, marginLeft: 'auto',
backgroundColor: colors.primary, },
}, radioSelected: {
footer: { borderColor: colors.primary,
padding: 16, },
borderTopWidth: 1, radioInner: {
borderTopColor: colors.border, width: 12,
}, height: 12,
}); borderRadius: 6,
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: {
padding: 16,
borderTopWidth: 1,
borderTopColor: colors.border,
},
});

View File

@ -1,18 +1,18 @@
import React, { useState } from 'react'; import React, { useState, useMemo } from 'react';
import { import { View, Text, TouchableOpacity, ScrollView } from 'react-native';
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 { AppStackParamList } from '../../../navigation/appStack'; import { useAppSelector } from '@store';
import { AddressLabel, CustomerAddress } from '@interfaces';
import { AppStackParamList } from '@navigation/appStack';
type CheckoutAddressNavProp = StackNavigationProp<AppStackParamList, 'CheckoutAddressScreen'>; type CheckoutAddressNavProp = StackNavigationProp<
AppStackParamList,
'CheckoutAddressScreen'
>;
const CHECKOUT_STEPS = [ const CHECKOUT_STEPS = [
{ key: 'address', label: 'Address' }, { key: 'address', label: 'Address' },
@ -20,62 +20,118 @@ 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 [deliveryOption, setDeliveryOption] = useState<'standard' | 'express'>('standard'); const { customerDetails } = useAppSelector(state => state.customerProfile);
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}>
<View style={styles.addressCard}> <Text style={styles.sectionTitle}>Deliver to</Text>
<Text style={styles.addressLabel}>Deliver to</Text>
<Text style={styles.addressText}>
Koramangala 4th Block, Bengaluru, 560034
</Text>
<Text style={styles.addressSubtext}>Home</Text>
</View>
<Text style={styles.sectionTitle}>Delivery Option</Text> {addresses.length === 0 && (
<TouchableOpacity <View style={styles.emptyState}>
style={[ <Text style={styles.emptyStateText}>No saved addresses yet.</Text>
styles.optionCard,
deliveryOption === 'standard' && styles.optionCardSelected,
]}
onPress={() => setDeliveryOption('standard')}
activeOpacity={0.7}
>
<View style={styles.optionInfo}>
<Text style={styles.optionTitle}>Standard Delivery</Text>
<Text style={styles.optionSubtext}>Free 25-30 min</Text>
</View> </View>
<View style={[styles.radio, deliveryOption === 'standard' && styles.radioSelected]}> )}
{deliveryOption === 'standard' && <View style={styles.radioInner} />}
</View> {addresses.map(address => {
</TouchableOpacity> const isSelected = selectedAddressId === address.id;
return (
<TouchableOpacity
key={address.id}
style={[
styles.addressCard,
isSelected && styles.addressCardSelected,
]}
activeOpacity={0.7}
onPress={() => setSelectedAddressId(address.id)}
>
<View style={styles.addressCardHeader}>
<View style={styles.labelBadge}>
<Text style={styles.labelBadgeIcon}>
{LABEL_ICON[address.label] ?? '📍'}
</Text>
<Text style={styles.labelBadgeText}>{address.label}</Text>
</View>
{address.isDefault && (
<View style={styles.defaultBadge}>
<Text style={styles.defaultBadgeText}>DEFAULT</Text>
</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
style={[ style={styles.addAddressButton}
styles.optionCard,
deliveryOption === 'express' && styles.optionCardSelected,
]}
onPress={() => setDeliveryOption('express')}
activeOpacity={0.7} activeOpacity={0.7}
onPress={() => {
// navigation.navigate('AddAddressScreen');
}}
> >
<View style={styles.optionInfo}> <Text style={styles.addAddressButtonText}>+ Add New Address</Text>
<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 title="Continue to Payment" onPress={() => navigation.navigate('CheckoutPaymentScreen')} /> <PrimaryButton
title="Continue to Payment"
onPress={handleContinue}
disabled={!selectedAddressId}
/>
</View> </View>
</View> </View>
); );

View File

@ -1,13 +1,30 @@
import React, { useState } from 'react'; import React, { useEffect, useState } from 'react';
import { View, Text, ScrollView } from 'react-native'; import { View, Text, ScrollView, Alert } from 'react-native';
import { useNavigation } from '@react-navigation/native'; import { RouteProp, useNavigation, useRoute } 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 { Header, StepProgress, PaymentOption, PrimaryButton } from '@components'; import {
Header,
StepProgress,
PaymentOption,
PrimaryButton,
} from '@components';
import { useAppTheme } from '@theme'; import { useAppTheme } from '@theme';
import { AppStackParamList } from '../../../navigation/appStack'; 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<AppStackParamList, 'CheckoutPaymentScreen'>; type CheckoutPaymentNavProp = StackNavigationProp<
AppStackParamList,
'CheckoutPaymentScreen'
>;
type CheckoutPaymentRouteProp = RouteProp<
AppStackParamList,
'CheckoutPaymentScreen'
>;
const CHECKOUT_STEPS = [ const CHECKOUT_STEPS = [
{ key: 'address', label: 'Address' }, { key: 'address', label: 'Address' },
@ -25,8 +42,59 @@ 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}>
@ -34,18 +102,22 @@ 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>
{PAYMENT_METHODS.map((method) => ( {paymentMethods?.map(method => (
<PaymentOption <PaymentOption
key={method.id} key={method.id}
type={method.type} label={method.code}
label={method.label} icon={method.iconUrl}
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 title="Place Order" onPress={() => navigation.navigate('OrderConfirmedScreen', { orderId: 'ORD-' + Math.floor(Math.random() * 900000 + 100000) })} /> <PrimaryButton
title={placeOrderLoading ? 'Placing...' : 'Place Order'}
onPress={handlePlaceOrder}
disabled={placeOrderLoading}
/>
</View> </View>
</View> </View>
); );

View File

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

View File

@ -0,0 +1,94 @@
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

@ -0,0 +1,74 @@
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

@ -22,7 +22,7 @@ import { useAppTheme } from '@theme';
import { Product } from '../../../interfaces'; import { Product } from '../../../interfaces';
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'; import { fetchCustomerDetails, useAppDispatch, useAppSelector } from '@store';
import { getAllProductsThunk } from './thunk'; import { getAllProductsThunk } from './thunk';
type NavProp = CompositeNavigationProp< type NavProp = CompositeNavigationProp<
@ -83,6 +83,7 @@ export const HomeScreen: React.FC = () => {
useEffect(() => { useEffect(() => {
dispatch(getAllProductsThunk()); dispatch(getAllProductsThunk());
dispatch(fetchCustomerDetails());
}, [dispatch]); }, [dispatch]);
const filteredProducts = const filteredProducts =

View File

@ -20,3 +20,4 @@ export * from './offersScreen';
export * from './helpSupportScreen'; export * from './helpSupportScreen';
export * from './accountScreen'; export * from './accountScreen';
export * from './writeReviewScreen'; export * from './writeReviewScreen';
export * from './orderDetailsScreen';

View File

@ -1,11 +1,9 @@
import React, { useState } from 'react'; import React, { useEffect, useState } from 'react';
import { View, Text, FlatList, TouchableOpacity } from 'react-native';
import { import {
View, useNavigation,
Text, CompositeNavigationProp,
FlatList, } from '@react-navigation/native';
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';
@ -13,6 +11,9 @@ 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'>,
@ -21,9 +22,30 @@ 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-771239', providerName: 'Fresh Mart', providerImage: '', orderDate: '28 Jun 2026', status: 'Ongoing', total: 349 }, id: 'ORD-591283',
{ id: 'ORD-108239', providerName: 'Burger Barn', providerImage: '', orderDate: '27 Jun 2026', status: 'Delivered', total: 449 }, providerName: 'Pizza Planet',
providerImage: '',
orderDate: '29 Jun 2026',
status: 'Delivered',
total: 599,
},
{
id: 'ORD-771239',
providerName: 'Fresh Mart',
providerImage: '',
orderDate: '28 Jun 2026',
status: 'Ongoing',
total: 349,
},
{
id: 'ORD-108239',
providerName: 'Burger Barn',
providerImage: '',
orderDate: '27 Jun 2026',
status: 'Delivered',
total: 449,
},
]; ];
export const MyOrdersScreen: React.FC = () => { export const MyOrdersScreen: React.FC = () => {
@ -31,24 +53,42 @@ 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);
const filteredOrders = activeTab === 'All' useEffect(() => {
? mockOrders dispatch(getOrderHistoryThunk());
: activeTab === 'Ongoing' }, []);
? mockOrders.filter((o) => o.status === 'Ongoing')
: mockOrders.filter((o) => o.status === 'Delivered'); 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'
? orders.filter(o => ONGOING_STATUSES.includes(o.status))
: orders.filter(o => COMPLETED_STATUSES.includes(o.status));
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={[ style={[styles.tab, activeTab === tab && styles.tabActive]}
styles.tab,
activeTab === tab && styles.tabActive,
]}
onPress={() => setActiveTab(tab)} onPress={() => setActiveTab(tab)}
activeOpacity={0.7} activeOpacity={0.7}
> >
@ -65,25 +105,36 @@ 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.providerName} providerName={item?.merchant?.name || ''}
providerImage={item.providerImage} providerImage={item?.merchant?.imageUrl || ''}
orderDate={item.orderDate} orderDate={formatDate(item?.createdAt)}
status={item.status} status={item.status}
total={item.total} total={Number(item?.totalAmount) || 0}
items={item.orderItems}
onReorder={() => { onReorder={() => {
navigation.navigate('ProviderDetailsScreen', { navigation.navigate('ProviderDetailsScreen', {
providerId: 'p1', providerId: 'p1',
providerName: item.providerName, providerName: item?.merchant?.name || '',
}); });
}} }}
onDetails={() => { onDetails={() => {
if (item.status === 'Ongoing') { if (
navigation.navigate('OrderTrackingScreen', { orderId: item.id }); item?.status === 'PENDING' ||
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', { orderId: item.id }); navigation.navigate('OrderDeliveredScreen', {
orderId: item.id,
});
} }
}} }}
/> />

View File

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

View File

@ -0,0 +1,194 @@
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

@ -0,0 +1,191 @@
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

@ -35,7 +35,7 @@ export interface User {
gender: Gender | null; gender: Gender | null;
dateOfBirth: string | null; dateOfBirth: string | null;
role: Role; role: Role;
merchants: Merchant[]; // merchants: Merchant[];
} }
export interface Role { export interface Role {
@ -50,7 +50,8 @@ export interface CategoryPreference {
} }
export interface Merchant { export interface Merchant {
// Extend when backend starts returning data name: string;
imageUrl: string | null;
} }
export type RoleEnum = 'CUSTOMER'; export type RoleEnum = 'CUSTOMER';

View File

@ -0,0 +1,49 @@
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,37 @@ export interface CartItem {
quantity: number; quantity: number;
} }
export type OrderStatus = // export type OrderStatus =
| 'placed' // | 'placed'
| 'confirmed' // | 'confirmed'
| 'dispatched' // | 'dispatched'
| 'delivered' // | 'delivered'
| 'cancelled'; // | '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;
@ -93,3 +93,6 @@ export * from './auth';
export * from './onboard'; export * from './onboard';
export * from './product'; export * from './product';
export * from './cart'; export * from './cart';
export * from './paymentMethods';
export * from './customerProfile';
export * from './order';

187
app/interfaces/order.ts Normal file
View File

@ -0,0 +1,187 @@
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

@ -0,0 +1,12 @@
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

@ -14,6 +14,7 @@ import {
OrderDeliveredScreen, OrderDeliveredScreen,
HelpSupportScreen, HelpSupportScreen,
WriteReviewScreen, WriteReviewScreen,
OrderDetailsScreen,
} from '@features/screens'; } from '@features/screens';
export type AppStackParamList = { export type AppStackParamList = {
@ -22,13 +23,14 @@ export type AppStackParamList = {
ProviderDetailsScreen: { providerId: string; providerName?: string }; ProviderDetailsScreen: { providerId: string; providerName?: string };
CartScreen: undefined; CartScreen: undefined;
CheckoutAddressScreen: undefined; CheckoutAddressScreen: undefined;
CheckoutPaymentScreen: undefined; CheckoutPaymentScreen: { selectedAddressId?: string } | 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; WriteReviewScreen: { productId: string } | undefined;
OrderDetailsScreen: { orderId: string } | undefined;
}; };
const Stack = createStackNavigator<AppStackParamList>(); const Stack = createStackNavigator<AppStackParamList>();
@ -66,6 +68,10 @@ export const AppStack: React.FC = () => {
/> />
<Stack.Screen name="HelpSupportScreen" component={HelpSupportScreen} /> <Stack.Screen name="HelpSupportScreen" component={HelpSupportScreen} />
<Stack.Screen name="WriteReviewScreen" component={WriteReviewScreen} /> <Stack.Screen name="WriteReviewScreen" component={WriteReviewScreen} />
<Stack.Screen
name="OrderDetailsScreen"
component={OrderDetailsScreen}
/>
</Stack.Navigator> </Stack.Navigator>
); );
}; };

View File

@ -15,7 +15,7 @@ const STORAGE_KEYS = {
} as const; } as const;
// ─── Config ────────────────────────────────────────────────────────────────── // ─── Config ──────────────────────────────────────────────────────────────────
const BASE_URL = 'https://2e1b-202-8-116-13.ngrok-free.app'; // TODO: replace with your actual base URL const BASE_URL = 'https://9b5b-202-8-116-13.ngrok-free.app'; // TODO: replace with your actual base URL
// ─── Token Helpers ─────────────────────────────────────────────────────────── // ─── Token Helpers ───────────────────────────────────────────────────────────
export const tokenManager = { export const tokenManager = {

View File

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

View File

@ -0,0 +1,32 @@
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

@ -0,0 +1,17 @@
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,3 +1,4 @@
export * from './auth'; export * from './auth';
export * from './cart'; export * from './cart';
export * from './order'; export * from './order';
export * from './customerProfile';

View File

@ -8,6 +8,8 @@ import completeProfileReducer from '@features/screens/completeProfileScreen/redu
import authReducer from './commonreducers/auth/reducer'; import authReducer from './commonreducers/auth/reducer';
import homeReducer from '@features/screens/homeScreen/reducer'; import homeReducer from '@features/screens/homeScreen/reducer';
import providerDetailsReducer from '@features/screens/providerDetailsScreen/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,
@ -18,6 +20,8 @@ const rootReducer = combineReducers({
completeProfile: completeProfileReducer, completeProfile: completeProfileReducer,
home: homeReducer, home: homeReducer,
providerDetails: providerDetailsReducer, providerDetails: providerDetailsReducer,
paymentMethods: paymentMethodsReducer,
customerProfile: customerProfileReducer,
}); });
export type RootState = ReturnType<typeof rootReducer>; export type RootState = ReturnType<typeof rootReducer>;

View File

@ -1,5 +1,31 @@
export const getFullUrl = (url?: string) => { export const getFullUrl = (url?: string) => {
const BASE_URL = 'https://2e1b-202-8-116-13.ngrok-free.app'; const BASE_URL = 'https://9b5b-202-8-116-13.ngrok-free.app';
if (!url) return ''; if (!url) return '';
return url.startsWith('/') ? `${BASE_URL}${url}` : url; 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

@ -27,6 +27,7 @@
"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", "reactotron-react-native": "^5.2.0",

View File

@ -5858,6 +5858,11 @@ react-native-svg@^15.15.5:
css-select "^5.1.0" css-select "^5.1.0"
css-tree "^1.1.3" css-tree "^1.1.3"
react-native-uuid@^2.0.4:
version "2.0.4"
resolved "https://registry.yarnpkg.com/react-native-uuid/-/react-native-uuid-2.0.4.tgz#0c4f345523c5feac0282d3a51fe19887727df988"
integrity sha512-LSJNeh559qC17fgVPBsWuTSW/OygFp2dwTcf94IQBLYft5FzIQS9pCsuT36OPvyvDOMb6yiGr6TafaJDnz9PPQ==
react-native-worklets@^0.10.0: react-native-worklets@^0.10.0:
version "0.10.1" version "0.10.1"
resolved "https://registry.yarnpkg.com/react-native-worklets/-/react-native-worklets-0.10.1.tgz#7a3038a3b5ec66cab030a00e1568b6599696f5f2" resolved "https://registry.yarnpkg.com/react-native-worklets/-/react-native-worklets-0.10.1.tgz#7a3038a3b5ec66cab030a00e1568b6599696f5f2"