From d7c2254d2e8f97e83e626528925ea5e5209f4149 Mon Sep 17 00:00:00 2001 From: Tamojit Biswas Date: Wed, 8 Jul 2026 18:00:00 +0530 Subject: [PATCH] feat: implement checkout address selection and customer profile data fetching --- app/api/customerDetailsApi.ts | 6 + app/api/deliveryApi.ts | 15 +- app/api/index.ts | 3 + app/api/orderApi.ts | 14 + app/api/paymentMethodsApi.ts | 6 + .../OrderStatusTimeline.props.ts | 7 + .../OrderStatusTimeline.styles.ts | 317 +++++++++++++++ .../OrderStatusTimeline.tsx | 377 ++++++++++++++++++ app/components/OrderStatusTimeline/index.ts | 2 + app/components/index.ts | 1 + .../orderHistoryCard.props.ts | 3 + .../orderHistoryCard.styles.ts | 34 +- .../orderHistoryCard/orderHistoryCard.tsx | 15 + .../paymentOption/paymentOption.props.ts | 2 +- .../paymentOption/paymentOption.styles.ts | 92 ++--- .../paymentOption/paymentOption.tsx | 7 +- app/components/productCard/productCard.tsx | 9 +- .../checkoutAddressScreen.styles.ts | 216 +++++----- .../checkoutAddressScreen.tsx | 148 ++++--- .../checkoutPaymentScreen.tsx | 90 ++++- .../screens/checkoutPaymentScreen/index.ts | 2 + .../screens/checkoutPaymentScreen/reducer.ts | 94 +++++ .../screens/checkoutPaymentScreen/thunk.ts | 74 ++++ .../screens/homeScreen/homeScreen.tsx | 3 +- app/features/screens/index.ts | 1 + .../screens/myOrdersScreen/myOrdersScreen.tsx | 109 +++-- .../screens/orderDetailsScreen/index.ts | 1 + .../orderDetailsScreen.styles.ts | 194 +++++++++ .../orderDetailsScreen/orderDetailsScreen.tsx | 191 +++++++++ app/interfaces/auth.ts | 5 +- app/interfaces/customerProfile.ts | 49 +++ app/interfaces/index.ts | 61 +-- app/interfaces/order.ts | 187 +++++++++ app/interfaces/paymentMethods.ts | 12 + app/navigation/appStack.tsx | 8 +- app/services/apiClient.ts | 2 +- .../commonreducers/customerProfile/index.ts | 2 + .../commonreducers/customerProfile/reducer.ts | 32 ++ .../commonreducers/customerProfile/thunk.ts | 17 + app/store/commonreducers/index.ts | 1 + app/store/rootReducer.ts | 4 + app/utils/helper.ts | 28 +- package.json | 1 + yarn.lock | 5 + 44 files changed, 2178 insertions(+), 269 deletions(-) create mode 100644 app/api/customerDetailsApi.ts create mode 100644 app/api/orderApi.ts create mode 100644 app/api/paymentMethodsApi.ts create mode 100644 app/components/OrderStatusTimeline/OrderStatusTimeline.props.ts create mode 100644 app/components/OrderStatusTimeline/OrderStatusTimeline.styles.ts create mode 100644 app/components/OrderStatusTimeline/OrderStatusTimeline.tsx create mode 100644 app/components/OrderStatusTimeline/index.ts create mode 100644 app/features/screens/checkoutPaymentScreen/reducer.ts create mode 100644 app/features/screens/checkoutPaymentScreen/thunk.ts create mode 100644 app/features/screens/orderDetailsScreen/index.ts create mode 100644 app/features/screens/orderDetailsScreen/orderDetailsScreen.styles.ts create mode 100644 app/features/screens/orderDetailsScreen/orderDetailsScreen.tsx create mode 100644 app/interfaces/customerProfile.ts create mode 100644 app/interfaces/order.ts create mode 100644 app/interfaces/paymentMethods.ts create mode 100644 app/store/commonreducers/customerProfile/index.ts create mode 100644 app/store/commonreducers/customerProfile/reducer.ts create mode 100644 app/store/commonreducers/customerProfile/thunk.ts diff --git a/app/api/customerDetailsApi.ts b/app/api/customerDetailsApi.ts new file mode 100644 index 0000000..f4e2b7b --- /dev/null +++ b/app/api/customerDetailsApi.ts @@ -0,0 +1,6 @@ +import { CustomerResponse } from '@interfaces'; +import { apiClient } from '@services'; + +export const getCustomerDetails = async () => { + return await apiClient.get('/customers/profile'); +}; diff --git a/app/api/deliveryApi.ts b/app/api/deliveryApi.ts index a73193a..182dcb0 100644 --- a/app/api/deliveryApi.ts +++ b/app/api/deliveryApi.ts @@ -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('/providers'); +export const getProvidersApi = () => apiClient.get('/providers'); export const getProviderCatalogApi = (providerId: string) => apiClient.get(`/providers/${providerId}/catalog`); @@ -19,8 +24,8 @@ export const getProviderCatalogApi = (providerId: string) => export const searchProvidersApi = (query: string) => apiClient.get(`/search?q=${query}`); -export const placeOrderApi = (orderData: Partial) => - apiClient.post('/orders', orderData); +// export const placeOrderApi = (orderData: Partial) => +// apiClient.post('/orders', orderData); export const getDeliveryAgentApi = () => apiClient.get('/delivery/agent'); diff --git a/app/api/index.ts b/app/api/index.ts index d4b4695..642c92b 100644 --- a/app/api/index.ts +++ b/app/api/index.ts @@ -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'; diff --git a/app/api/orderApi.ts b/app/api/orderApi.ts new file mode 100644 index 0000000..eaa5f6c --- /dev/null +++ b/app/api/orderApi.ts @@ -0,0 +1,14 @@ +import { apiClient } from '@services'; +import { Order, OrderRequest, PlaceOrderResponse } from '@interfaces'; + +export const placeOrderApi = async (payload: OrderRequest) => { + return await apiClient.post('/orders', payload); +}; + +export const getOrderHistoryApi = async () => { + return await apiClient.get('/orders'); +}; + +export const getOrderByIdApi = async (orderId: string) => { + return await apiClient.get(`/orders/${orderId}`); +}; diff --git a/app/api/paymentMethodsApi.ts b/app/api/paymentMethodsApi.ts new file mode 100644 index 0000000..e27ef59 --- /dev/null +++ b/app/api/paymentMethodsApi.ts @@ -0,0 +1,6 @@ +import { PaymentMethodsResponse } from '@interfaces'; +import { apiClient } from '@services'; + +export const getAllPaymentMethodsApi = async () => { + return await apiClient.get('/payments/options'); +}; diff --git a/app/components/OrderStatusTimeline/OrderStatusTimeline.props.ts b/app/components/OrderStatusTimeline/OrderStatusTimeline.props.ts new file mode 100644 index 0000000..c11397e --- /dev/null +++ b/app/components/OrderStatusTimeline/OrderStatusTimeline.props.ts @@ -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 +} diff --git a/app/components/OrderStatusTimeline/OrderStatusTimeline.styles.ts b/app/components/OrderStatusTimeline/OrderStatusTimeline.styles.ts new file mode 100644 index 0000000..7abd781 --- /dev/null +++ b/app/components/OrderStatusTimeline/OrderStatusTimeline.styles.ts @@ -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, + }, + }); diff --git a/app/components/OrderStatusTimeline/OrderStatusTimeline.tsx b/app/components/OrderStatusTimeline/OrderStatusTimeline.tsx new file mode 100644 index 0000000..81c782c --- /dev/null +++ b/app/components/OrderStatusTimeline/OrderStatusTimeline.tsx @@ -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 = { + 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 = ({ + 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>((acc, t) => { + acc[t.status] = t.time; + return acc; + }, {}); + + const details = STATUS_DETAILS[currentStatus] || STATUS_DETAILS.PENDING; + + if (currentStatus === 'CANCELLED') { + return ( + + {/* Cancelled Status Banner */} + + + + + + + {details.title} + + {details.desc} + + + + {/* Collapsible detailed view */} + + + + {expanded ? 'Hide Details' : 'View Detailed History'} + + + {expanded ? '▲' : '▼'} + + + + + {expanded && ( + + + + + + + + Order Cancelled + + {trackingMap['CANCELLED'] && ( + + {formatDate(trackingMap['CANCELLED'])} at{' '} + {formatTime(trackingMap['CANCELLED'])} + + )} + + + + )} + + ); + } + + 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 ( + + {/* Current Status Header Banner */} + + + {details.icon} + + + {details.title} + {details.desc} + + + + {/* Horizontal Progress Stepper */} + + + + + + {steps.map((step, idx) => { + const isCompleted = idx < activeStep; + const isActive = idx === activeStep; + + return ( + + + {isCompleted ? ( + + ) : isActive ? ( + + ) : null} + + + {step.label} + + + ); + })} + + + + {/* Delivery Partner Details (if driver assigned) */} + {showDriver && ( + + + + RS + + + Your Delivery Hero + Rahul Sharma + + KA-01-AB-1234 • Honda Activa + + + + ⭐ 4.8 + + + + + 📞 + Call Partner + + + 💬 + Chat + + + + )} + + {/* View Detailed History Toggle */} + + + + {expanded ? 'Hide Details' : 'View Detailed History'} + + {expanded ? '▲' : '▼'} + + + + {/* Detailed Vertical Logs */} + {expanded && ( + + {STATUS_FLOW.map((status, index) => { + const isCompleted = index <= currentIndex; + const isLast = index === STATUS_FLOW.length - 1; + const time = trackingMap[status]; + + return ( + + + + {isCompleted && } + + {!isLast && ( + + )} + + + + {STATUS_LABEL[status]} + + {time && ( + + {formatDate(time)} at {formatTime(time)} + + )} + + + ); + })} + + )} + + ); +}; diff --git a/app/components/OrderStatusTimeline/index.ts b/app/components/OrderStatusTimeline/index.ts new file mode 100644 index 0000000..94f6629 --- /dev/null +++ b/app/components/OrderStatusTimeline/index.ts @@ -0,0 +1,2 @@ +export * from './OrderStatusTimeline'; +export type * from './OrderStatusTimeline.props'; diff --git a/app/components/index.ts b/app/components/index.ts index a2babe1..84244e6 100644 --- a/app/components/index.ts +++ b/app/components/index.ts @@ -13,3 +13,4 @@ export * from './categoryChip'; export * from './ratingStars'; export * from './orderHistoryCard'; export * from './paymentOption'; +export * from './OrderStatusTimeline'; diff --git a/app/components/orderHistoryCard/orderHistoryCard.props.ts b/app/components/orderHistoryCard/orderHistoryCard.props.ts index 196c1ca..2161c08 100644 --- a/app/components/orderHistoryCard/orderHistoryCard.props.ts +++ b/app/components/orderHistoryCard/orderHistoryCard.props.ts @@ -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[]; } diff --git a/app/components/orderHistoryCard/orderHistoryCard.styles.ts b/app/components/orderHistoryCard/orderHistoryCard.styles.ts index a8bb88a..bb95871 100644 --- a/app/components/orderHistoryCard/orderHistoryCard.styles.ts +++ b/app/components/orderHistoryCard/orderHistoryCard.styles.ts @@ -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, }, diff --git a/app/components/orderHistoryCard/orderHistoryCard.tsx b/app/components/orderHistoryCard/orderHistoryCard.tsx index 098a362..e37a9b8 100644 --- a/app/components/orderHistoryCard/orderHistoryCard.tsx +++ b/app/components/orderHistoryCard/orderHistoryCard.tsx @@ -12,6 +12,7 @@ export const OrderHistoryCard: React.FC = ({ total, onReorder, onDetails, + items, }) => { const { colors } = useAppTheme(); const styles = getStyles(colors); @@ -30,6 +31,20 @@ export const OrderHistoryCard: React.FC = ({ {status} + {items && items.length > 0 && ( + + {items.map((item, index) => ( + + + {item.quantity}x + + + {item.name} + + + ))} + + )} ₹{total} diff --git a/app/components/paymentOption/paymentOption.props.ts b/app/components/paymentOption/paymentOption.props.ts index 1b993ff..8b3e634 100644 --- a/app/components/paymentOption/paymentOption.props.ts +++ b/app/components/paymentOption/paymentOption.props.ts @@ -1,6 +1,6 @@ export interface PaymentOptionProps { - type: string; label: string; isSelected: boolean; + icon?: string; onSelect: () => void; } diff --git a/app/components/paymentOption/paymentOption.styles.ts b/app/components/paymentOption/paymentOption.styles.ts index f44a9fd..7a1783f 100644 --- a/app/components/paymentOption/paymentOption.styles.ts +++ b/app/components/paymentOption/paymentOption.styles.ts @@ -1,48 +1,50 @@ import { StyleSheet } from 'react-native'; import { typography } from '@theme'; -export const getStyles = (colors: any) => StyleSheet.create({ - container: { - flexDirection: 'row', - alignItems: 'center', - paddingVertical: 14, - paddingHorizontal: 16, - borderWidth: 1.5, - borderColor: colors.border, - borderRadius: 12, - marginBottom: 10, - backgroundColor: colors.background, - }, - selected: { - borderColor: colors.primary, - backgroundColor: '#E8F5E9', - }, - icon: { - fontSize: 24, - marginRight: 12, - }, - label: { - flex: 1, - fontSize: typography.fontSize.md, - fontWeight: typography.fontWeight.medium, - color: colors.text, - }, - radio: { - width: 22, - height: 22, - borderRadius: 11, - borderWidth: 2, - borderColor: colors.border, - justifyContent: 'center', - alignItems: 'center', - }, - radioSelected: { - borderColor: colors.primary, - }, - radioInner: { - width: 12, - height: 12, - borderRadius: 6, - backgroundColor: colors.primary, - }, -}); +export const getStyles = (colors: any) => + StyleSheet.create({ + container: { + flexDirection: 'row', + alignItems: 'center', + paddingVertical: 14, + paddingHorizontal: 16, + borderWidth: 1.5, + borderColor: colors.border, + borderRadius: 12, + marginBottom: 10, + backgroundColor: colors.background, + }, + selected: { + borderColor: colors.primary, + backgroundColor: '#E8F5E9', + }, + icon: { + width: 24, + height: 24, + marginRight: 12, + }, + label: { + flex: 1, + fontSize: typography.fontSize.md, + fontWeight: typography.fontWeight.medium, + color: colors.text, + }, + radio: { + width: 22, + height: 22, + borderRadius: 11, + borderWidth: 2, + borderColor: colors.border, + justifyContent: 'center', + alignItems: 'center', + }, + radioSelected: { + borderColor: colors.primary, + }, + radioInner: { + width: 12, + height: 12, + borderRadius: 6, + backgroundColor: colors.primary, + }, + }); diff --git a/app/components/paymentOption/paymentOption.tsx b/app/components/paymentOption/paymentOption.tsx index 5fea9a2..9593521 100644 --- a/app/components/paymentOption/paymentOption.tsx +++ b/app/components/paymentOption/paymentOption.tsx @@ -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 = ({ - type, label, isSelected, + icon, onSelect, }) => { const { colors } = useAppTheme(); const styles = getStyles(colors); + // console.log(icon); return ( = ({ onPress={onSelect} activeOpacity={0.7} > - {type === 'UPI' ? '📱' : type === 'Card' ? '💳' : type === 'Wallet' ? '👛' : '💵'} + {label} {isSelected && } diff --git a/app/components/productCard/productCard.tsx b/app/components/productCard/productCard.tsx index 7935fca..aca900d 100644 --- a/app/components/productCard/productCard.tsx +++ b/app/components/productCard/productCard.tsx @@ -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 = ({ = ({ )} - + ADD - + */} ); diff --git a/app/features/screens/checkoutAddressScreen/checkoutAddressScreen.styles.ts b/app/features/screens/checkoutAddressScreen/checkoutAddressScreen.styles.ts index 29caf10..5a5922e 100644 --- a/app/features/screens/checkoutAddressScreen/checkoutAddressScreen.styles.ts +++ b/app/features/screens/checkoutAddressScreen/checkoutAddressScreen.styles.ts @@ -1,92 +1,130 @@ import { StyleSheet } from 'react-native'; import { typography } from '@theme'; -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, - }, - sectionTitle: { - fontSize: typography.fontSize.md, - fontWeight: typography.fontWeight.semibold, - color: colors.text, - marginBottom: 12, - }, - optionCard: { - flexDirection: 'row', - alignItems: 'center', - padding: 16, - borderRadius: 12, - borderWidth: 1.5, - borderColor: colors.border, - marginBottom: 10, - backgroundColor: colors.background, - }, - optionCardSelected: { - borderColor: colors.primary, - backgroundColor: '#E8F5E9', - }, - optionInfo: { - flex: 1, - }, - optionTitle: { - fontSize: typography.fontSize.md, - fontWeight: typography.fontWeight.medium, - color: colors.text, - marginBottom: 2, - }, - optionSubtext: { - fontSize: typography.fontSize.sm, - color: colors.textSecondary, - }, - radio: { - width: 22, - height: 22, - borderRadius: 11, - borderWidth: 2, - borderColor: colors.border, - justifyContent: 'center', - alignItems: 'center', - }, - radioSelected: { - borderColor: colors.primary, - }, - radioInner: { - width: 12, - height: 12, - borderRadius: 6, - backgroundColor: colors.primary, - }, - footer: { - padding: 16, - borderTopWidth: 1, - borderTopColor: colors.border, - }, -}); +export const getStyles = (colors: any) => + StyleSheet.create({ + container: { + flex: 1, + backgroundColor: colors.background, + }, + content: { + padding: 16, + paddingBottom: 24, + }, + sectionTitle: { + fontSize: typography.fontSize.md, + fontWeight: typography.fontWeight.semibold, + color: colors.text, + marginBottom: 12, + }, + emptyState: { + padding: 24, + alignItems: 'center', + 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: 12, + }, + addressCardSelected: { + borderColor: colors.primary, + backgroundColor: '#E8F5E9', + }, + addressCardHeader: { + flexDirection: 'row', + alignItems: 'center', + marginBottom: 8, + }, + 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, + }, + 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, + height: 22, + borderRadius: 11, + borderWidth: 2, + borderColor: colors.border, + justifyContent: 'center', + alignItems: 'center', + marginLeft: 'auto', + }, + radioSelected: { + borderColor: colors.primary, + }, + radioInner: { + 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, + }, + }); diff --git a/app/features/screens/checkoutAddressScreen/checkoutAddressScreen.tsx b/app/features/screens/checkoutAddressScreen/checkoutAddressScreen.tsx index 4747356..28f6651 100644 --- a/app/features/screens/checkoutAddressScreen/checkoutAddressScreen.tsx +++ b/app/features/screens/checkoutAddressScreen/checkoutAddressScreen.tsx @@ -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; +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 = { + Home: '🏠', + Work: '💼', + Other: '📍', +}; + export const CheckoutAddressScreen: React.FC = () => { const { colors } = useAppTheme(); const styles = getStyles(colors); const navigation = useNavigation(); - 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( + defaultAddressId, + ); + + const handleContinue = () => { + if (!selectedAddressId) return; + navigation.navigate('CheckoutPaymentScreen', { + selectedAddressId, + }); + }; return (
navigation.goBack()} /> - - Deliver to - - Koramangala 4th Block, Bengaluru, 560034 - - Home - + Deliver to - Delivery Option - setDeliveryOption('standard')} - activeOpacity={0.7} - > - - Standard Delivery - Free • 25-30 min + {addresses.length === 0 && ( + + No saved addresses yet. - - {deliveryOption === 'standard' && } - - + )} + + {addresses.map(address => { + const isSelected = selectedAddressId === address.id; + return ( + setSelectedAddressId(address.id)} + > + + + + {LABEL_ICON[address.label] ?? '📍'} + + {address.label} + + + {address.isDefault && ( + + DEFAULT + + )} + + + {isSelected && } + + + + + {address.houseNumber ? `${address.houseNumber}, ` : ''} + {address.addressLine1} + + + {!!address.landmark && ( + + Landmark: {address.landmark} + + )} + + + {address.city}, {address.state} - {address.postalCode} + + + 📞 {address.phone} + + ); + })} setDeliveryOption('express')} + style={styles.addAddressButton} activeOpacity={0.7} + onPress={() => { + // navigation.navigate('AddAddressScreen'); + }} > - - Express Delivery - ₹40 • 10-15 min - - - {deliveryOption === 'express' && } - + + Add New Address - navigation.navigate('CheckoutPaymentScreen')} /> + ); diff --git a/app/features/screens/checkoutPaymentScreen/checkoutPaymentScreen.tsx b/app/features/screens/checkoutPaymentScreen/checkoutPaymentScreen.tsx index a9c0565..1ca579d 100644 --- a/app/features/screens/checkoutPaymentScreen/checkoutPaymentScreen.tsx +++ b/app/features/screens/checkoutPaymentScreen/checkoutPaymentScreen.tsx @@ -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; +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(); const navigation = useNavigation(); + 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 ( @@ -34,18 +102,22 @@ export const CheckoutPaymentScreen: React.FC = () => { Select Payment Method - {PAYMENT_METHODS.map((method) => ( + {paymentMethods?.map(method => ( setSelectedMethod(method.id)} /> ))} - navigation.navigate('OrderConfirmedScreen', { orderId: 'ORD-' + Math.floor(Math.random() * 900000 + 100000) })} /> + ); diff --git a/app/features/screens/checkoutPaymentScreen/index.ts b/app/features/screens/checkoutPaymentScreen/index.ts index 6d7df67..83d998a 100644 --- a/app/features/screens/checkoutPaymentScreen/index.ts +++ b/app/features/screens/checkoutPaymentScreen/index.ts @@ -1 +1,3 @@ export * from './checkoutPaymentScreen'; +export * from './thunk'; +export * from './reducer'; diff --git a/app/features/screens/checkoutPaymentScreen/reducer.ts b/app/features/screens/checkoutPaymentScreen/reducer.ts new file mode 100644 index 0000000..9ffed3c --- /dev/null +++ b/app/features/screens/checkoutPaymentScreen/reducer.ts @@ -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; diff --git a/app/features/screens/checkoutPaymentScreen/thunk.ts b/app/features/screens/checkoutPaymentScreen/thunk.ts new file mode 100644 index 0000000..7a2e709 --- /dev/null +++ b/app/features/screens/checkoutPaymentScreen/thunk.ts @@ -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', + ); + } +}); diff --git a/app/features/screens/homeScreen/homeScreen.tsx b/app/features/screens/homeScreen/homeScreen.tsx index 91df4a5..fb5df2f 100644 --- a/app/features/screens/homeScreen/homeScreen.tsx +++ b/app/features/screens/homeScreen/homeScreen.tsx @@ -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 = diff --git a/app/features/screens/index.ts b/app/features/screens/index.ts index 7eaf429..c2542e4 100644 --- a/app/features/screens/index.ts +++ b/app/features/screens/index.ts @@ -20,3 +20,4 @@ export * from './offersScreen'; export * from './helpSupportScreen'; export * from './accountScreen'; export * from './writeReviewScreen'; +export * from './orderDetailsScreen'; diff --git a/app/features/screens/myOrdersScreen/myOrdersScreen.tsx b/app/features/screens/myOrdersScreen/myOrdersScreen.tsx index 6a882b4..3f0e179 100644 --- a/app/features/screens/myOrdersScreen/myOrdersScreen.tsx +++ b/app/features/screens/myOrdersScreen/myOrdersScreen.tsx @@ -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, @@ -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(); const [activeTab, setActiveTab] = useState('All'); + const dispatch = useAppDispatch(); + const { orderHistory } = useAppSelector( + (state: RootState) => state.paymentMethods, + ); + console.log(orderHistory); - const filteredOrders = activeTab === 'All' - ? mockOrders - : activeTab === 'Ongoing' - ? mockOrders.filter((o) => o.status === 'Ongoing') - : mockOrders.filter((o) => o.status === 'Delivered'); + 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' + ? orders.filter(o => ONGOING_STATUSES.includes(o.status)) + : orders.filter(o => COMPLETED_STATUSES.includes(o.status)); return (
- {TABS.map((tab) => ( + {TABS.map(tab => ( setActiveTab(tab)} activeOpacity={0.7} > @@ -65,25 +105,36 @@ export const MyOrdersScreen: React.FC = () => { item.id} + keyExtractor={item => item.id} renderItem={({ item }) => ( { 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, + }); } }} /> diff --git a/app/features/screens/orderDetailsScreen/index.ts b/app/features/screens/orderDetailsScreen/index.ts new file mode 100644 index 0000000..b54c20b --- /dev/null +++ b/app/features/screens/orderDetailsScreen/index.ts @@ -0,0 +1 @@ +export * from './orderDetailsScreen'; diff --git a/app/features/screens/orderDetailsScreen/orderDetailsScreen.styles.ts b/app/features/screens/orderDetailsScreen/orderDetailsScreen.styles.ts new file mode 100644 index 0000000..c43d2c2 --- /dev/null +++ b/app/features/screens/orderDetailsScreen/orderDetailsScreen.styles.ts @@ -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, + }, + }); diff --git a/app/features/screens/orderDetailsScreen/orderDetailsScreen.tsx b/app/features/screens/orderDetailsScreen/orderDetailsScreen.tsx new file mode 100644 index 0000000..1a2a985 --- /dev/null +++ b/app/features/screens/orderDetailsScreen/orderDetailsScreen.tsx @@ -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; +type OrderDetailsNavProp = StackNavigationProp; + +export const OrderDetailsScreen: React.FC = () => { + const { colors } = useAppTheme(); + const styles = getStyles(colors); + const route = useRoute(); + const navigation = useNavigation(); + 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 ( + +
navigation.goBack()} /> + + + Fetching order details... + + + ); + } + + if (orderDetailsError || !orderDetails) { + return ( + +
navigation.goBack()} /> + + + {orderDetailsError || 'Could not load order details.'} + + + + ); + } + + const orderDate = orderDetails.createdAt + ? `${formatDate(orderDetails.createdAt)} at ${formatTime( + orderDetails.createdAt, + )}` + : ''; + + return ( + +
navigation.goBack()} /> + + {/* Top Info Section */} + + + + Order {orderDetails.orderNumber?.split('-').pop() || ''} + + + {orderDetails.status} + + + Placed on {orderDate} + + Payment: {orderDetails.paymentMethod} + + + {/* Merchant Info */} + + + 🏪 + + + {orderDetails.merchant?.name || 'Store'} + + + + + {/* Order Status Timeline Section */} + + Order Status + + + + {/* Order Items Section */} + + Order Items + {orderDetails.orderItems?.map((item, index) => ( + + + + {item.quantity}x + + {item.name} + + ₹{item.totalAmount} + + ))} + + + {/* Bill Summary Section */} + + Bill Summary + + Item Total + ₹{orderDetails.subtotal} + + + Delivery Fee + ₹{orderDetails.deliveryFee} + + + Platform Fee + ₹{orderDetails.platformFee} + + + Taxes + ₹{orderDetails.taxAmount} + + + + + + Grand Total + + ₹{orderDetails.totalAmount} + + + + + {/* Delivery Address Section */} + + Delivery Details + + {orderDetails.dropAddress?.label || 'Home'} + + + {orderDetails.dropAddress?.houseNumber + ? `${orderDetails.dropAddress.houseNumber}, ` + : ''} + {orderDetails.dropAddress?.addressLine1} + + + {orderDetails.dropAddress?.landmark + ? `Landmark: ${orderDetails.dropAddress.landmark}` + : ''} + + + Phone: {orderDetails.dropAddress?.phone} + + + + {/* Payment Info */} + {orderDetails.payments && orderDetails.payments.length > 0 && ( + + Payment Details + + 💳 + + Paid via {orderDetails.payments[0].method} ( + {orderDetails.payments[0].status}) + + + + )} + + + ); +}; + +export default OrderDetailsScreen; diff --git a/app/interfaces/auth.ts b/app/interfaces/auth.ts index e6180b7..34b4ffe 100644 --- a/app/interfaces/auth.ts +++ b/app/interfaces/auth.ts @@ -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'; diff --git a/app/interfaces/customerProfile.ts b/app/interfaces/customerProfile.ts new file mode 100644 index 0000000..105b5fa --- /dev/null +++ b/app/interfaces/customerProfile.ts @@ -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'; diff --git a/app/interfaces/index.ts b/app/interfaces/index.ts index b28f55c..4182e88 100644 --- a/app/interfaces/index.ts +++ b/app/interfaces/index.ts @@ -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'; diff --git a/app/interfaces/order.ts b/app/interfaces/order.ts new file mode 100644 index 0000000..f45c7b6 --- /dev/null +++ b/app/interfaces/order.ts @@ -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; +} + +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; + 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'; diff --git a/app/interfaces/paymentMethods.ts b/app/interfaces/paymentMethods.ts new file mode 100644 index 0000000..2bd6164 --- /dev/null +++ b/app/interfaces/paymentMethods.ts @@ -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[]; diff --git a/app/navigation/appStack.tsx b/app/navigation/appStack.tsx index 8fb7a61..6fa7868 100644 --- a/app/navigation/appStack.tsx +++ b/app/navigation/appStack.tsx @@ -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(); @@ -66,6 +68,10 @@ export const AppStack: React.FC = () => { /> + ); }; diff --git a/app/services/apiClient.ts b/app/services/apiClient.ts index 76778a8..803a6d9 100644 --- a/app/services/apiClient.ts +++ b/app/services/apiClient.ts @@ -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 = { diff --git a/app/store/commonreducers/customerProfile/index.ts b/app/store/commonreducers/customerProfile/index.ts new file mode 100644 index 0000000..c4eb6e2 --- /dev/null +++ b/app/store/commonreducers/customerProfile/index.ts @@ -0,0 +1,2 @@ +export * from './thunk'; +export * from './reducer'; diff --git a/app/store/commonreducers/customerProfile/reducer.ts b/app/store/commonreducers/customerProfile/reducer.ts new file mode 100644 index 0000000..8558283 --- /dev/null +++ b/app/store/commonreducers/customerProfile/reducer.ts @@ -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; diff --git a/app/store/commonreducers/customerProfile/thunk.ts b/app/store/commonreducers/customerProfile/thunk.ts new file mode 100644 index 0000000..6e0b187 --- /dev/null +++ b/app/store/commonreducers/customerProfile/thunk.ts @@ -0,0 +1,17 @@ +import { getCustomerDetails } from '@api'; +import { CustomerResponse } from '@interfaces'; +import { createAsyncThunk } from '@reduxjs/toolkit'; + +export const fetchCustomerDetails = createAsyncThunk( + 'customer/fetchCustomerDetails', + async (_, { rejectWithValue }) => { + try { + const response = await getCustomerDetails(); + console.log(response); + return response; + } catch (error: any) { + console.log(error); + return rejectWithValue(error?.message); + } + }, +); diff --git a/app/store/commonreducers/index.ts b/app/store/commonreducers/index.ts index 32cc23d..6304c85 100644 --- a/app/store/commonreducers/index.ts +++ b/app/store/commonreducers/index.ts @@ -1,3 +1,4 @@ export * from './auth'; export * from './cart'; export * from './order'; +export * from './customerProfile'; diff --git a/app/store/rootReducer.ts b/app/store/rootReducer.ts index d290838..38e0ff6 100644 --- a/app/store/rootReducer.ts +++ b/app/store/rootReducer.ts @@ -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; diff --git a/app/utils/helper.ts b/app/utils/helper.ts index 119b66c..69a8018 100644 --- a/app/utils/helper.ts +++ b/app/utils/helper.ts @@ -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', + }); +}; diff --git a/package.json b/package.json index 475c668..7ac003a 100644 --- a/package.json +++ b/package.json @@ -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", diff --git a/yarn.lock b/yarn.lock index 35bcf49..2d4e5a5 100644 --- a/yarn.lock +++ b/yarn.lock @@ -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"