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 { apiClient } from '../services/apiClient';
|
||||||
import { Provider, CatalogItem, Order, DeliveryAgent, Location } from '../interfaces';
|
import {
|
||||||
|
Provider,
|
||||||
|
CatalogItem,
|
||||||
|
Order,
|
||||||
|
DeliveryAgent,
|
||||||
|
Location,
|
||||||
|
} from '../interfaces';
|
||||||
|
|
||||||
// ─── Types ───────────────────────────────────────────────────────────────────
|
// ─── Types ───────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
@ -10,8 +16,7 @@ export interface PlaceOrderResponse {
|
|||||||
|
|
||||||
// ─── Endpoints ───────────────────────────────────────────────────────────────
|
// ─── Endpoints ───────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
export const getProvidersApi = () =>
|
export const getProvidersApi = () => apiClient.get<Provider[]>('/providers');
|
||||||
apiClient.get<Provider[]>('/providers');
|
|
||||||
|
|
||||||
export const getProviderCatalogApi = (providerId: string) =>
|
export const getProviderCatalogApi = (providerId: string) =>
|
||||||
apiClient.get<CatalogItem[]>(`/providers/${providerId}/catalog`);
|
apiClient.get<CatalogItem[]>(`/providers/${providerId}/catalog`);
|
||||||
@ -19,8 +24,8 @@ export const getProviderCatalogApi = (providerId: string) =>
|
|||||||
export const searchProvidersApi = (query: string) =>
|
export const searchProvidersApi = (query: string) =>
|
||||||
apiClient.get<Provider[]>(`/search?q=${query}`);
|
apiClient.get<Provider[]>(`/search?q=${query}`);
|
||||||
|
|
||||||
export const placeOrderApi = (orderData: Partial<Order>) =>
|
// export const placeOrderApi = (orderData: Partial<Order>) =>
|
||||||
apiClient.post<PlaceOrderResponse>('/orders', orderData);
|
// apiClient.post<PlaceOrderResponse>('/orders', orderData);
|
||||||
|
|
||||||
export const getDeliveryAgentApi = () =>
|
export const getDeliveryAgentApi = () =>
|
||||||
apiClient.get<DeliveryAgent>('/delivery/agent');
|
apiClient.get<DeliveryAgent>('/delivery/agent');
|
||||||
|
|||||||
@ -3,3 +3,6 @@ export * from './deliveryApi';
|
|||||||
export * from './onboardApi';
|
export * from './onboardApi';
|
||||||
export * from './productApi';
|
export * from './productApi';
|
||||||
export * from './cartApi';
|
export * from './cartApi';
|
||||||
|
export * from './paymentMethodsApi';
|
||||||
|
export * from './customerDetailsApi';
|
||||||
|
export * from './orderApi';
|
||||||
|
|||||||
14
app/api/orderApi.ts
Normal file
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 './ratingStars';
|
||||||
export * from './orderHistoryCard';
|
export * from './orderHistoryCard';
|
||||||
export * from './paymentOption';
|
export * from './paymentOption';
|
||||||
|
export * from './OrderStatusTimeline';
|
||||||
|
|||||||
@ -1,3 +1,5 @@
|
|||||||
|
import { OrderItem } from '@interfaces';
|
||||||
|
|
||||||
export interface OrderHistoryCardProps {
|
export interface OrderHistoryCardProps {
|
||||||
providerName: string;
|
providerName: string;
|
||||||
providerImage: string;
|
providerImage: string;
|
||||||
@ -6,4 +8,5 @@ export interface OrderHistoryCardProps {
|
|||||||
total: number;
|
total: number;
|
||||||
onReorder?: () => void;
|
onReorder?: () => void;
|
||||||
onDetails?: () => void;
|
onDetails?: () => void;
|
||||||
|
items?: OrderItem[];
|
||||||
}
|
}
|
||||||
|
|||||||
@ -53,12 +53,42 @@ export const getStyles = (colors: any) => StyleSheet.create({
|
|||||||
fontWeight: typography.fontWeight.medium,
|
fontWeight: typography.fontWeight.medium,
|
||||||
color: colors.primary,
|
color: colors.primary,
|
||||||
},
|
},
|
||||||
|
itemsContainer: {
|
||||||
|
paddingVertical: 12,
|
||||||
|
borderTopWidth: 1,
|
||||||
|
borderTopColor: colors.border,
|
||||||
|
},
|
||||||
|
itemRow: {
|
||||||
|
flexDirection: 'row',
|
||||||
|
alignItems: 'flex-start',
|
||||||
|
marginBottom: 8,
|
||||||
|
},
|
||||||
|
itemQuantityBadge: {
|
||||||
|
backgroundColor: colors.surface,
|
||||||
|
borderWidth: 1,
|
||||||
|
borderColor: colors.border,
|
||||||
|
borderRadius: 4,
|
||||||
|
paddingHorizontal: 6,
|
||||||
|
paddingVertical: 2,
|
||||||
|
marginRight: 8,
|
||||||
|
marginTop: 2,
|
||||||
|
},
|
||||||
|
itemQuantityText: {
|
||||||
|
fontSize: typography.fontSize.xs,
|
||||||
|
color: colors.text,
|
||||||
|
fontWeight: typography.fontWeight.medium,
|
||||||
|
},
|
||||||
|
itemName: {
|
||||||
|
flex: 1,
|
||||||
|
fontSize: typography.fontSize.sm,
|
||||||
|
color: colors.text,
|
||||||
|
lineHeight: 20,
|
||||||
|
},
|
||||||
footer: {
|
footer: {
|
||||||
flexDirection: 'row',
|
flexDirection: 'row',
|
||||||
justifyContent: 'space-between',
|
justifyContent: 'space-between',
|
||||||
alignItems: 'center',
|
alignItems: 'center',
|
||||||
marginTop: 10,
|
paddingTop: 12,
|
||||||
paddingTop: 10,
|
|
||||||
borderTopWidth: 1,
|
borderTopWidth: 1,
|
||||||
borderTopColor: colors.border,
|
borderTopColor: colors.border,
|
||||||
},
|
},
|
||||||
|
|||||||
@ -12,6 +12,7 @@ export const OrderHistoryCard: React.FC<OrderHistoryCardProps> = ({
|
|||||||
total,
|
total,
|
||||||
onReorder,
|
onReorder,
|
||||||
onDetails,
|
onDetails,
|
||||||
|
items,
|
||||||
}) => {
|
}) => {
|
||||||
const { colors } = useAppTheme();
|
const { colors } = useAppTheme();
|
||||||
const styles = getStyles(colors);
|
const styles = getStyles(colors);
|
||||||
@ -30,6 +31,20 @@ export const OrderHistoryCard: React.FC<OrderHistoryCardProps> = ({
|
|||||||
<Text style={styles.statusText}>{status}</Text>
|
<Text style={styles.statusText}>{status}</Text>
|
||||||
</View>
|
</View>
|
||||||
</View>
|
</View>
|
||||||
|
{items && items.length > 0 && (
|
||||||
|
<View style={styles.itemsContainer}>
|
||||||
|
{items.map((item, index) => (
|
||||||
|
<View key={item.id || index} style={styles.itemRow}>
|
||||||
|
<View style={styles.itemQuantityBadge}>
|
||||||
|
<Text style={styles.itemQuantityText}>{item.quantity}x</Text>
|
||||||
|
</View>
|
||||||
|
<Text style={styles.itemName} numberOfLines={1}>
|
||||||
|
{item.name}
|
||||||
|
</Text>
|
||||||
|
</View>
|
||||||
|
))}
|
||||||
|
</View>
|
||||||
|
)}
|
||||||
<View style={styles.footer}>
|
<View style={styles.footer}>
|
||||||
<Text style={styles.total}>₹{total}</Text>
|
<Text style={styles.total}>₹{total}</Text>
|
||||||
<View style={styles.actions}>
|
<View style={styles.actions}>
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
export interface PaymentOptionProps {
|
export interface PaymentOptionProps {
|
||||||
type: string;
|
|
||||||
label: string;
|
label: string;
|
||||||
isSelected: boolean;
|
isSelected: boolean;
|
||||||
|
icon?: string;
|
||||||
onSelect: () => void;
|
onSelect: () => void;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,7 +1,8 @@
|
|||||||
import { StyleSheet } from 'react-native';
|
import { StyleSheet } from 'react-native';
|
||||||
import { typography } from '@theme';
|
import { typography } from '@theme';
|
||||||
|
|
||||||
export const getStyles = (colors: any) => StyleSheet.create({
|
export const getStyles = (colors: any) =>
|
||||||
|
StyleSheet.create({
|
||||||
container: {
|
container: {
|
||||||
flexDirection: 'row',
|
flexDirection: 'row',
|
||||||
alignItems: 'center',
|
alignItems: 'center',
|
||||||
@ -18,7 +19,8 @@ export const getStyles = (colors: any) => StyleSheet.create({
|
|||||||
backgroundColor: '#E8F5E9',
|
backgroundColor: '#E8F5E9',
|
||||||
},
|
},
|
||||||
icon: {
|
icon: {
|
||||||
fontSize: 24,
|
width: 24,
|
||||||
|
height: 24,
|
||||||
marginRight: 12,
|
marginRight: 12,
|
||||||
},
|
},
|
||||||
label: {
|
label: {
|
||||||
@ -45,4 +47,4 @@ export const getStyles = (colors: any) => StyleSheet.create({
|
|||||||
borderRadius: 6,
|
borderRadius: 6,
|
||||||
backgroundColor: colors.primary,
|
backgroundColor: colors.primary,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|||||||
@ -1,17 +1,18 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { View, Text, TouchableOpacity } from 'react-native';
|
import { View, Text, TouchableOpacity, Image } from 'react-native';
|
||||||
import { PaymentOptionProps } from './paymentOption.props';
|
import { PaymentOptionProps } from './paymentOption.props';
|
||||||
import { getStyles } from './paymentOption.styles';
|
import { getStyles } from './paymentOption.styles';
|
||||||
import { useAppTheme } from '@theme';
|
import { useAppTheme } from '@theme';
|
||||||
|
|
||||||
export const PaymentOption: React.FC<PaymentOptionProps> = ({
|
export const PaymentOption: React.FC<PaymentOptionProps> = ({
|
||||||
type,
|
|
||||||
label,
|
label,
|
||||||
isSelected,
|
isSelected,
|
||||||
|
icon,
|
||||||
onSelect,
|
onSelect,
|
||||||
}) => {
|
}) => {
|
||||||
const { colors } = useAppTheme();
|
const { colors } = useAppTheme();
|
||||||
const styles = getStyles(colors);
|
const styles = getStyles(colors);
|
||||||
|
// console.log(icon);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<TouchableOpacity
|
<TouchableOpacity
|
||||||
@ -19,7 +20,7 @@ export const PaymentOption: React.FC<PaymentOptionProps> = ({
|
|||||||
onPress={onSelect}
|
onPress={onSelect}
|
||||||
activeOpacity={0.7}
|
activeOpacity={0.7}
|
||||||
>
|
>
|
||||||
<Text style={styles.icon}>{type === 'UPI' ? '📱' : type === 'Card' ? '💳' : type === 'Wallet' ? '👛' : '💵'}</Text>
|
<Image source={{ uri: icon }} style={styles.icon} />
|
||||||
<Text style={styles.label}>{label}</Text>
|
<Text style={styles.label}>{label}</Text>
|
||||||
<View style={[styles.radio, isSelected && styles.radioSelected]}>
|
<View style={[styles.radio, isSelected && styles.radioSelected]}>
|
||||||
{isSelected && <View style={styles.radioInner} />}
|
{isSelected && <View style={styles.radioInner} />}
|
||||||
|
|||||||
@ -9,6 +9,7 @@ import {
|
|||||||
} from 'react-native';
|
} from 'react-native';
|
||||||
import { getStyles } from './productCard.styles';
|
import { getStyles } from './productCard.styles';
|
||||||
import { useAppTheme } from '@theme';
|
import { useAppTheme } from '@theme';
|
||||||
|
import { getFullUrl } from '@utils/helper';
|
||||||
|
|
||||||
export interface ProductCardProps {
|
export interface ProductCardProps {
|
||||||
id: string;
|
id: string;
|
||||||
@ -63,9 +64,7 @@ export const ProductCard: React.FC<ProductCardProps> = ({
|
|||||||
<View style={styles.imageContainer}>
|
<View style={styles.imageContainer}>
|
||||||
<Image
|
<Image
|
||||||
source={{
|
source={{
|
||||||
uri: imageUrl?.startsWith('/')
|
uri: getFullUrl(imageUrl),
|
||||||
? `https://f7ee-202-8-116-13.ngrok-free.app${imageUrl}`
|
|
||||||
: imageUrl,
|
|
||||||
}}
|
}}
|
||||||
style={styles.image}
|
style={styles.image}
|
||||||
// fallback source can be handled here if needed
|
// fallback source can be handled here if needed
|
||||||
@ -99,13 +98,13 @@ export const ProductCard: React.FC<ProductCardProps> = ({
|
|||||||
)}
|
)}
|
||||||
</View>
|
</View>
|
||||||
|
|
||||||
<TouchableOpacity
|
{/* <TouchableOpacity
|
||||||
style={styles.addButton}
|
style={styles.addButton}
|
||||||
onPress={onAddPress || onPress}
|
onPress={onAddPress || onPress}
|
||||||
activeOpacity={0.7}
|
activeOpacity={0.7}
|
||||||
>
|
>
|
||||||
<Text style={styles.addButtonText}>+ ADD</Text>
|
<Text style={styles.addButtonText}>+ ADD</Text>
|
||||||
</TouchableOpacity>
|
</TouchableOpacity> */}
|
||||||
</View>
|
</View>
|
||||||
</TouchableOpacity>
|
</TouchableOpacity>
|
||||||
);
|
);
|
||||||
|
|||||||
@ -1,37 +1,15 @@
|
|||||||
import { StyleSheet } from 'react-native';
|
import { StyleSheet } from 'react-native';
|
||||||
import { typography } from '@theme';
|
import { typography } from '@theme';
|
||||||
|
|
||||||
export const getStyles = (colors: any) => StyleSheet.create({
|
export const getStyles = (colors: any) =>
|
||||||
|
StyleSheet.create({
|
||||||
container: {
|
container: {
|
||||||
flex: 1,
|
flex: 1,
|
||||||
backgroundColor: colors.background,
|
backgroundColor: colors.background,
|
||||||
},
|
},
|
||||||
content: {
|
content: {
|
||||||
padding: 16,
|
padding: 16,
|
||||||
},
|
paddingBottom: 24,
|
||||||
addressCard: {
|
|
||||||
backgroundColor: colors.cardBg,
|
|
||||||
borderRadius: 12,
|
|
||||||
padding: 16,
|
|
||||||
borderWidth: 1,
|
|
||||||
borderColor: colors.border,
|
|
||||||
marginBottom: 24,
|
|
||||||
},
|
|
||||||
addressLabel: {
|
|
||||||
fontSize: typography.fontSize.xs,
|
|
||||||
color: colors.textSecondary,
|
|
||||||
marginBottom: 4,
|
|
||||||
},
|
|
||||||
addressText: {
|
|
||||||
fontSize: typography.fontSize.md,
|
|
||||||
fontWeight: typography.fontWeight.semibold,
|
|
||||||
color: colors.text,
|
|
||||||
marginBottom: 4,
|
|
||||||
},
|
|
||||||
addressSubtext: {
|
|
||||||
fontSize: typography.fontSize.sm,
|
|
||||||
color: colors.primary,
|
|
||||||
fontWeight: typography.fontWeight.medium,
|
|
||||||
},
|
},
|
||||||
sectionTitle: {
|
sectionTitle: {
|
||||||
fontSize: typography.fontSize.md,
|
fontSize: typography.fontSize.md,
|
||||||
@ -39,32 +17,61 @@ export const getStyles = (colors: any) => StyleSheet.create({
|
|||||||
color: colors.text,
|
color: colors.text,
|
||||||
marginBottom: 12,
|
marginBottom: 12,
|
||||||
},
|
},
|
||||||
optionCard: {
|
emptyState: {
|
||||||
flexDirection: 'row',
|
padding: 24,
|
||||||
alignItems: 'center',
|
alignItems: 'center',
|
||||||
padding: 16,
|
justifyContent: 'center',
|
||||||
|
},
|
||||||
|
emptyStateText: {
|
||||||
|
fontSize: typography.fontSize.sm,
|
||||||
|
color: colors.textSecondary,
|
||||||
|
},
|
||||||
|
addressCard: {
|
||||||
|
backgroundColor: colors.cardBg,
|
||||||
borderRadius: 12,
|
borderRadius: 12,
|
||||||
|
padding: 16,
|
||||||
borderWidth: 1.5,
|
borderWidth: 1.5,
|
||||||
borderColor: colors.border,
|
borderColor: colors.border,
|
||||||
marginBottom: 10,
|
marginBottom: 12,
|
||||||
backgroundColor: colors.background,
|
|
||||||
},
|
},
|
||||||
optionCardSelected: {
|
addressCardSelected: {
|
||||||
borderColor: colors.primary,
|
borderColor: colors.primary,
|
||||||
backgroundColor: '#E8F5E9',
|
backgroundColor: '#E8F5E9',
|
||||||
},
|
},
|
||||||
optionInfo: {
|
addressCardHeader: {
|
||||||
flex: 1,
|
flexDirection: 'row',
|
||||||
|
alignItems: 'center',
|
||||||
|
marginBottom: 8,
|
||||||
},
|
},
|
||||||
optionTitle: {
|
labelBadge: {
|
||||||
fontSize: typography.fontSize.md,
|
flexDirection: 'row',
|
||||||
fontWeight: typography.fontWeight.medium,
|
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,
|
color: colors.text,
|
||||||
marginBottom: 2,
|
|
||||||
},
|
},
|
||||||
optionSubtext: {
|
defaultBadge: {
|
||||||
fontSize: typography.fontSize.sm,
|
backgroundColor: colors.primary,
|
||||||
color: colors.textSecondary,
|
borderRadius: 20,
|
||||||
|
paddingVertical: 3,
|
||||||
|
paddingHorizontal: 8,
|
||||||
|
},
|
||||||
|
defaultBadgeText: {
|
||||||
|
fontSize: 10,
|
||||||
|
fontWeight: typography.fontWeight.bold,
|
||||||
|
color: '#fff',
|
||||||
|
letterSpacing: 0.5,
|
||||||
},
|
},
|
||||||
radio: {
|
radio: {
|
||||||
width: 22,
|
width: 22,
|
||||||
@ -74,6 +81,7 @@ export const getStyles = (colors: any) => StyleSheet.create({
|
|||||||
borderColor: colors.border,
|
borderColor: colors.border,
|
||||||
justifyContent: 'center',
|
justifyContent: 'center',
|
||||||
alignItems: 'center',
|
alignItems: 'center',
|
||||||
|
marginLeft: 'auto',
|
||||||
},
|
},
|
||||||
radioSelected: {
|
radioSelected: {
|
||||||
borderColor: colors.primary,
|
borderColor: colors.primary,
|
||||||
@ -84,9 +92,39 @@ export const getStyles = (colors: any) => StyleSheet.create({
|
|||||||
borderRadius: 6,
|
borderRadius: 6,
|
||||||
backgroundColor: colors.primary,
|
backgroundColor: colors.primary,
|
||||||
},
|
},
|
||||||
|
addressText: {
|
||||||
|
fontSize: typography.fontSize.md,
|
||||||
|
fontWeight: typography.fontWeight.medium,
|
||||||
|
color: colors.text,
|
||||||
|
marginBottom: 4,
|
||||||
|
},
|
||||||
|
addressSubtext: {
|
||||||
|
fontSize: typography.fontSize.sm,
|
||||||
|
color: colors.textSecondary,
|
||||||
|
marginBottom: 2,
|
||||||
|
},
|
||||||
|
addressPhone: {
|
||||||
|
fontSize: typography.fontSize.sm,
|
||||||
|
color: colors.textSecondary,
|
||||||
|
marginTop: 4,
|
||||||
|
},
|
||||||
|
addAddressButton: {
|
||||||
|
borderWidth: 1.5,
|
||||||
|
borderColor: colors.primary,
|
||||||
|
borderStyle: 'dashed',
|
||||||
|
borderRadius: 12,
|
||||||
|
paddingVertical: 14,
|
||||||
|
alignItems: 'center',
|
||||||
|
marginTop: 4,
|
||||||
|
},
|
||||||
|
addAddressButtonText: {
|
||||||
|
fontSize: typography.fontSize.md,
|
||||||
|
fontWeight: typography.fontWeight.semibold,
|
||||||
|
color: colors.primary,
|
||||||
|
},
|
||||||
footer: {
|
footer: {
|
||||||
padding: 16,
|
padding: 16,
|
||||||
borderTopWidth: 1,
|
borderTopWidth: 1,
|
||||||
borderTopColor: colors.border,
|
borderTopColor: colors.border,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|||||||
@ -1,18 +1,18 @@
|
|||||||
import React, { useState } from 'react';
|
import React, { useState, useMemo } from 'react';
|
||||||
import {
|
import { View, Text, TouchableOpacity, ScrollView } from 'react-native';
|
||||||
View,
|
|
||||||
Text,
|
|
||||||
TouchableOpacity,
|
|
||||||
ScrollView,
|
|
||||||
} from 'react-native';
|
|
||||||
import { useNavigation } from '@react-navigation/native';
|
import { useNavigation } from '@react-navigation/native';
|
||||||
import { StackNavigationProp } from '@react-navigation/stack';
|
import { StackNavigationProp } from '@react-navigation/stack';
|
||||||
import { getStyles } from './checkoutAddressScreen.styles';
|
import { getStyles } from './checkoutAddressScreen.styles';
|
||||||
import { Header, StepProgress, PrimaryButton } from '@components';
|
import { Header, StepProgress, PrimaryButton } from '@components';
|
||||||
import { useAppTheme } from '@theme';
|
import { useAppTheme } from '@theme';
|
||||||
import { AppStackParamList } from '../../../navigation/appStack';
|
import { useAppSelector } from '@store';
|
||||||
|
import { AddressLabel, CustomerAddress } from '@interfaces';
|
||||||
|
import { AppStackParamList } from '@navigation/appStack';
|
||||||
|
|
||||||
type CheckoutAddressNavProp = StackNavigationProp<AppStackParamList, 'CheckoutAddressScreen'>;
|
type CheckoutAddressNavProp = StackNavigationProp<
|
||||||
|
AppStackParamList,
|
||||||
|
'CheckoutAddressScreen'
|
||||||
|
>;
|
||||||
|
|
||||||
const CHECKOUT_STEPS = [
|
const CHECKOUT_STEPS = [
|
||||||
{ key: 'address', label: 'Address' },
|
{ key: 'address', label: 'Address' },
|
||||||
@ -20,62 +20,118 @@ const CHECKOUT_STEPS = [
|
|||||||
{ key: 'confirm', label: 'Confirm' },
|
{ key: 'confirm', label: 'Confirm' },
|
||||||
];
|
];
|
||||||
|
|
||||||
|
const LABEL_ICON: Record<AddressLabel, string> = {
|
||||||
|
Home: '🏠',
|
||||||
|
Work: '💼',
|
||||||
|
Other: '📍',
|
||||||
|
};
|
||||||
|
|
||||||
export const CheckoutAddressScreen: React.FC = () => {
|
export const CheckoutAddressScreen: React.FC = () => {
|
||||||
const { colors } = useAppTheme();
|
const { colors } = useAppTheme();
|
||||||
const styles = getStyles(colors);
|
const styles = getStyles(colors);
|
||||||
const navigation = useNavigation<CheckoutAddressNavProp>();
|
const navigation = useNavigation<CheckoutAddressNavProp>();
|
||||||
const [deliveryOption, setDeliveryOption] = useState<'standard' | 'express'>('standard');
|
const { customerDetails } = useAppSelector(state => state.customerProfile);
|
||||||
|
|
||||||
|
const addresses: CustomerAddress[] = customerDetails?.addresses ?? [];
|
||||||
|
|
||||||
|
const defaultAddressId = useMemo(() => {
|
||||||
|
const def = addresses.find(a => a.isDefault);
|
||||||
|
return def?.id ?? addresses[0]?.id ?? null;
|
||||||
|
}, [addresses]);
|
||||||
|
|
||||||
|
const [selectedAddressId, setSelectedAddressId] = useState<string | null>(
|
||||||
|
defaultAddressId,
|
||||||
|
);
|
||||||
|
|
||||||
|
const handleContinue = () => {
|
||||||
|
if (!selectedAddressId) return;
|
||||||
|
navigation.navigate('CheckoutPaymentScreen', {
|
||||||
|
selectedAddressId,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<View style={styles.container}>
|
<View style={styles.container}>
|
||||||
<Header title="Checkout" onBack={() => navigation.goBack()} />
|
<Header title="Checkout" onBack={() => navigation.goBack()} />
|
||||||
<StepProgress steps={CHECKOUT_STEPS} activeStep={0} />
|
<StepProgress steps={CHECKOUT_STEPS} activeStep={0} />
|
||||||
<ScrollView contentContainerStyle={styles.content}>
|
<ScrollView contentContainerStyle={styles.content}>
|
||||||
<View style={styles.addressCard}>
|
<Text style={styles.sectionTitle}>Deliver to</Text>
|
||||||
<Text style={styles.addressLabel}>Deliver to</Text>
|
|
||||||
<Text style={styles.addressText}>
|
{addresses.length === 0 && (
|
||||||
Koramangala 4th Block, Bengaluru, 560034
|
<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>
|
||||||
<Text style={styles.addressSubtext}>Home</Text>
|
<Text style={styles.labelBadgeText}>{address.label}</Text>
|
||||||
</View>
|
</View>
|
||||||
|
|
||||||
<Text style={styles.sectionTitle}>Delivery Option</Text>
|
{address.isDefault && (
|
||||||
<TouchableOpacity
|
<View style={styles.defaultBadge}>
|
||||||
style={[
|
<Text style={styles.defaultBadgeText}>DEFAULT</Text>
|
||||||
styles.optionCard,
|
</View>
|
||||||
deliveryOption === 'standard' && styles.optionCardSelected,
|
)}
|
||||||
]}
|
|
||||||
onPress={() => setDeliveryOption('standard')}
|
<View
|
||||||
activeOpacity={0.7}
|
style={[styles.radio, isSelected && styles.radioSelected]}
|
||||||
>
|
>
|
||||||
<View style={styles.optionInfo}>
|
{isSelected && <View style={styles.radioInner} />}
|
||||||
<Text style={styles.optionTitle}>Standard Delivery</Text>
|
|
||||||
<Text style={styles.optionSubtext}>Free • 25-30 min</Text>
|
|
||||||
</View>
|
</View>
|
||||||
<View style={[styles.radio, deliveryOption === 'standard' && styles.radioSelected]}>
|
|
||||||
{deliveryOption === 'standard' && <View style={styles.radioInner} />}
|
|
||||||
</View>
|
</View>
|
||||||
|
|
||||||
|
<Text style={styles.addressText} numberOfLines={2}>
|
||||||
|
{address.houseNumber ? `${address.houseNumber}, ` : ''}
|
||||||
|
{address.addressLine1}
|
||||||
|
</Text>
|
||||||
|
|
||||||
|
{!!address.landmark && (
|
||||||
|
<Text style={styles.addressSubtext} numberOfLines={1}>
|
||||||
|
Landmark: {address.landmark}
|
||||||
|
</Text>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<Text style={styles.addressSubtext} numberOfLines={1}>
|
||||||
|
{address.city}, {address.state} - {address.postalCode}
|
||||||
|
</Text>
|
||||||
|
|
||||||
|
<Text style={styles.addressPhone}>📞 {address.phone}</Text>
|
||||||
</TouchableOpacity>
|
</TouchableOpacity>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
|
||||||
<TouchableOpacity
|
<TouchableOpacity
|
||||||
style={[
|
style={styles.addAddressButton}
|
||||||
styles.optionCard,
|
|
||||||
deliveryOption === 'express' && styles.optionCardSelected,
|
|
||||||
]}
|
|
||||||
onPress={() => setDeliveryOption('express')}
|
|
||||||
activeOpacity={0.7}
|
activeOpacity={0.7}
|
||||||
|
onPress={() => {
|
||||||
|
// navigation.navigate('AddAddressScreen');
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
<View style={styles.optionInfo}>
|
<Text style={styles.addAddressButtonText}>+ Add New Address</Text>
|
||||||
<Text style={styles.optionTitle}>Express Delivery</Text>
|
|
||||||
<Text style={styles.optionSubtext}>₹40 • 10-15 min</Text>
|
|
||||||
</View>
|
|
||||||
<View style={[styles.radio, deliveryOption === 'express' && styles.radioSelected]}>
|
|
||||||
{deliveryOption === 'express' && <View style={styles.radioInner} />}
|
|
||||||
</View>
|
|
||||||
</TouchableOpacity>
|
</TouchableOpacity>
|
||||||
</ScrollView>
|
</ScrollView>
|
||||||
<View style={styles.footer}>
|
<View style={styles.footer}>
|
||||||
<PrimaryButton title="Continue to Payment" onPress={() => navigation.navigate('CheckoutPaymentScreen')} />
|
<PrimaryButton
|
||||||
|
title="Continue to Payment"
|
||||||
|
onPress={handleContinue}
|
||||||
|
disabled={!selectedAddressId}
|
||||||
|
/>
|
||||||
</View>
|
</View>
|
||||||
</View>
|
</View>
|
||||||
);
|
);
|
||||||
|
|||||||
@ -1,13 +1,30 @@
|
|||||||
import React, { useState } from 'react';
|
import React, { useEffect, useState } from 'react';
|
||||||
import { View, Text, ScrollView } from 'react-native';
|
import { View, Text, ScrollView, Alert } from 'react-native';
|
||||||
import { useNavigation } from '@react-navigation/native';
|
import { RouteProp, useNavigation, useRoute } from '@react-navigation/native';
|
||||||
import { StackNavigationProp } from '@react-navigation/stack';
|
import { StackNavigationProp } from '@react-navigation/stack';
|
||||||
import { getStyles } from './checkoutPaymentScreen.styles';
|
import { getStyles } from './checkoutPaymentScreen.styles';
|
||||||
import { Header, StepProgress, PaymentOption, PrimaryButton } from '@components';
|
import {
|
||||||
|
Header,
|
||||||
|
StepProgress,
|
||||||
|
PaymentOption,
|
||||||
|
PrimaryButton,
|
||||||
|
} from '@components';
|
||||||
import { useAppTheme } from '@theme';
|
import { useAppTheme } from '@theme';
|
||||||
import { AppStackParamList } from '../../../navigation/appStack';
|
import { AppStackParamList } from '../../../navigation/appStack';
|
||||||
|
import { RootState, useAppDispatch, useAppSelector } from '@store';
|
||||||
|
import { getAllPaymentMethodsThunk, placeOrderThunk } from './thunk';
|
||||||
|
import { useSelector } from 'react-redux';
|
||||||
|
import uuid from 'react-native-uuid';
|
||||||
|
import { v4 } from 'react-native-uuid/dist/v4';
|
||||||
|
|
||||||
type CheckoutPaymentNavProp = StackNavigationProp<AppStackParamList, 'CheckoutPaymentScreen'>;
|
type CheckoutPaymentNavProp = StackNavigationProp<
|
||||||
|
AppStackParamList,
|
||||||
|
'CheckoutPaymentScreen'
|
||||||
|
>;
|
||||||
|
type CheckoutPaymentRouteProp = RouteProp<
|
||||||
|
AppStackParamList,
|
||||||
|
'CheckoutPaymentScreen'
|
||||||
|
>;
|
||||||
|
|
||||||
const CHECKOUT_STEPS = [
|
const CHECKOUT_STEPS = [
|
||||||
{ key: 'address', label: 'Address' },
|
{ key: 'address', label: 'Address' },
|
||||||
@ -25,8 +42,59 @@ const PAYMENT_METHODS = [
|
|||||||
export const CheckoutPaymentScreen: React.FC = () => {
|
export const CheckoutPaymentScreen: React.FC = () => {
|
||||||
const { colors } = useAppTheme();
|
const { colors } = useAppTheme();
|
||||||
const styles = getStyles(colors);
|
const styles = getStyles(colors);
|
||||||
|
const route = useRoute<CheckoutPaymentRouteProp>();
|
||||||
const navigation = useNavigation<CheckoutPaymentNavProp>();
|
const navigation = useNavigation<CheckoutPaymentNavProp>();
|
||||||
|
const dispatch = useAppDispatch();
|
||||||
const [selectedMethod, setSelectedMethod] = useState('upi');
|
const [selectedMethod, setSelectedMethod] = useState('upi');
|
||||||
|
const selectedAddressId = route.params?.selectedAddressId;
|
||||||
|
// console.log(selectedAddressId);
|
||||||
|
const uuid = v4();
|
||||||
|
// console.log(uuid);
|
||||||
|
const {
|
||||||
|
paymentMethods,
|
||||||
|
placeOrderSuccess,
|
||||||
|
placeOrderLoading,
|
||||||
|
placeOrderError,
|
||||||
|
} = useAppSelector((state: RootState) => state.paymentMethods);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
dispatch(getAllPaymentMethodsThunk());
|
||||||
|
}, [dispatch]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (placeOrderSuccess) {
|
||||||
|
navigation.navigate('OrderConfirmedScreen', {
|
||||||
|
orderId: 'ORD-' + Math.floor(Math.random() * 900000 + 100000),
|
||||||
|
});
|
||||||
|
} else if (placeOrderError) {
|
||||||
|
Alert.alert('Error', placeOrderError);
|
||||||
|
}
|
||||||
|
}, [placeOrderSuccess, placeOrderError]);
|
||||||
|
|
||||||
|
// console.log('paymentMethods', paymentMethods);
|
||||||
|
|
||||||
|
const selectedPaymentMethod = paymentMethods.find(
|
||||||
|
item => item.id === selectedMethod,
|
||||||
|
);
|
||||||
|
|
||||||
|
const methodName = selectedPaymentMethod?.code;
|
||||||
|
// console.log('methodName', methodName);
|
||||||
|
|
||||||
|
// useEffect(() => {
|
||||||
|
|
||||||
|
// },);
|
||||||
|
|
||||||
|
const handlePlaceOrder = () => {
|
||||||
|
dispatch(
|
||||||
|
placeOrderThunk({
|
||||||
|
addressId: selectedAddressId || '',
|
||||||
|
paymentMethodId: selectedMethod,
|
||||||
|
paymentMethod: methodName || '',
|
||||||
|
orderType: 'DELIVERY',
|
||||||
|
idempotencyKey: `idemp-key-${uuid}`,
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<View style={styles.container}>
|
<View style={styles.container}>
|
||||||
@ -34,18 +102,22 @@ export const CheckoutPaymentScreen: React.FC = () => {
|
|||||||
<StepProgress steps={CHECKOUT_STEPS} activeStep={1} />
|
<StepProgress steps={CHECKOUT_STEPS} activeStep={1} />
|
||||||
<ScrollView contentContainerStyle={styles.content}>
|
<ScrollView contentContainerStyle={styles.content}>
|
||||||
<Text style={styles.sectionTitle}>Select Payment Method</Text>
|
<Text style={styles.sectionTitle}>Select Payment Method</Text>
|
||||||
{PAYMENT_METHODS.map((method) => (
|
{paymentMethods?.map(method => (
|
||||||
<PaymentOption
|
<PaymentOption
|
||||||
key={method.id}
|
key={method.id}
|
||||||
type={method.type}
|
label={method.code}
|
||||||
label={method.label}
|
icon={method.iconUrl}
|
||||||
isSelected={selectedMethod === method.id}
|
isSelected={selectedMethod === method.id}
|
||||||
onSelect={() => setSelectedMethod(method.id)}
|
onSelect={() => setSelectedMethod(method.id)}
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
</ScrollView>
|
</ScrollView>
|
||||||
<View style={styles.footer}>
|
<View style={styles.footer}>
|
||||||
<PrimaryButton title="Place Order" onPress={() => navigation.navigate('OrderConfirmedScreen', { orderId: 'ORD-' + Math.floor(Math.random() * 900000 + 100000) })} />
|
<PrimaryButton
|
||||||
|
title={placeOrderLoading ? 'Placing...' : 'Place Order'}
|
||||||
|
onPress={handlePlaceOrder}
|
||||||
|
disabled={placeOrderLoading}
|
||||||
|
/>
|
||||||
</View>
|
</View>
|
||||||
</View>
|
</View>
|
||||||
);
|
);
|
||||||
|
|||||||
@ -1 +1,3 @@
|
|||||||
export * from './checkoutPaymentScreen';
|
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 { Product } from '../../../interfaces';
|
||||||
import { AppStackParamList } from '../../../navigation/appStack';
|
import { AppStackParamList } from '../../../navigation/appStack';
|
||||||
import { MainTabParamList } from '../../../navigation/mainTabNavigator';
|
import { MainTabParamList } from '../../../navigation/mainTabNavigator';
|
||||||
import { useAppDispatch, useAppSelector } from '@store';
|
import { fetchCustomerDetails, useAppDispatch, useAppSelector } from '@store';
|
||||||
import { getAllProductsThunk } from './thunk';
|
import { getAllProductsThunk } from './thunk';
|
||||||
|
|
||||||
type NavProp = CompositeNavigationProp<
|
type NavProp = CompositeNavigationProp<
|
||||||
@ -83,6 +83,7 @@ export const HomeScreen: React.FC = () => {
|
|||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
dispatch(getAllProductsThunk());
|
dispatch(getAllProductsThunk());
|
||||||
|
dispatch(fetchCustomerDetails());
|
||||||
}, [dispatch]);
|
}, [dispatch]);
|
||||||
|
|
||||||
const filteredProducts =
|
const filteredProducts =
|
||||||
|
|||||||
@ -20,3 +20,4 @@ export * from './offersScreen';
|
|||||||
export * from './helpSupportScreen';
|
export * from './helpSupportScreen';
|
||||||
export * from './accountScreen';
|
export * from './accountScreen';
|
||||||
export * from './writeReviewScreen';
|
export * from './writeReviewScreen';
|
||||||
|
export * from './orderDetailsScreen';
|
||||||
|
|||||||
@ -1,11 +1,9 @@
|
|||||||
import React, { useState } from 'react';
|
import React, { useEffect, useState } from 'react';
|
||||||
|
import { View, Text, FlatList, TouchableOpacity } from 'react-native';
|
||||||
import {
|
import {
|
||||||
View,
|
useNavigation,
|
||||||
Text,
|
CompositeNavigationProp,
|
||||||
FlatList,
|
} from '@react-navigation/native';
|
||||||
TouchableOpacity,
|
|
||||||
} from 'react-native';
|
|
||||||
import { useNavigation, CompositeNavigationProp } from '@react-navigation/native';
|
|
||||||
import { BottomTabNavigationProp } from '@react-navigation/bottom-tabs';
|
import { BottomTabNavigationProp } from '@react-navigation/bottom-tabs';
|
||||||
import { StackNavigationProp } from '@react-navigation/stack';
|
import { StackNavigationProp } from '@react-navigation/stack';
|
||||||
import { getStyles } from './myOrdersScreen.styles';
|
import { getStyles } from './myOrdersScreen.styles';
|
||||||
@ -13,6 +11,9 @@ import { Header, OrderHistoryCard } from '@components';
|
|||||||
import { useAppTheme } from '@theme';
|
import { useAppTheme } from '@theme';
|
||||||
import { AppStackParamList } from '../../../navigation/appStack';
|
import { AppStackParamList } from '../../../navigation/appStack';
|
||||||
import { MainTabParamList } from '../../../navigation/mainTabNavigator';
|
import { MainTabParamList } from '../../../navigation/mainTabNavigator';
|
||||||
|
import { RootState, useAppDispatch, useAppSelector } from '@store';
|
||||||
|
import { getOrderHistoryThunk } from '../checkoutPaymentScreen';
|
||||||
|
import { formatDate } from '@utils/helper';
|
||||||
|
|
||||||
type MyOrdersNavProp = CompositeNavigationProp<
|
type MyOrdersNavProp = CompositeNavigationProp<
|
||||||
BottomTabNavigationProp<MainTabParamList, 'MyOrdersScreen'>,
|
BottomTabNavigationProp<MainTabParamList, 'MyOrdersScreen'>,
|
||||||
@ -21,9 +22,30 @@ type MyOrdersNavProp = CompositeNavigationProp<
|
|||||||
|
|
||||||
const TABS = ['All', 'Ongoing', 'Completed'];
|
const TABS = ['All', 'Ongoing', 'Completed'];
|
||||||
const mockOrders = [
|
const mockOrders = [
|
||||||
{ id: 'ORD-591283', providerName: 'Pizza Planet', providerImage: '', orderDate: '29 Jun 2026', status: 'Delivered', total: 599 },
|
{
|
||||||
{ id: 'ORD-771239', providerName: 'Fresh Mart', providerImage: '', orderDate: '28 Jun 2026', status: 'Ongoing', total: 349 },
|
id: 'ORD-591283',
|
||||||
{ id: 'ORD-108239', providerName: 'Burger Barn', providerImage: '', orderDate: '27 Jun 2026', status: 'Delivered', total: 449 },
|
providerName: 'Pizza Planet',
|
||||||
|
providerImage: '',
|
||||||
|
orderDate: '29 Jun 2026',
|
||||||
|
status: 'Delivered',
|
||||||
|
total: 599,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'ORD-771239',
|
||||||
|
providerName: 'Fresh Mart',
|
||||||
|
providerImage: '',
|
||||||
|
orderDate: '28 Jun 2026',
|
||||||
|
status: 'Ongoing',
|
||||||
|
total: 349,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'ORD-108239',
|
||||||
|
providerName: 'Burger Barn',
|
||||||
|
providerImage: '',
|
||||||
|
orderDate: '27 Jun 2026',
|
||||||
|
status: 'Delivered',
|
||||||
|
total: 449,
|
||||||
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
export const MyOrdersScreen: React.FC = () => {
|
export const MyOrdersScreen: React.FC = () => {
|
||||||
@ -31,24 +53,42 @@ export const MyOrdersScreen: React.FC = () => {
|
|||||||
const styles = getStyles(colors);
|
const styles = getStyles(colors);
|
||||||
const navigation = useNavigation<MyOrdersNavProp>();
|
const navigation = useNavigation<MyOrdersNavProp>();
|
||||||
const [activeTab, setActiveTab] = useState('All');
|
const [activeTab, setActiveTab] = useState('All');
|
||||||
|
const dispatch = useAppDispatch();
|
||||||
|
const { orderHistory } = useAppSelector(
|
||||||
|
(state: RootState) => state.paymentMethods,
|
||||||
|
);
|
||||||
|
console.log(orderHistory);
|
||||||
|
|
||||||
const filteredOrders = activeTab === 'All'
|
useEffect(() => {
|
||||||
? mockOrders
|
dispatch(getOrderHistoryThunk());
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const ONGOING_STATUSES = [
|
||||||
|
'PENDING',
|
||||||
|
'CONFIRMED',
|
||||||
|
'PREPARING',
|
||||||
|
'READY_FOR_PICKUP',
|
||||||
|
'OUT_FOR_DELIVERY',
|
||||||
|
];
|
||||||
|
const COMPLETED_STATUSES = ['DELIVERED', 'CANCELLED'];
|
||||||
|
|
||||||
|
const orders = Array.isArray(orderHistory) ? orderHistory : [];
|
||||||
|
|
||||||
|
const filteredOrders =
|
||||||
|
activeTab === 'All'
|
||||||
|
? orders
|
||||||
: activeTab === 'Ongoing'
|
: activeTab === 'Ongoing'
|
||||||
? mockOrders.filter((o) => o.status === 'Ongoing')
|
? orders.filter(o => ONGOING_STATUSES.includes(o.status))
|
||||||
: mockOrders.filter((o) => o.status === 'Delivered');
|
: orders.filter(o => COMPLETED_STATUSES.includes(o.status));
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<View style={styles.container}>
|
<View style={styles.container}>
|
||||||
<Header title="My Orders" />
|
<Header title="My Orders" />
|
||||||
<View style={styles.tabRow}>
|
<View style={styles.tabRow}>
|
||||||
{TABS.map((tab) => (
|
{TABS.map(tab => (
|
||||||
<TouchableOpacity
|
<TouchableOpacity
|
||||||
key={tab}
|
key={tab}
|
||||||
style={[
|
style={[styles.tab, activeTab === tab && styles.tabActive]}
|
||||||
styles.tab,
|
|
||||||
activeTab === tab && styles.tabActive,
|
|
||||||
]}
|
|
||||||
onPress={() => setActiveTab(tab)}
|
onPress={() => setActiveTab(tab)}
|
||||||
activeOpacity={0.7}
|
activeOpacity={0.7}
|
||||||
>
|
>
|
||||||
@ -65,25 +105,36 @@ export const MyOrdersScreen: React.FC = () => {
|
|||||||
</View>
|
</View>
|
||||||
<FlatList
|
<FlatList
|
||||||
data={filteredOrders}
|
data={filteredOrders}
|
||||||
keyExtractor={(item) => item.id}
|
keyExtractor={item => item.id}
|
||||||
renderItem={({ item }) => (
|
renderItem={({ item }) => (
|
||||||
<OrderHistoryCard
|
<OrderHistoryCard
|
||||||
providerName={item.providerName}
|
providerName={item?.merchant?.name || ''}
|
||||||
providerImage={item.providerImage}
|
providerImage={item?.merchant?.imageUrl || ''}
|
||||||
orderDate={item.orderDate}
|
orderDate={formatDate(item?.createdAt)}
|
||||||
status={item.status}
|
status={item.status}
|
||||||
total={item.total}
|
total={Number(item?.totalAmount) || 0}
|
||||||
|
items={item.orderItems}
|
||||||
onReorder={() => {
|
onReorder={() => {
|
||||||
navigation.navigate('ProviderDetailsScreen', {
|
navigation.navigate('ProviderDetailsScreen', {
|
||||||
providerId: 'p1',
|
providerId: 'p1',
|
||||||
providerName: item.providerName,
|
providerName: item?.merchant?.name || '',
|
||||||
});
|
});
|
||||||
}}
|
}}
|
||||||
onDetails={() => {
|
onDetails={() => {
|
||||||
if (item.status === 'Ongoing') {
|
if (
|
||||||
navigation.navigate('OrderTrackingScreen', { orderId: item.id });
|
item?.status === 'PENDING' ||
|
||||||
|
item?.status === 'CONFIRMED' ||
|
||||||
|
item?.status === 'PREPARING' ||
|
||||||
|
item?.status === 'READY_FOR_PICKUP' ||
|
||||||
|
item?.status === 'OUT_FOR_DELIVERY'
|
||||||
|
) {
|
||||||
|
navigation.navigate('OrderDetailsScreen', {
|
||||||
|
orderId: item.id,
|
||||||
|
});
|
||||||
} else {
|
} else {
|
||||||
navigation.navigate('OrderDeliveredScreen', { orderId: item.id });
|
navigation.navigate('OrderDeliveredScreen', {
|
||||||
|
orderId: item.id,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
|||||||
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;
|
gender: Gender | null;
|
||||||
dateOfBirth: string | null;
|
dateOfBirth: string | null;
|
||||||
role: Role;
|
role: Role;
|
||||||
merchants: Merchant[];
|
// merchants: Merchant[];
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface Role {
|
export interface Role {
|
||||||
@ -50,7 +50,8 @@ export interface CategoryPreference {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export interface Merchant {
|
export interface Merchant {
|
||||||
// Extend when backend starts returning data
|
name: string;
|
||||||
|
imageUrl: string | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
export type RoleEnum = 'CUSTOMER';
|
export type RoleEnum = 'CUSTOMER';
|
||||||
|
|||||||
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;
|
quantity: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
export type OrderStatus =
|
// export type OrderStatus =
|
||||||
| 'placed'
|
// | 'placed'
|
||||||
| 'confirmed'
|
// | 'confirmed'
|
||||||
| 'dispatched'
|
// | 'dispatched'
|
||||||
| 'delivered'
|
// | 'delivered'
|
||||||
| 'cancelled';
|
// | 'cancelled';
|
||||||
|
|
||||||
export interface Order {
|
// export interface Order {
|
||||||
id: string;
|
// id: string;
|
||||||
providerId: string;
|
// providerId: string;
|
||||||
providerName: string;
|
// providerName: string;
|
||||||
providerImage: string;
|
// providerImage: string;
|
||||||
items: CartItem[];
|
// items: CartItem[];
|
||||||
status: OrderStatus;
|
// status: OrderStatus;
|
||||||
total: number;
|
// total: number;
|
||||||
deliveryFee: number;
|
// deliveryFee: number;
|
||||||
platformFee: number;
|
// platformFee: number;
|
||||||
discount: number;
|
// discount: number;
|
||||||
couponCode?: string;
|
// couponCode?: string;
|
||||||
createdAt: string;
|
// createdAt: string;
|
||||||
estimatedDelivery: string;
|
// estimatedDelivery: string;
|
||||||
deliveryAddress: Location;
|
// deliveryAddress: Location;
|
||||||
}
|
// }
|
||||||
|
|
||||||
export interface TrackingStep {
|
// export interface TrackingStep {
|
||||||
key: OrderStatus;
|
// key: OrderStatus;
|
||||||
label: string;
|
// label: string;
|
||||||
timestamp?: string;
|
// timestamp?: string;
|
||||||
isCompleted: boolean;
|
// isCompleted: boolean;
|
||||||
isActive: boolean;
|
// isActive: boolean;
|
||||||
}
|
// }
|
||||||
|
|
||||||
export interface DeliveryAgent {
|
export interface DeliveryAgent {
|
||||||
name: string;
|
name: string;
|
||||||
@ -93,3 +93,6 @@ export * from './auth';
|
|||||||
export * from './onboard';
|
export * from './onboard';
|
||||||
export * from './product';
|
export * from './product';
|
||||||
export * from './cart';
|
export * from './cart';
|
||||||
|
export * from './paymentMethods';
|
||||||
|
export * from './customerProfile';
|
||||||
|
export * from './order';
|
||||||
|
|||||||
187
app/interfaces/order.ts
Normal file
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,
|
OrderDeliveredScreen,
|
||||||
HelpSupportScreen,
|
HelpSupportScreen,
|
||||||
WriteReviewScreen,
|
WriteReviewScreen,
|
||||||
|
OrderDetailsScreen,
|
||||||
} from '@features/screens';
|
} from '@features/screens';
|
||||||
|
|
||||||
export type AppStackParamList = {
|
export type AppStackParamList = {
|
||||||
@ -22,13 +23,14 @@ export type AppStackParamList = {
|
|||||||
ProviderDetailsScreen: { providerId: string; providerName?: string };
|
ProviderDetailsScreen: { providerId: string; providerName?: string };
|
||||||
CartScreen: undefined;
|
CartScreen: undefined;
|
||||||
CheckoutAddressScreen: undefined;
|
CheckoutAddressScreen: undefined;
|
||||||
CheckoutPaymentScreen: undefined;
|
CheckoutPaymentScreen: { selectedAddressId?: string } | undefined;
|
||||||
OrderConfirmedScreen: { orderId?: string } | undefined;
|
OrderConfirmedScreen: { orderId?: string } | undefined;
|
||||||
OrderTrackingScreen: { orderId?: string } | undefined;
|
OrderTrackingScreen: { orderId?: string } | undefined;
|
||||||
LiveTrackingScreen: { orderId?: string } | undefined;
|
LiveTrackingScreen: { orderId?: string } | undefined;
|
||||||
OrderDeliveredScreen: { orderId?: string } | undefined;
|
OrderDeliveredScreen: { orderId?: string } | undefined;
|
||||||
HelpSupportScreen: undefined;
|
HelpSupportScreen: undefined;
|
||||||
WriteReviewScreen: { productId: string } | undefined;
|
WriteReviewScreen: { productId: string } | undefined;
|
||||||
|
OrderDetailsScreen: { orderId: string } | undefined;
|
||||||
};
|
};
|
||||||
|
|
||||||
const Stack = createStackNavigator<AppStackParamList>();
|
const Stack = createStackNavigator<AppStackParamList>();
|
||||||
@ -66,6 +68,10 @@ export const AppStack: React.FC = () => {
|
|||||||
/>
|
/>
|
||||||
<Stack.Screen name="HelpSupportScreen" component={HelpSupportScreen} />
|
<Stack.Screen name="HelpSupportScreen" component={HelpSupportScreen} />
|
||||||
<Stack.Screen name="WriteReviewScreen" component={WriteReviewScreen} />
|
<Stack.Screen name="WriteReviewScreen" component={WriteReviewScreen} />
|
||||||
|
<Stack.Screen
|
||||||
|
name="OrderDetailsScreen"
|
||||||
|
component={OrderDetailsScreen}
|
||||||
|
/>
|
||||||
</Stack.Navigator>
|
</Stack.Navigator>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@ -15,7 +15,7 @@ const STORAGE_KEYS = {
|
|||||||
} as const;
|
} as const;
|
||||||
|
|
||||||
// ─── Config ──────────────────────────────────────────────────────────────────
|
// ─── Config ──────────────────────────────────────────────────────────────────
|
||||||
const BASE_URL = 'https://2e1b-202-8-116-13.ngrok-free.app'; // TODO: replace with your actual base URL
|
const BASE_URL = 'https://9b5b-202-8-116-13.ngrok-free.app'; // TODO: replace with your actual base URL
|
||||||
|
|
||||||
// ─── Token Helpers ───────────────────────────────────────────────────────────
|
// ─── Token Helpers ───────────────────────────────────────────────────────────
|
||||||
export const tokenManager = {
|
export const tokenManager = {
|
||||||
|
|||||||
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 './auth';
|
||||||
export * from './cart';
|
export * from './cart';
|
||||||
export * from './order';
|
export * from './order';
|
||||||
|
export * from './customerProfile';
|
||||||
|
|||||||
@ -8,6 +8,8 @@ import completeProfileReducer from '@features/screens/completeProfileScreen/redu
|
|||||||
import authReducer from './commonreducers/auth/reducer';
|
import authReducer from './commonreducers/auth/reducer';
|
||||||
import homeReducer from '@features/screens/homeScreen/reducer';
|
import homeReducer from '@features/screens/homeScreen/reducer';
|
||||||
import providerDetailsReducer from '@features/screens/providerDetailsScreen/reducer';
|
import providerDetailsReducer from '@features/screens/providerDetailsScreen/reducer';
|
||||||
|
import paymentMethodsReducer from '@features/screens/checkoutPaymentScreen/reducer';
|
||||||
|
import customerProfileReducer from './commonreducers/customerProfile/reducer';
|
||||||
|
|
||||||
const rootReducer = combineReducers({
|
const rootReducer = combineReducers({
|
||||||
auth: authReducer,
|
auth: authReducer,
|
||||||
@ -18,6 +20,8 @@ const rootReducer = combineReducers({
|
|||||||
completeProfile: completeProfileReducer,
|
completeProfile: completeProfileReducer,
|
||||||
home: homeReducer,
|
home: homeReducer,
|
||||||
providerDetails: providerDetailsReducer,
|
providerDetails: providerDetailsReducer,
|
||||||
|
paymentMethods: paymentMethodsReducer,
|
||||||
|
customerProfile: customerProfileReducer,
|
||||||
});
|
});
|
||||||
|
|
||||||
export type RootState = ReturnType<typeof rootReducer>;
|
export type RootState = ReturnType<typeof rootReducer>;
|
||||||
|
|||||||
@ -1,5 +1,31 @@
|
|||||||
export const getFullUrl = (url?: string) => {
|
export const getFullUrl = (url?: string) => {
|
||||||
const BASE_URL = 'https://2e1b-202-8-116-13.ngrok-free.app';
|
const BASE_URL = 'https://9b5b-202-8-116-13.ngrok-free.app';
|
||||||
if (!url) return '';
|
if (!url) return '';
|
||||||
return url.startsWith('/') ? `${BASE_URL}${url}` : url;
|
return url.startsWith('/') ? `${BASE_URL}${url}` : url;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const formatTime = (dateString?: string) => {
|
||||||
|
if (!dateString) return '';
|
||||||
|
|
||||||
|
const dateObj = new Date(dateString);
|
||||||
|
|
||||||
|
// Example output: 11:57 AM
|
||||||
|
return dateObj.toLocaleTimeString('en-US', {
|
||||||
|
hour: 'numeric',
|
||||||
|
minute: '2-digit',
|
||||||
|
hour12: true,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
export const formatDate = (dateString?: string) => {
|
||||||
|
if (!dateString) return '';
|
||||||
|
|
||||||
|
const dateObj = new Date(dateString);
|
||||||
|
|
||||||
|
// Example output: July 8, 2026
|
||||||
|
return dateObj.toLocaleDateString('en-US', {
|
||||||
|
month: 'long',
|
||||||
|
day: 'numeric',
|
||||||
|
year: 'numeric',
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|||||||
@ -27,6 +27,7 @@
|
|||||||
"react-native-safe-area-context": "^5.8.0",
|
"react-native-safe-area-context": "^5.8.0",
|
||||||
"react-native-screens": "^4.25.2",
|
"react-native-screens": "^4.25.2",
|
||||||
"react-native-svg": "^15.15.5",
|
"react-native-svg": "^15.15.5",
|
||||||
|
"react-native-uuid": "^2.0.4",
|
||||||
"react-native-worklets": "^0.10.0",
|
"react-native-worklets": "^0.10.0",
|
||||||
"react-redux": "^9.3.0",
|
"react-redux": "^9.3.0",
|
||||||
"reactotron-react-native": "^5.2.0",
|
"reactotron-react-native": "^5.2.0",
|
||||||
|
|||||||
@ -5858,6 +5858,11 @@ react-native-svg@^15.15.5:
|
|||||||
css-select "^5.1.0"
|
css-select "^5.1.0"
|
||||||
css-tree "^1.1.3"
|
css-tree "^1.1.3"
|
||||||
|
|
||||||
|
react-native-uuid@^2.0.4:
|
||||||
|
version "2.0.4"
|
||||||
|
resolved "https://registry.yarnpkg.com/react-native-uuid/-/react-native-uuid-2.0.4.tgz#0c4f345523c5feac0282d3a51fe19887727df988"
|
||||||
|
integrity sha512-LSJNeh559qC17fgVPBsWuTSW/OygFp2dwTcf94IQBLYft5FzIQS9pCsuT36OPvyvDOMb6yiGr6TafaJDnz9PPQ==
|
||||||
|
|
||||||
react-native-worklets@^0.10.0:
|
react-native-worklets@^0.10.0:
|
||||||
version "0.10.1"
|
version "0.10.1"
|
||||||
resolved "https://registry.yarnpkg.com/react-native-worklets/-/react-native-worklets-0.10.1.tgz#7a3038a3b5ec66cab030a00e1568b6599696f5f2"
|
resolved "https://registry.yarnpkg.com/react-native-worklets/-/react-native-worklets-0.10.1.tgz#7a3038a3b5ec66cab030a00e1568b6599696f5f2"
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user