feat: implement checkout address selection and customer profile data fetching
This commit is contained in:
parent
0806289a81
commit
d7c2254d2e
6
app/api/customerDetailsApi.ts
Normal file
6
app/api/customerDetailsApi.ts
Normal file
@ -0,0 +1,6 @@
|
||||
import { CustomerResponse } from '@interfaces';
|
||||
import { apiClient } from '@services';
|
||||
|
||||
export const getCustomerDetails = async () => {
|
||||
return await apiClient.get<CustomerResponse>('/customers/profile');
|
||||
};
|
||||
@ -1,5 +1,11 @@
|
||||
import { apiClient } from '../services/apiClient';
|
||||
import { Provider, CatalogItem, Order, DeliveryAgent, Location } from '../interfaces';
|
||||
import {
|
||||
Provider,
|
||||
CatalogItem,
|
||||
Order,
|
||||
DeliveryAgent,
|
||||
Location,
|
||||
} from '../interfaces';
|
||||
|
||||
// ─── Types ───────────────────────────────────────────────────────────────────
|
||||
|
||||
@ -10,8 +16,7 @@ export interface PlaceOrderResponse {
|
||||
|
||||
// ─── Endpoints ───────────────────────────────────────────────────────────────
|
||||
|
||||
export const getProvidersApi = () =>
|
||||
apiClient.get<Provider[]>('/providers');
|
||||
export const getProvidersApi = () => apiClient.get<Provider[]>('/providers');
|
||||
|
||||
export const getProviderCatalogApi = (providerId: string) =>
|
||||
apiClient.get<CatalogItem[]>(`/providers/${providerId}/catalog`);
|
||||
@ -19,8 +24,8 @@ export const getProviderCatalogApi = (providerId: string) =>
|
||||
export const searchProvidersApi = (query: string) =>
|
||||
apiClient.get<Provider[]>(`/search?q=${query}`);
|
||||
|
||||
export const placeOrderApi = (orderData: Partial<Order>) =>
|
||||
apiClient.post<PlaceOrderResponse>('/orders', orderData);
|
||||
// export const placeOrderApi = (orderData: Partial<Order>) =>
|
||||
// apiClient.post<PlaceOrderResponse>('/orders', orderData);
|
||||
|
||||
export const getDeliveryAgentApi = () =>
|
||||
apiClient.get<DeliveryAgent>('/delivery/agent');
|
||||
|
||||
@ -3,3 +3,6 @@ export * from './deliveryApi';
|
||||
export * from './onboardApi';
|
||||
export * from './productApi';
|
||||
export * from './cartApi';
|
||||
export * from './paymentMethodsApi';
|
||||
export * from './customerDetailsApi';
|
||||
export * from './orderApi';
|
||||
|
||||
14
app/api/orderApi.ts
Normal file
14
app/api/orderApi.ts
Normal 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}`);
|
||||
};
|
||||
6
app/api/paymentMethodsApi.ts
Normal file
6
app/api/paymentMethodsApi.ts
Normal file
@ -0,0 +1,6 @@
|
||||
import { PaymentMethodsResponse } from '@interfaces';
|
||||
import { apiClient } from '@services';
|
||||
|
||||
export const getAllPaymentMethodsApi = async () => {
|
||||
return await apiClient.get<PaymentMethodsResponse>('/payments/options');
|
||||
};
|
||||
@ -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
|
||||
}
|
||||
317
app/components/OrderStatusTimeline/OrderStatusTimeline.styles.ts
Normal file
317
app/components/OrderStatusTimeline/OrderStatusTimeline.styles.ts
Normal 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,
|
||||
},
|
||||
});
|
||||
377
app/components/OrderStatusTimeline/OrderStatusTimeline.tsx
Normal file
377
app/components/OrderStatusTimeline/OrderStatusTimeline.tsx
Normal 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>
|
||||
);
|
||||
};
|
||||
2
app/components/OrderStatusTimeline/index.ts
Normal file
2
app/components/OrderStatusTimeline/index.ts
Normal file
@ -0,0 +1,2 @@
|
||||
export * from './OrderStatusTimeline';
|
||||
export type * from './OrderStatusTimeline.props';
|
||||
@ -13,3 +13,4 @@ export * from './categoryChip';
|
||||
export * from './ratingStars';
|
||||
export * from './orderHistoryCard';
|
||||
export * from './paymentOption';
|
||||
export * from './OrderStatusTimeline';
|
||||
|
||||
@ -1,3 +1,5 @@
|
||||
import { OrderItem } from '@interfaces';
|
||||
|
||||
export interface OrderHistoryCardProps {
|
||||
providerName: string;
|
||||
providerImage: string;
|
||||
@ -6,4 +8,5 @@ export interface OrderHistoryCardProps {
|
||||
total: number;
|
||||
onReorder?: () => void;
|
||||
onDetails?: () => void;
|
||||
items?: OrderItem[];
|
||||
}
|
||||
|
||||
@ -53,12 +53,42 @@ export const getStyles = (colors: any) => StyleSheet.create({
|
||||
fontWeight: typography.fontWeight.medium,
|
||||
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: {
|
||||
flexDirection: 'row',
|
||||
justifyContent: 'space-between',
|
||||
alignItems: 'center',
|
||||
marginTop: 10,
|
||||
paddingTop: 10,
|
||||
paddingTop: 12,
|
||||
borderTopWidth: 1,
|
||||
borderTopColor: colors.border,
|
||||
},
|
||||
|
||||
@ -12,6 +12,7 @@ export const OrderHistoryCard: React.FC<OrderHistoryCardProps> = ({
|
||||
total,
|
||||
onReorder,
|
||||
onDetails,
|
||||
items,
|
||||
}) => {
|
||||
const { colors } = useAppTheme();
|
||||
const styles = getStyles(colors);
|
||||
@ -30,6 +31,20 @@ export const OrderHistoryCard: React.FC<OrderHistoryCardProps> = ({
|
||||
<Text style={styles.statusText}>{status}</Text>
|
||||
</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}>
|
||||
<Text style={styles.total}>₹{total}</Text>
|
||||
<View style={styles.actions}>
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
export interface PaymentOptionProps {
|
||||
type: string;
|
||||
label: string;
|
||||
isSelected: boolean;
|
||||
icon?: string;
|
||||
onSelect: () => void;
|
||||
}
|
||||
|
||||
@ -1,7 +1,8 @@
|
||||
import { StyleSheet } from 'react-native';
|
||||
import { typography } from '@theme';
|
||||
|
||||
export const getStyles = (colors: any) => StyleSheet.create({
|
||||
export const getStyles = (colors: any) =>
|
||||
StyleSheet.create({
|
||||
container: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
@ -18,7 +19,8 @@ export const getStyles = (colors: any) => StyleSheet.create({
|
||||
backgroundColor: '#E8F5E9',
|
||||
},
|
||||
icon: {
|
||||
fontSize: 24,
|
||||
width: 24,
|
||||
height: 24,
|
||||
marginRight: 12,
|
||||
},
|
||||
label: {
|
||||
|
||||
@ -1,17 +1,18 @@
|
||||
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 { getStyles } from './paymentOption.styles';
|
||||
import { useAppTheme } from '@theme';
|
||||
|
||||
export const PaymentOption: React.FC<PaymentOptionProps> = ({
|
||||
type,
|
||||
label,
|
||||
isSelected,
|
||||
icon,
|
||||
onSelect,
|
||||
}) => {
|
||||
const { colors } = useAppTheme();
|
||||
const styles = getStyles(colors);
|
||||
// console.log(icon);
|
||||
|
||||
return (
|
||||
<TouchableOpacity
|
||||
@ -19,7 +20,7 @@ export const PaymentOption: React.FC<PaymentOptionProps> = ({
|
||||
onPress={onSelect}
|
||||
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>
|
||||
<View style={[styles.radio, isSelected && styles.radioSelected]}>
|
||||
{isSelected && <View style={styles.radioInner} />}
|
||||
|
||||
@ -9,6 +9,7 @@ import {
|
||||
} from 'react-native';
|
||||
import { getStyles } from './productCard.styles';
|
||||
import { useAppTheme } from '@theme';
|
||||
import { getFullUrl } from '@utils/helper';
|
||||
|
||||
export interface ProductCardProps {
|
||||
id: string;
|
||||
@ -63,9 +64,7 @@ export const ProductCard: React.FC<ProductCardProps> = ({
|
||||
<View style={styles.imageContainer}>
|
||||
<Image
|
||||
source={{
|
||||
uri: imageUrl?.startsWith('/')
|
||||
? `https://f7ee-202-8-116-13.ngrok-free.app${imageUrl}`
|
||||
: imageUrl,
|
||||
uri: getFullUrl(imageUrl),
|
||||
}}
|
||||
style={styles.image}
|
||||
// fallback source can be handled here if needed
|
||||
@ -99,13 +98,13 @@ export const ProductCard: React.FC<ProductCardProps> = ({
|
||||
)}
|
||||
</View>
|
||||
|
||||
<TouchableOpacity
|
||||
{/* <TouchableOpacity
|
||||
style={styles.addButton}
|
||||
onPress={onAddPress || onPress}
|
||||
activeOpacity={0.7}
|
||||
>
|
||||
<Text style={styles.addButtonText}>+ ADD</Text>
|
||||
</TouchableOpacity>
|
||||
</TouchableOpacity> */}
|
||||
</View>
|
||||
</TouchableOpacity>
|
||||
);
|
||||
|
||||
@ -1,37 +1,15 @@
|
||||
import { StyleSheet } from 'react-native';
|
||||
import { typography } from '@theme';
|
||||
|
||||
export const getStyles = (colors: any) => StyleSheet.create({
|
||||
export const getStyles = (colors: any) =>
|
||||
StyleSheet.create({
|
||||
container: {
|
||||
flex: 1,
|
||||
backgroundColor: colors.background,
|
||||
},
|
||||
content: {
|
||||
padding: 16,
|
||||
},
|
||||
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,
|
||||
paddingBottom: 24,
|
||||
},
|
||||
sectionTitle: {
|
||||
fontSize: typography.fontSize.md,
|
||||
@ -39,32 +17,61 @@ export const getStyles = (colors: any) => StyleSheet.create({
|
||||
color: colors.text,
|
||||
marginBottom: 12,
|
||||
},
|
||||
optionCard: {
|
||||
flexDirection: 'row',
|
||||
emptyState: {
|
||||
padding: 24,
|
||||
alignItems: 'center',
|
||||
padding: 16,
|
||||
justifyContent: 'center',
|
||||
},
|
||||
emptyStateText: {
|
||||
fontSize: typography.fontSize.sm,
|
||||
color: colors.textSecondary,
|
||||
},
|
||||
addressCard: {
|
||||
backgroundColor: colors.cardBg,
|
||||
borderRadius: 12,
|
||||
padding: 16,
|
||||
borderWidth: 1.5,
|
||||
borderColor: colors.border,
|
||||
marginBottom: 10,
|
||||
backgroundColor: colors.background,
|
||||
marginBottom: 12,
|
||||
},
|
||||
optionCardSelected: {
|
||||
addressCardSelected: {
|
||||
borderColor: colors.primary,
|
||||
backgroundColor: '#E8F5E9',
|
||||
},
|
||||
optionInfo: {
|
||||
flex: 1,
|
||||
addressCardHeader: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
marginBottom: 8,
|
||||
},
|
||||
optionTitle: {
|
||||
fontSize: typography.fontSize.md,
|
||||
fontWeight: typography.fontWeight.medium,
|
||||
labelBadge: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
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,
|
||||
marginBottom: 2,
|
||||
},
|
||||
optionSubtext: {
|
||||
fontSize: typography.fontSize.sm,
|
||||
color: colors.textSecondary,
|
||||
defaultBadge: {
|
||||
backgroundColor: colors.primary,
|
||||
borderRadius: 20,
|
||||
paddingVertical: 3,
|
||||
paddingHorizontal: 8,
|
||||
},
|
||||
defaultBadgeText: {
|
||||
fontSize: 10,
|
||||
fontWeight: typography.fontWeight.bold,
|
||||
color: '#fff',
|
||||
letterSpacing: 0.5,
|
||||
},
|
||||
radio: {
|
||||
width: 22,
|
||||
@ -74,6 +81,7 @@ export const getStyles = (colors: any) => StyleSheet.create({
|
||||
borderColor: colors.border,
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
marginLeft: 'auto',
|
||||
},
|
||||
radioSelected: {
|
||||
borderColor: colors.primary,
|
||||
@ -84,6 +92,36 @@ export const getStyles = (colors: any) => StyleSheet.create({
|
||||
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,
|
||||
|
||||
@ -1,18 +1,18 @@
|
||||
import React, { useState } from 'react';
|
||||
import {
|
||||
View,
|
||||
Text,
|
||||
TouchableOpacity,
|
||||
ScrollView,
|
||||
} from 'react-native';
|
||||
import React, { useState, useMemo } from 'react';
|
||||
import { View, Text, TouchableOpacity, ScrollView } from 'react-native';
|
||||
import { useNavigation } from '@react-navigation/native';
|
||||
import { StackNavigationProp } from '@react-navigation/stack';
|
||||
import { getStyles } from './checkoutAddressScreen.styles';
|
||||
import { Header, StepProgress, PrimaryButton } from '@components';
|
||||
import { useAppTheme } from '@theme';
|
||||
import { AppStackParamList } from '../../../navigation/appStack';
|
||||
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 = [
|
||||
{ key: 'address', label: 'Address' },
|
||||
@ -20,62 +20,118 @@ const CHECKOUT_STEPS = [
|
||||
{ key: 'confirm', label: 'Confirm' },
|
||||
];
|
||||
|
||||
const LABEL_ICON: Record<AddressLabel, string> = {
|
||||
Home: '🏠',
|
||||
Work: '💼',
|
||||
Other: '📍',
|
||||
};
|
||||
|
||||
export const CheckoutAddressScreen: React.FC = () => {
|
||||
const { colors } = useAppTheme();
|
||||
const styles = getStyles(colors);
|
||||
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 (
|
||||
<View style={styles.container}>
|
||||
<Header title="Checkout" onBack={() => navigation.goBack()} />
|
||||
<StepProgress steps={CHECKOUT_STEPS} activeStep={0} />
|
||||
<ScrollView contentContainerStyle={styles.content}>
|
||||
<View style={styles.addressCard}>
|
||||
<Text style={styles.addressLabel}>Deliver to</Text>
|
||||
<Text style={styles.addressText}>
|
||||
Koramangala 4th Block, Bengaluru, 560034
|
||||
<Text style={styles.sectionTitle}>Deliver to</Text>
|
||||
|
||||
{addresses.length === 0 && (
|
||||
<View style={styles.emptyState}>
|
||||
<Text style={styles.emptyStateText}>No saved addresses yet.</Text>
|
||||
</View>
|
||||
)}
|
||||
|
||||
{addresses.map(address => {
|
||||
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.addressSubtext}>Home</Text>
|
||||
<Text style={styles.labelBadgeText}>{address.label}</Text>
|
||||
</View>
|
||||
|
||||
<Text style={styles.sectionTitle}>Delivery Option</Text>
|
||||
<TouchableOpacity
|
||||
style={[
|
||||
styles.optionCard,
|
||||
deliveryOption === 'standard' && styles.optionCardSelected,
|
||||
]}
|
||||
onPress={() => setDeliveryOption('standard')}
|
||||
activeOpacity={0.7}
|
||||
{address.isDefault && (
|
||||
<View style={styles.defaultBadge}>
|
||||
<Text style={styles.defaultBadgeText}>DEFAULT</Text>
|
||||
</View>
|
||||
)}
|
||||
|
||||
<View
|
||||
style={[styles.radio, isSelected && styles.radioSelected]}
|
||||
>
|
||||
<View style={styles.optionInfo}>
|
||||
<Text style={styles.optionTitle}>Standard Delivery</Text>
|
||||
<Text style={styles.optionSubtext}>Free • 25-30 min</Text>
|
||||
{isSelected && <View style={styles.radioInner} />}
|
||||
</View>
|
||||
<View style={[styles.radio, deliveryOption === 'standard' && styles.radioSelected]}>
|
||||
{deliveryOption === 'standard' && <View style={styles.radioInner} />}
|
||||
</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
|
||||
style={[
|
||||
styles.optionCard,
|
||||
deliveryOption === 'express' && styles.optionCardSelected,
|
||||
]}
|
||||
onPress={() => setDeliveryOption('express')}
|
||||
style={styles.addAddressButton}
|
||||
activeOpacity={0.7}
|
||||
onPress={() => {
|
||||
// navigation.navigate('AddAddressScreen');
|
||||
}}
|
||||
>
|
||||
<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>
|
||||
<Text style={styles.addAddressButtonText}>+ Add New Address</Text>
|
||||
</TouchableOpacity>
|
||||
</ScrollView>
|
||||
<View style={styles.footer}>
|
||||
<PrimaryButton title="Continue to Payment" onPress={() => navigation.navigate('CheckoutPaymentScreen')} />
|
||||
<PrimaryButton
|
||||
title="Continue to Payment"
|
||||
onPress={handleContinue}
|
||||
disabled={!selectedAddressId}
|
||||
/>
|
||||
</View>
|
||||
</View>
|
||||
);
|
||||
|
||||
@ -1,13 +1,30 @@
|
||||
import React, { useState } from 'react';
|
||||
import { View, Text, ScrollView } from 'react-native';
|
||||
import { useNavigation } from '@react-navigation/native';
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import { View, Text, ScrollView, Alert } from 'react-native';
|
||||
import { RouteProp, useNavigation, useRoute } from '@react-navigation/native';
|
||||
import { StackNavigationProp } from '@react-navigation/stack';
|
||||
import { getStyles } from './checkoutPaymentScreen.styles';
|
||||
import { Header, StepProgress, PaymentOption, PrimaryButton } from '@components';
|
||||
import {
|
||||
Header,
|
||||
StepProgress,
|
||||
PaymentOption,
|
||||
PrimaryButton,
|
||||
} from '@components';
|
||||
import { useAppTheme } from '@theme';
|
||||
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 = [
|
||||
{ key: 'address', label: 'Address' },
|
||||
@ -25,8 +42,59 @@ const PAYMENT_METHODS = [
|
||||
export const CheckoutPaymentScreen: React.FC = () => {
|
||||
const { colors } = useAppTheme();
|
||||
const styles = getStyles(colors);
|
||||
const route = useRoute<CheckoutPaymentRouteProp>();
|
||||
const navigation = useNavigation<CheckoutPaymentNavProp>();
|
||||
const dispatch = useAppDispatch();
|
||||
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 (
|
||||
<View style={styles.container}>
|
||||
@ -34,18 +102,22 @@ export const CheckoutPaymentScreen: React.FC = () => {
|
||||
<StepProgress steps={CHECKOUT_STEPS} activeStep={1} />
|
||||
<ScrollView contentContainerStyle={styles.content}>
|
||||
<Text style={styles.sectionTitle}>Select Payment Method</Text>
|
||||
{PAYMENT_METHODS.map((method) => (
|
||||
{paymentMethods?.map(method => (
|
||||
<PaymentOption
|
||||
key={method.id}
|
||||
type={method.type}
|
||||
label={method.label}
|
||||
label={method.code}
|
||||
icon={method.iconUrl}
|
||||
isSelected={selectedMethod === method.id}
|
||||
onSelect={() => setSelectedMethod(method.id)}
|
||||
/>
|
||||
))}
|
||||
</ScrollView>
|
||||
<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>
|
||||
);
|
||||
|
||||
@ -1 +1,3 @@
|
||||
export * from './checkoutPaymentScreen';
|
||||
export * from './thunk';
|
||||
export * from './reducer';
|
||||
|
||||
94
app/features/screens/checkoutPaymentScreen/reducer.ts
Normal file
94
app/features/screens/checkoutPaymentScreen/reducer.ts
Normal 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;
|
||||
74
app/features/screens/checkoutPaymentScreen/thunk.ts
Normal file
74
app/features/screens/checkoutPaymentScreen/thunk.ts
Normal 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',
|
||||
);
|
||||
}
|
||||
});
|
||||
@ -22,7 +22,7 @@ import { useAppTheme } from '@theme';
|
||||
import { Product } from '../../../interfaces';
|
||||
import { AppStackParamList } from '../../../navigation/appStack';
|
||||
import { MainTabParamList } from '../../../navigation/mainTabNavigator';
|
||||
import { useAppDispatch, useAppSelector } from '@store';
|
||||
import { fetchCustomerDetails, useAppDispatch, useAppSelector } from '@store';
|
||||
import { getAllProductsThunk } from './thunk';
|
||||
|
||||
type NavProp = CompositeNavigationProp<
|
||||
@ -83,6 +83,7 @@ export const HomeScreen: React.FC = () => {
|
||||
|
||||
useEffect(() => {
|
||||
dispatch(getAllProductsThunk());
|
||||
dispatch(fetchCustomerDetails());
|
||||
}, [dispatch]);
|
||||
|
||||
const filteredProducts =
|
||||
|
||||
@ -20,3 +20,4 @@ export * from './offersScreen';
|
||||
export * from './helpSupportScreen';
|
||||
export * from './accountScreen';
|
||||
export * from './writeReviewScreen';
|
||||
export * from './orderDetailsScreen';
|
||||
|
||||
@ -1,11 +1,9 @@
|
||||
import React, { useState } from 'react';
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import { View, Text, FlatList, TouchableOpacity } from 'react-native';
|
||||
import {
|
||||
View,
|
||||
Text,
|
||||
FlatList,
|
||||
TouchableOpacity,
|
||||
} from 'react-native';
|
||||
import { useNavigation, CompositeNavigationProp } from '@react-navigation/native';
|
||||
useNavigation,
|
||||
CompositeNavigationProp,
|
||||
} from '@react-navigation/native';
|
||||
import { BottomTabNavigationProp } from '@react-navigation/bottom-tabs';
|
||||
import { StackNavigationProp } from '@react-navigation/stack';
|
||||
import { getStyles } from './myOrdersScreen.styles';
|
||||
@ -13,6 +11,9 @@ import { Header, OrderHistoryCard } from '@components';
|
||||
import { useAppTheme } from '@theme';
|
||||
import { AppStackParamList } from '../../../navigation/appStack';
|
||||
import { MainTabParamList } from '../../../navigation/mainTabNavigator';
|
||||
import { RootState, useAppDispatch, useAppSelector } from '@store';
|
||||
import { getOrderHistoryThunk } from '../checkoutPaymentScreen';
|
||||
import { formatDate } from '@utils/helper';
|
||||
|
||||
type MyOrdersNavProp = CompositeNavigationProp<
|
||||
BottomTabNavigationProp<MainTabParamList, 'MyOrdersScreen'>,
|
||||
@ -21,9 +22,30 @@ type MyOrdersNavProp = CompositeNavigationProp<
|
||||
|
||||
const TABS = ['All', 'Ongoing', 'Completed'];
|
||||
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-108239', providerName: 'Burger Barn', providerImage: '', orderDate: '27 Jun 2026', status: 'Delivered', total: 449 },
|
||||
{
|
||||
id: 'ORD-591283',
|
||||
providerName: 'Pizza Planet',
|
||||
providerImage: '',
|
||||
orderDate: '29 Jun 2026',
|
||||
status: 'Delivered',
|
||||
total: 599,
|
||||
},
|
||||
{
|
||||
id: 'ORD-771239',
|
||||
providerName: 'Fresh Mart',
|
||||
providerImage: '',
|
||||
orderDate: '28 Jun 2026',
|
||||
status: 'Ongoing',
|
||||
total: 349,
|
||||
},
|
||||
{
|
||||
id: 'ORD-108239',
|
||||
providerName: 'Burger Barn',
|
||||
providerImage: '',
|
||||
orderDate: '27 Jun 2026',
|
||||
status: 'Delivered',
|
||||
total: 449,
|
||||
},
|
||||
];
|
||||
|
||||
export const MyOrdersScreen: React.FC = () => {
|
||||
@ -31,24 +53,42 @@ export const MyOrdersScreen: React.FC = () => {
|
||||
const styles = getStyles(colors);
|
||||
const navigation = useNavigation<MyOrdersNavProp>();
|
||||
const [activeTab, setActiveTab] = useState('All');
|
||||
const dispatch = useAppDispatch();
|
||||
const { orderHistory } = useAppSelector(
|
||||
(state: RootState) => state.paymentMethods,
|
||||
);
|
||||
console.log(orderHistory);
|
||||
|
||||
const filteredOrders = activeTab === 'All'
|
||||
? mockOrders
|
||||
useEffect(() => {
|
||||
dispatch(getOrderHistoryThunk());
|
||||
}, []);
|
||||
|
||||
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'
|
||||
? mockOrders.filter((o) => o.status === 'Ongoing')
|
||||
: mockOrders.filter((o) => o.status === 'Delivered');
|
||||
? orders.filter(o => ONGOING_STATUSES.includes(o.status))
|
||||
: orders.filter(o => COMPLETED_STATUSES.includes(o.status));
|
||||
|
||||
return (
|
||||
<View style={styles.container}>
|
||||
<Header title="My Orders" />
|
||||
<View style={styles.tabRow}>
|
||||
{TABS.map((tab) => (
|
||||
{TABS.map(tab => (
|
||||
<TouchableOpacity
|
||||
key={tab}
|
||||
style={[
|
||||
styles.tab,
|
||||
activeTab === tab && styles.tabActive,
|
||||
]}
|
||||
style={[styles.tab, activeTab === tab && styles.tabActive]}
|
||||
onPress={() => setActiveTab(tab)}
|
||||
activeOpacity={0.7}
|
||||
>
|
||||
@ -65,25 +105,36 @@ export const MyOrdersScreen: React.FC = () => {
|
||||
</View>
|
||||
<FlatList
|
||||
data={filteredOrders}
|
||||
keyExtractor={(item) => item.id}
|
||||
keyExtractor={item => item.id}
|
||||
renderItem={({ item }) => (
|
||||
<OrderHistoryCard
|
||||
providerName={item.providerName}
|
||||
providerImage={item.providerImage}
|
||||
orderDate={item.orderDate}
|
||||
providerName={item?.merchant?.name || ''}
|
||||
providerImage={item?.merchant?.imageUrl || ''}
|
||||
orderDate={formatDate(item?.createdAt)}
|
||||
status={item.status}
|
||||
total={item.total}
|
||||
total={Number(item?.totalAmount) || 0}
|
||||
items={item.orderItems}
|
||||
onReorder={() => {
|
||||
navigation.navigate('ProviderDetailsScreen', {
|
||||
providerId: 'p1',
|
||||
providerName: item.providerName,
|
||||
providerName: item?.merchant?.name || '',
|
||||
});
|
||||
}}
|
||||
onDetails={() => {
|
||||
if (item.status === 'Ongoing') {
|
||||
navigation.navigate('OrderTrackingScreen', { orderId: item.id });
|
||||
if (
|
||||
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 {
|
||||
navigation.navigate('OrderDeliveredScreen', { orderId: item.id });
|
||||
navigation.navigate('OrderDeliveredScreen', {
|
||||
orderId: item.id,
|
||||
});
|
||||
}
|
||||
}}
|
||||
/>
|
||||
|
||||
1
app/features/screens/orderDetailsScreen/index.ts
Normal file
1
app/features/screens/orderDetailsScreen/index.ts
Normal file
@ -0,0 +1 @@
|
||||
export * from './orderDetailsScreen';
|
||||
@ -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,
|
||||
},
|
||||
});
|
||||
191
app/features/screens/orderDetailsScreen/orderDetailsScreen.tsx
Normal file
191
app/features/screens/orderDetailsScreen/orderDetailsScreen.tsx
Normal 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;
|
||||
@ -35,7 +35,7 @@ export interface User {
|
||||
gender: Gender | null;
|
||||
dateOfBirth: string | null;
|
||||
role: Role;
|
||||
merchants: Merchant[];
|
||||
// merchants: Merchant[];
|
||||
}
|
||||
|
||||
export interface Role {
|
||||
@ -50,7 +50,8 @@ export interface CategoryPreference {
|
||||
}
|
||||
|
||||
export interface Merchant {
|
||||
// Extend when backend starts returning data
|
||||
name: string;
|
||||
imageUrl: string | null;
|
||||
}
|
||||
|
||||
export type RoleEnum = 'CUSTOMER';
|
||||
|
||||
49
app/interfaces/customerProfile.ts
Normal file
49
app/interfaces/customerProfile.ts
Normal 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';
|
||||
@ -43,37 +43,37 @@ export interface CartItem {
|
||||
quantity: number;
|
||||
}
|
||||
|
||||
export type OrderStatus =
|
||||
| 'placed'
|
||||
| 'confirmed'
|
||||
| 'dispatched'
|
||||
| 'delivered'
|
||||
| 'cancelled';
|
||||
// export type OrderStatus =
|
||||
// | 'placed'
|
||||
// | 'confirmed'
|
||||
// | 'dispatched'
|
||||
// | 'delivered'
|
||||
// | 'cancelled';
|
||||
|
||||
export interface Order {
|
||||
id: string;
|
||||
providerId: string;
|
||||
providerName: string;
|
||||
providerImage: string;
|
||||
items: CartItem[];
|
||||
status: OrderStatus;
|
||||
total: number;
|
||||
deliveryFee: number;
|
||||
platformFee: number;
|
||||
discount: number;
|
||||
couponCode?: string;
|
||||
createdAt: string;
|
||||
estimatedDelivery: string;
|
||||
deliveryAddress: Location;
|
||||
}
|
||||
// export interface Order {
|
||||
// id: string;
|
||||
// providerId: string;
|
||||
// providerName: string;
|
||||
// providerImage: string;
|
||||
// items: CartItem[];
|
||||
// status: OrderStatus;
|
||||
// total: number;
|
||||
// deliveryFee: number;
|
||||
// platformFee: number;
|
||||
// discount: number;
|
||||
// couponCode?: string;
|
||||
// createdAt: string;
|
||||
// estimatedDelivery: string;
|
||||
// deliveryAddress: Location;
|
||||
// }
|
||||
|
||||
export interface TrackingStep {
|
||||
key: OrderStatus;
|
||||
label: string;
|
||||
timestamp?: string;
|
||||
isCompleted: boolean;
|
||||
isActive: boolean;
|
||||
}
|
||||
// export interface TrackingStep {
|
||||
// key: OrderStatus;
|
||||
// label: string;
|
||||
// timestamp?: string;
|
||||
// isCompleted: boolean;
|
||||
// isActive: boolean;
|
||||
// }
|
||||
|
||||
export interface DeliveryAgent {
|
||||
name: string;
|
||||
@ -93,3 +93,6 @@ export * from './auth';
|
||||
export * from './onboard';
|
||||
export * from './product';
|
||||
export * from './cart';
|
||||
export * from './paymentMethods';
|
||||
export * from './customerProfile';
|
||||
export * from './order';
|
||||
|
||||
187
app/interfaces/order.ts
Normal file
187
app/interfaces/order.ts
Normal 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';
|
||||
12
app/interfaces/paymentMethods.ts
Normal file
12
app/interfaces/paymentMethods.ts
Normal 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[];
|
||||
@ -14,6 +14,7 @@ import {
|
||||
OrderDeliveredScreen,
|
||||
HelpSupportScreen,
|
||||
WriteReviewScreen,
|
||||
OrderDetailsScreen,
|
||||
} from '@features/screens';
|
||||
|
||||
export type AppStackParamList = {
|
||||
@ -22,13 +23,14 @@ export type AppStackParamList = {
|
||||
ProviderDetailsScreen: { providerId: string; providerName?: string };
|
||||
CartScreen: undefined;
|
||||
CheckoutAddressScreen: undefined;
|
||||
CheckoutPaymentScreen: undefined;
|
||||
CheckoutPaymentScreen: { selectedAddressId?: string } | undefined;
|
||||
OrderConfirmedScreen: { orderId?: string } | undefined;
|
||||
OrderTrackingScreen: { orderId?: string } | undefined;
|
||||
LiveTrackingScreen: { orderId?: string } | undefined;
|
||||
OrderDeliveredScreen: { orderId?: string } | undefined;
|
||||
HelpSupportScreen: undefined;
|
||||
WriteReviewScreen: { productId: string } | undefined;
|
||||
OrderDetailsScreen: { orderId: string } | undefined;
|
||||
};
|
||||
|
||||
const Stack = createStackNavigator<AppStackParamList>();
|
||||
@ -66,6 +68,10 @@ export const AppStack: React.FC = () => {
|
||||
/>
|
||||
<Stack.Screen name="HelpSupportScreen" component={HelpSupportScreen} />
|
||||
<Stack.Screen name="WriteReviewScreen" component={WriteReviewScreen} />
|
||||
<Stack.Screen
|
||||
name="OrderDetailsScreen"
|
||||
component={OrderDetailsScreen}
|
||||
/>
|
||||
</Stack.Navigator>
|
||||
);
|
||||
};
|
||||
|
||||
@ -15,7 +15,7 @@ const STORAGE_KEYS = {
|
||||
} as const;
|
||||
|
||||
// ─── 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 ───────────────────────────────────────────────────────────
|
||||
export const tokenManager = {
|
||||
|
||||
2
app/store/commonreducers/customerProfile/index.ts
Normal file
2
app/store/commonreducers/customerProfile/index.ts
Normal file
@ -0,0 +1,2 @@
|
||||
export * from './thunk';
|
||||
export * from './reducer';
|
||||
32
app/store/commonreducers/customerProfile/reducer.ts
Normal file
32
app/store/commonreducers/customerProfile/reducer.ts
Normal 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;
|
||||
17
app/store/commonreducers/customerProfile/thunk.ts
Normal file
17
app/store/commonreducers/customerProfile/thunk.ts
Normal 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);
|
||||
}
|
||||
},
|
||||
);
|
||||
@ -1,3 +1,4 @@
|
||||
export * from './auth';
|
||||
export * from './cart';
|
||||
export * from './order';
|
||||
export * from './customerProfile';
|
||||
|
||||
@ -8,6 +8,8 @@ import completeProfileReducer from '@features/screens/completeProfileScreen/redu
|
||||
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({
|
||||
auth: authReducer,
|
||||
@ -18,6 +20,8 @@ const rootReducer = combineReducers({
|
||||
completeProfile: completeProfileReducer,
|
||||
home: homeReducer,
|
||||
providerDetails: providerDetailsReducer,
|
||||
paymentMethods: paymentMethodsReducer,
|
||||
customerProfile: customerProfileReducer,
|
||||
});
|
||||
|
||||
export type RootState = ReturnType<typeof rootReducer>;
|
||||
|
||||
@ -1,5 +1,31 @@
|
||||
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 '';
|
||||
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',
|
||||
});
|
||||
};
|
||||
|
||||
@ -27,6 +27,7 @@
|
||||
"react-native-safe-area-context": "^5.8.0",
|
||||
"react-native-screens": "^4.25.2",
|
||||
"react-native-svg": "^15.15.5",
|
||||
"react-native-uuid": "^2.0.4",
|
||||
"react-native-worklets": "^0.10.0",
|
||||
"react-redux": "^9.3.0",
|
||||
"reactotron-react-native": "^5.2.0",
|
||||
|
||||
@ -5858,6 +5858,11 @@ react-native-svg@^15.15.5:
|
||||
css-select "^5.1.0"
|
||||
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:
|
||||
version "0.10.1"
|
||||
resolved "https://registry.yarnpkg.com/react-native-worklets/-/react-native-worklets-0.10.1.tgz#7a3038a3b5ec66cab030a00e1568b6599696f5f2"
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user