Compare commits
No commits in common. "feat(api-integration)" and "main" have entirely different histories.
feat(api-i
...
main
@ -1,5 +0,0 @@
|
|||||||
import Reactotron from 'reactotron-react-native';
|
|
||||||
|
|
||||||
Reactotron.configure() // controls connection & communication settings
|
|
||||||
.useReactNative() // add all built-in react native plugins
|
|
||||||
.connect(); // let's connect!
|
|
||||||
@ -1,8 +1,20 @@
|
|||||||
import { apiClient } from '../services/apiClient';
|
import { apiClient } from '../services/apiClient';
|
||||||
import { LoginResponse, User, VerifyOtpResponse } from '../interfaces';
|
import { User } from '../interfaces';
|
||||||
|
|
||||||
// ─── Types ───────────────────────────────────────────────────────────────────
|
// ─── Types ───────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
export interface LoginResponse {
|
||||||
|
success: boolean;
|
||||||
|
message: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface VerifyOtpResponse {
|
||||||
|
success: boolean;
|
||||||
|
user: User;
|
||||||
|
accessToken: string;
|
||||||
|
refreshToken: string;
|
||||||
|
}
|
||||||
|
|
||||||
export interface UpdateProfileResponse {
|
export interface UpdateProfileResponse {
|
||||||
success: boolean;
|
success: boolean;
|
||||||
user: User;
|
user: User;
|
||||||
@ -14,11 +26,11 @@ export interface LogoutResponse {
|
|||||||
|
|
||||||
// ─── Endpoints ───────────────────────────────────────────────────────────────
|
// ─── Endpoints ───────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
export const loginApi = (phone: string) =>
|
export const loginApi = (mobileNumber: string) =>
|
||||||
apiClient.post<LoginResponse>('/auth/otp/request', { phone });
|
apiClient.post<LoginResponse>('/auth/login', { mobileNumber });
|
||||||
|
|
||||||
export const verifyOtpApi = (phone: string, code: string, role: string) =>
|
export const verifyOtpApi = (mobileNumber: string, otp: string) =>
|
||||||
apiClient.post<VerifyOtpResponse>('/auth/otp/verify', { phone, code, role });
|
apiClient.post<VerifyOtpResponse>('/auth/verify-otp', { mobileNumber, otp });
|
||||||
|
|
||||||
export const updateProfileApi = (data: {
|
export const updateProfileApi = (data: {
|
||||||
name: string;
|
name: string;
|
||||||
|
|||||||
@ -1,16 +0,0 @@
|
|||||||
import { AddToCartRequest, CartResponse } from '@interfaces/cart';
|
|
||||||
import { apiClient } from '@services';
|
|
||||||
|
|
||||||
export const addToCartApi = async (addToCartRequest: AddToCartRequest) => {
|
|
||||||
return apiClient.post<CartResponse>('/cart/items', addToCartRequest);
|
|
||||||
};
|
|
||||||
|
|
||||||
export const getCartApi = async () => {
|
|
||||||
return apiClient.get<CartResponse>('/cart');
|
|
||||||
};
|
|
||||||
|
|
||||||
export const updateCartItem = async (productId: string, quantity: number) => {
|
|
||||||
return apiClient.patch<CartResponse>(`/cart/items/${productId}`, {
|
|
||||||
quantity,
|
|
||||||
});
|
|
||||||
};
|
|
||||||
@ -1,6 +0,0 @@
|
|||||||
import { CustomerResponse } from '@interfaces';
|
|
||||||
import { apiClient } from '@services';
|
|
||||||
|
|
||||||
export const getCustomerDetails = async () => {
|
|
||||||
return await apiClient.get<CustomerResponse>('/customers/profile');
|
|
||||||
};
|
|
||||||
@ -1,11 +1,5 @@
|
|||||||
import { apiClient } from '../services/apiClient';
|
import { apiClient } from '../services/apiClient';
|
||||||
import {
|
import { Provider, CatalogItem, Order, DeliveryAgent, Location } from '../interfaces';
|
||||||
Provider,
|
|
||||||
CatalogItem,
|
|
||||||
Order,
|
|
||||||
DeliveryAgent,
|
|
||||||
Location,
|
|
||||||
} from '../interfaces';
|
|
||||||
|
|
||||||
// ─── Types ───────────────────────────────────────────────────────────────────
|
// ─── Types ───────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
@ -16,7 +10,8 @@ export interface PlaceOrderResponse {
|
|||||||
|
|
||||||
// ─── Endpoints ───────────────────────────────────────────────────────────────
|
// ─── Endpoints ───────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
export const getProvidersApi = () => apiClient.get<Provider[]>('/providers');
|
export const getProvidersApi = () =>
|
||||||
|
apiClient.get<Provider[]>('/providers');
|
||||||
|
|
||||||
export const getProviderCatalogApi = (providerId: string) =>
|
export const getProviderCatalogApi = (providerId: string) =>
|
||||||
apiClient.get<CatalogItem[]>(`/providers/${providerId}/catalog`);
|
apiClient.get<CatalogItem[]>(`/providers/${providerId}/catalog`);
|
||||||
@ -24,8 +19,8 @@ export const getProviderCatalogApi = (providerId: string) =>
|
|||||||
export const searchProvidersApi = (query: string) =>
|
export const searchProvidersApi = (query: string) =>
|
||||||
apiClient.get<Provider[]>(`/search?q=${query}`);
|
apiClient.get<Provider[]>(`/search?q=${query}`);
|
||||||
|
|
||||||
// export const placeOrderApi = (orderData: Partial<Order>) =>
|
export const placeOrderApi = (orderData: Partial<Order>) =>
|
||||||
// apiClient.post<PlaceOrderResponse>('/orders', orderData);
|
apiClient.post<PlaceOrderResponse>('/orders', orderData);
|
||||||
|
|
||||||
export const getDeliveryAgentApi = () =>
|
export const getDeliveryAgentApi = () =>
|
||||||
apiClient.get<DeliveryAgent>('/delivery/agent');
|
apiClient.get<DeliveryAgent>('/delivery/agent');
|
||||||
|
|||||||
@ -1,8 +1,2 @@
|
|||||||
export * from './authApi';
|
export * from './authApi';
|
||||||
export * from './deliveryApi';
|
export * from './deliveryApi';
|
||||||
export * from './onboardApi';
|
|
||||||
export * from './productApi';
|
|
||||||
export * from './cartApi';
|
|
||||||
export * from './paymentMethodsApi';
|
|
||||||
export * from './customerDetailsApi';
|
|
||||||
export * from './orderApi';
|
|
||||||
|
|||||||
@ -1,12 +0,0 @@
|
|||||||
import { Category, onBoardPayload, UserProfile } from '@interfaces/onboard';
|
|
||||||
import { apiClient } from '@services';
|
|
||||||
|
|
||||||
export const getCatagories = async (): Promise<Category[]> => {
|
|
||||||
return await apiClient.get('/categories');
|
|
||||||
};
|
|
||||||
|
|
||||||
export const onBoardComplete = async (
|
|
||||||
payload: onBoardPayload,
|
|
||||||
): Promise<UserProfile> => {
|
|
||||||
return await apiClient.post('/auth/onboard', payload);
|
|
||||||
};
|
|
||||||
@ -1,14 +0,0 @@
|
|||||||
import { apiClient } from '@services';
|
|
||||||
import { Order, OrderRequest, PlaceOrderResponse } from '@interfaces';
|
|
||||||
|
|
||||||
export const placeOrderApi = async (payload: OrderRequest) => {
|
|
||||||
return await apiClient.post<PlaceOrderResponse>('/orders', payload);
|
|
||||||
};
|
|
||||||
|
|
||||||
export const getOrderHistoryApi = async () => {
|
|
||||||
return await apiClient.get<Order[]>('/orders');
|
|
||||||
};
|
|
||||||
|
|
||||||
export const getOrderByIdApi = async (orderId: string) => {
|
|
||||||
return await apiClient.get<Order>(`/orders/${orderId}`);
|
|
||||||
};
|
|
||||||
@ -1,6 +0,0 @@
|
|||||||
import { PaymentMethodsResponse } from '@interfaces';
|
|
||||||
import { apiClient } from '@services';
|
|
||||||
|
|
||||||
export const getAllPaymentMethodsApi = async () => {
|
|
||||||
return await apiClient.get<PaymentMethodsResponse>('/payments/options');
|
|
||||||
};
|
|
||||||
@ -1,17 +0,0 @@
|
|||||||
import { Products, PaginationMeta, Product } from '@interfaces';
|
|
||||||
import { apiClient } from '@services';
|
|
||||||
|
|
||||||
export interface GetProductsResponse {
|
|
||||||
products: Products[];
|
|
||||||
meta: PaginationMeta;
|
|
||||||
}
|
|
||||||
|
|
||||||
export const getProductsApi = async (): Promise<GetProductsResponse> => {
|
|
||||||
return await apiClient.get<GetProductsResponse>(`/products`);
|
|
||||||
};
|
|
||||||
|
|
||||||
export const getProductDetailsApi = async (
|
|
||||||
productId: string,
|
|
||||||
): Promise<Product> => {
|
|
||||||
return await apiClient.get<Product>(`/products/${productId}`);
|
|
||||||
};
|
|
||||||
@ -1,7 +0,0 @@
|
|||||||
import { OrderStatus, TrackingEntry } from '@interfaces';
|
|
||||||
|
|
||||||
export interface OrderStatusTimelineProps {
|
|
||||||
tracking?: TrackingEntry[];
|
|
||||||
currentStatus: OrderStatus;
|
|
||||||
styles?: any; // optional – component uses its own internal styles via useAppTheme
|
|
||||||
}
|
|
||||||
@ -1,317 +0,0 @@
|
|||||||
import { StyleSheet } from 'react-native';
|
|
||||||
import { typography } from '@theme';
|
|
||||||
|
|
||||||
export const getStyles = (colors: any) =>
|
|
||||||
StyleSheet.create({
|
|
||||||
// Status Card / Container
|
|
||||||
cardContainer: {
|
|
||||||
backgroundColor: colors.cardBg,
|
|
||||||
borderRadius: 16,
|
|
||||||
borderWidth: 1,
|
|
||||||
borderColor: colors.border,
|
|
||||||
overflow: 'hidden',
|
|
||||||
},
|
|
||||||
headerSection: {
|
|
||||||
padding: 16,
|
|
||||||
flexDirection: 'row',
|
|
||||||
alignItems: 'center',
|
|
||||||
borderBottomWidth: 1,
|
|
||||||
borderBottomColor: colors.border,
|
|
||||||
},
|
|
||||||
headerIconContainer: {
|
|
||||||
width: 48,
|
|
||||||
height: 48,
|
|
||||||
borderRadius: 24,
|
|
||||||
backgroundColor: '#E8F5E9',
|
|
||||||
justifyContent: 'center',
|
|
||||||
alignItems: 'center',
|
|
||||||
marginRight: 16,
|
|
||||||
},
|
|
||||||
headerIconText: {
|
|
||||||
fontSize: 24,
|
|
||||||
},
|
|
||||||
headerTextContainer: {
|
|
||||||
flex: 1,
|
|
||||||
},
|
|
||||||
statusHeaderTitle: {
|
|
||||||
fontSize: typography.fontSize.lg,
|
|
||||||
fontWeight: typography.fontWeight.bold,
|
|
||||||
color: colors.text,
|
|
||||||
marginBottom: 4,
|
|
||||||
},
|
|
||||||
statusHeaderDesc: {
|
|
||||||
fontSize: typography.fontSize.sm,
|
|
||||||
color: colors.textSecondary,
|
|
||||||
lineHeight: 18,
|
|
||||||
},
|
|
||||||
|
|
||||||
// Horizontal Stepper
|
|
||||||
stepperWrapper: {
|
|
||||||
paddingVertical: 20,
|
|
||||||
paddingHorizontal: 12,
|
|
||||||
borderBottomWidth: 1,
|
|
||||||
borderBottomColor: colors.border,
|
|
||||||
},
|
|
||||||
stepperRow: {
|
|
||||||
flexDirection: 'row',
|
|
||||||
alignItems: 'center',
|
|
||||||
justifyContent: 'space-between',
|
|
||||||
position: 'relative',
|
|
||||||
},
|
|
||||||
stepLineBackground: {
|
|
||||||
position: 'absolute',
|
|
||||||
top: 14,
|
|
||||||
left: '12%',
|
|
||||||
right: '12%',
|
|
||||||
height: 3,
|
|
||||||
backgroundColor: colors.border,
|
|
||||||
zIndex: 1,
|
|
||||||
},
|
|
||||||
stepLineActive: {
|
|
||||||
position: 'absolute',
|
|
||||||
top: 14,
|
|
||||||
left: '12%',
|
|
||||||
height: 3,
|
|
||||||
backgroundColor: colors.primary,
|
|
||||||
zIndex: 2,
|
|
||||||
},
|
|
||||||
stepItem: {
|
|
||||||
alignItems: 'center',
|
|
||||||
flex: 1,
|
|
||||||
zIndex: 3,
|
|
||||||
},
|
|
||||||
stepDot: {
|
|
||||||
width: 28,
|
|
||||||
height: 28,
|
|
||||||
borderRadius: 14,
|
|
||||||
backgroundColor: colors.cardBg,
|
|
||||||
borderWidth: 2,
|
|
||||||
borderColor: colors.border,
|
|
||||||
justifyContent: 'center',
|
|
||||||
alignItems: 'center',
|
|
||||||
marginBottom: 6,
|
|
||||||
},
|
|
||||||
stepDotCompleted: {
|
|
||||||
borderColor: colors.primary,
|
|
||||||
backgroundColor: colors.primary,
|
|
||||||
},
|
|
||||||
stepDotActive: {
|
|
||||||
borderColor: colors.primary,
|
|
||||||
backgroundColor: colors.cardBg,
|
|
||||||
borderWidth: 3,
|
|
||||||
},
|
|
||||||
stepCheckmark: {
|
|
||||||
color: '#FFFFFF',
|
|
||||||
fontSize: 12,
|
|
||||||
fontWeight: 'bold',
|
|
||||||
},
|
|
||||||
stepInnerDotActive: {
|
|
||||||
width: 10,
|
|
||||||
height: 10,
|
|
||||||
borderRadius: 5,
|
|
||||||
backgroundColor: colors.primary,
|
|
||||||
},
|
|
||||||
stepLabel: {
|
|
||||||
fontSize: 11,
|
|
||||||
fontWeight: typography.fontWeight.medium,
|
|
||||||
color: colors.textSecondary,
|
|
||||||
textAlign: 'center',
|
|
||||||
},
|
|
||||||
stepLabelActive: {
|
|
||||||
color: colors.primary,
|
|
||||||
fontWeight: typography.fontWeight.bold,
|
|
||||||
},
|
|
||||||
|
|
||||||
// Driver Section
|
|
||||||
driverSection: {
|
|
||||||
padding: 16,
|
|
||||||
borderBottomWidth: 1,
|
|
||||||
borderBottomColor: colors.border,
|
|
||||||
backgroundColor: colors.inputBg,
|
|
||||||
},
|
|
||||||
driverInfoRow: {
|
|
||||||
flexDirection: 'row',
|
|
||||||
alignItems: 'center',
|
|
||||||
marginBottom: 12,
|
|
||||||
},
|
|
||||||
driverAvatarContainer: {
|
|
||||||
width: 44,
|
|
||||||
height: 44,
|
|
||||||
borderRadius: 22,
|
|
||||||
backgroundColor: colors.primary,
|
|
||||||
justifyContent: 'center',
|
|
||||||
alignItems: 'center',
|
|
||||||
marginRight: 12,
|
|
||||||
},
|
|
||||||
driverAvatarText: {
|
|
||||||
color: '#FFFFFF',
|
|
||||||
fontSize: 16,
|
|
||||||
fontWeight: typography.fontWeight.bold,
|
|
||||||
},
|
|
||||||
driverMeta: {
|
|
||||||
flex: 1,
|
|
||||||
},
|
|
||||||
driverRoleText: {
|
|
||||||
fontSize: 10,
|
|
||||||
textTransform: 'uppercase',
|
|
||||||
letterSpacing: 0.5,
|
|
||||||
color: colors.textSecondary,
|
|
||||||
fontWeight: typography.fontWeight.semibold,
|
|
||||||
marginBottom: 2,
|
|
||||||
},
|
|
||||||
driverNameText: {
|
|
||||||
fontSize: typography.fontSize.md,
|
|
||||||
fontWeight: typography.fontWeight.bold,
|
|
||||||
color: colors.text,
|
|
||||||
},
|
|
||||||
driverVehicleText: {
|
|
||||||
fontSize: typography.fontSize.xs,
|
|
||||||
color: colors.textSecondary,
|
|
||||||
marginTop: 2,
|
|
||||||
},
|
|
||||||
ratingBadge: {
|
|
||||||
flexDirection: 'row',
|
|
||||||
alignItems: 'center',
|
|
||||||
backgroundColor: '#FFF9C4',
|
|
||||||
paddingHorizontal: 8,
|
|
||||||
paddingVertical: 4,
|
|
||||||
borderRadius: 12,
|
|
||||||
},
|
|
||||||
ratingText: {
|
|
||||||
fontSize: 12,
|
|
||||||
fontWeight: typography.fontWeight.bold,
|
|
||||||
color: '#F57F17',
|
|
||||||
},
|
|
||||||
driverActionsRow: {
|
|
||||||
flexDirection: 'row',
|
|
||||||
justifyContent: 'space-between',
|
|
||||||
},
|
|
||||||
actionButton: {
|
|
||||||
flex: 1,
|
|
||||||
flexDirection: 'row',
|
|
||||||
height: 40,
|
|
||||||
borderRadius: 8,
|
|
||||||
justifyContent: 'center',
|
|
||||||
alignItems: 'center',
|
|
||||||
borderWidth: 1,
|
|
||||||
},
|
|
||||||
callButton: {
|
|
||||||
backgroundColor: colors.primary,
|
|
||||||
borderColor: colors.primary,
|
|
||||||
marginRight: 6,
|
|
||||||
},
|
|
||||||
messageButton: {
|
|
||||||
backgroundColor: colors.cardBg,
|
|
||||||
borderColor: colors.border,
|
|
||||||
marginLeft: 6,
|
|
||||||
},
|
|
||||||
actionIcon: {
|
|
||||||
marginRight: 6,
|
|
||||||
fontSize: 16,
|
|
||||||
},
|
|
||||||
callButtonText: {
|
|
||||||
color: '#FFFFFF',
|
|
||||||
fontWeight: typography.fontWeight.bold,
|
|
||||||
fontSize: typography.fontSize.sm,
|
|
||||||
},
|
|
||||||
messageButtonText: {
|
|
||||||
color: colors.text,
|
|
||||||
fontWeight: typography.fontWeight.semibold,
|
|
||||||
fontSize: typography.fontSize.sm,
|
|
||||||
},
|
|
||||||
|
|
||||||
// Collapsible Trigger / Detailed Logs
|
|
||||||
toggleSection: {
|
|
||||||
alignItems: 'center',
|
|
||||||
justifyContent: 'center',
|
|
||||||
paddingVertical: 12,
|
|
||||||
},
|
|
||||||
toggleButton: {
|
|
||||||
flexDirection: 'row',
|
|
||||||
alignItems: 'center',
|
|
||||||
paddingHorizontal: 16,
|
|
||||||
paddingVertical: 4,
|
|
||||||
},
|
|
||||||
toggleButtonText: {
|
|
||||||
fontSize: typography.fontSize.sm,
|
|
||||||
fontWeight: typography.fontWeight.bold,
|
|
||||||
color: colors.primary,
|
|
||||||
marginRight: 4,
|
|
||||||
},
|
|
||||||
toggleChevron: {
|
|
||||||
fontSize: 12,
|
|
||||||
color: colors.primary,
|
|
||||||
},
|
|
||||||
detailedTimelineWrapper: {
|
|
||||||
paddingHorizontal: 20,
|
|
||||||
paddingBottom: 16,
|
|
||||||
},
|
|
||||||
|
|
||||||
// Legacy vertical timeline classes (re-styled for premium feel)
|
|
||||||
timelineRow: {
|
|
||||||
flexDirection: 'row',
|
|
||||||
},
|
|
||||||
timelineDotColumn: {
|
|
||||||
alignItems: 'center',
|
|
||||||
width: 24,
|
|
||||||
},
|
|
||||||
timelineDot: {
|
|
||||||
width: 14,
|
|
||||||
height: 14,
|
|
||||||
borderRadius: 7,
|
|
||||||
borderWidth: 2,
|
|
||||||
borderColor: colors.border,
|
|
||||||
backgroundColor: colors.cardBg,
|
|
||||||
justifyContent: 'center',
|
|
||||||
alignItems: 'center',
|
|
||||||
marginTop: 4,
|
|
||||||
},
|
|
||||||
timelineDotCompleted: {
|
|
||||||
borderColor: colors.primary,
|
|
||||||
backgroundColor: colors.primary,
|
|
||||||
},
|
|
||||||
timelineDotCancelled: {
|
|
||||||
borderColor: colors.error,
|
|
||||||
backgroundColor: colors.error,
|
|
||||||
},
|
|
||||||
timelineCheck: {
|
|
||||||
color: '#FFFFFF',
|
|
||||||
fontSize: 8,
|
|
||||||
fontWeight: 'bold',
|
|
||||||
},
|
|
||||||
timelineLine: {
|
|
||||||
width: 2,
|
|
||||||
flex: 1,
|
|
||||||
minHeight: 36,
|
|
||||||
backgroundColor: colors.border,
|
|
||||||
marginVertical: 4,
|
|
||||||
},
|
|
||||||
timelineLineCompleted: {
|
|
||||||
backgroundColor: colors.primary,
|
|
||||||
},
|
|
||||||
timelineContent: {
|
|
||||||
flex: 1,
|
|
||||||
paddingBottom: 18,
|
|
||||||
paddingLeft: 16,
|
|
||||||
},
|
|
||||||
timelineLabelCompleted: {
|
|
||||||
fontSize: typography.fontSize.sm,
|
|
||||||
fontWeight: typography.fontWeight.semibold,
|
|
||||||
color: colors.text,
|
|
||||||
},
|
|
||||||
timelineLabelPending: {
|
|
||||||
fontSize: typography.fontSize.sm,
|
|
||||||
fontWeight: typography.fontWeight.regular,
|
|
||||||
color: colors.textSecondary,
|
|
||||||
},
|
|
||||||
timelineLabelCancelled: {
|
|
||||||
fontSize: typography.fontSize.sm,
|
|
||||||
fontWeight: typography.fontWeight.bold,
|
|
||||||
color: colors.error,
|
|
||||||
},
|
|
||||||
timelineTime: {
|
|
||||||
fontSize: typography.fontSize.xs,
|
|
||||||
color: colors.textSecondary,
|
|
||||||
marginTop: 4,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
@ -1,377 +0,0 @@
|
|||||||
import React, { useState } from 'react';
|
|
||||||
import {
|
|
||||||
View,
|
|
||||||
Text,
|
|
||||||
TouchableOpacity,
|
|
||||||
Linking,
|
|
||||||
Platform,
|
|
||||||
LayoutAnimation,
|
|
||||||
UIManager,
|
|
||||||
} from 'react-native';
|
|
||||||
import { OrderStatus, TrackingEntry } from '@interfaces';
|
|
||||||
import { formatDate, formatTime } from '@utils/helper';
|
|
||||||
import { OrderStatusTimelineProps } from './OrderStatusTimeline.props';
|
|
||||||
import { useAppTheme } from '@theme';
|
|
||||||
import { getStyles } from './OrderStatusTimeline.styles';
|
|
||||||
|
|
||||||
if (
|
|
||||||
Platform.OS === 'android' &&
|
|
||||||
UIManager.setLayoutAnimationEnabledExperimental
|
|
||||||
) {
|
|
||||||
UIManager.setLayoutAnimationEnabledExperimental(true);
|
|
||||||
}
|
|
||||||
|
|
||||||
export const STATUS_FLOW: OrderStatus[] = [
|
|
||||||
'PENDING',
|
|
||||||
'CONFIRMED',
|
|
||||||
'PREPARING',
|
|
||||||
'READY_FOR_PICKUP',
|
|
||||||
'OUT_FOR_DELIVERY',
|
|
||||||
'DELIVERED',
|
|
||||||
];
|
|
||||||
|
|
||||||
export const STATUS_LABEL: Record<OrderStatus, string> = {
|
|
||||||
PENDING: 'Order Placed',
|
|
||||||
CONFIRMED: 'Confirmed',
|
|
||||||
PREPARING: 'Preparing',
|
|
||||||
READY_FOR_PICKUP: 'Ready for Pickup',
|
|
||||||
OUT_FOR_DELIVERY: 'Out for Delivery',
|
|
||||||
DELIVERED: 'Delivered',
|
|
||||||
CANCELLED: 'Cancelled',
|
|
||||||
};
|
|
||||||
|
|
||||||
const STATUS_DETAILS: Record<
|
|
||||||
OrderStatus,
|
|
||||||
{ title: string; desc: string; icon: string }
|
|
||||||
> = {
|
|
||||||
PENDING: {
|
|
||||||
title: 'Waiting for Confirmation',
|
|
||||||
desc: 'The merchant is reviewing your order. We will start preparing it soon.',
|
|
||||||
icon: '⏳',
|
|
||||||
},
|
|
||||||
CONFIRMED: {
|
|
||||||
title: 'Order Confirmed',
|
|
||||||
desc: 'Your order has been accepted. The chef will start preparing it shortly.',
|
|
||||||
icon: '✅',
|
|
||||||
},
|
|
||||||
PREPARING: {
|
|
||||||
title: 'Preparing Your Food',
|
|
||||||
desc: 'The kitchen is preparing your delicious order with fresh ingredients.',
|
|
||||||
icon: '🍳',
|
|
||||||
},
|
|
||||||
READY_FOR_PICKUP: {
|
|
||||||
title: 'Ready for Pickup',
|
|
||||||
desc: 'Your order is ready. The delivery partner is picking it up now.',
|
|
||||||
icon: '📦',
|
|
||||||
},
|
|
||||||
OUT_FOR_DELIVERY: {
|
|
||||||
title: 'Out for Delivery',
|
|
||||||
desc: 'Our delivery partner is on the way with your food. Keep your phone handy!',
|
|
||||||
icon: '🛵',
|
|
||||||
},
|
|
||||||
DELIVERED: {
|
|
||||||
title: 'Order Delivered',
|
|
||||||
desc: 'Your order has been successfully delivered. Enjoy your meal!',
|
|
||||||
icon: '🎉',
|
|
||||||
},
|
|
||||||
CANCELLED: {
|
|
||||||
title: 'Order Cancelled',
|
|
||||||
desc: 'This order was cancelled. Any refund will be processed shortly.',
|
|
||||||
icon: '❌',
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
const getStepIndex = (status: OrderStatus): number => {
|
|
||||||
switch (status) {
|
|
||||||
case 'PENDING':
|
|
||||||
case 'CONFIRMED':
|
|
||||||
return 0;
|
|
||||||
case 'PREPARING':
|
|
||||||
return 1;
|
|
||||||
case 'READY_FOR_PICKUP':
|
|
||||||
case 'OUT_FOR_DELIVERY':
|
|
||||||
return 2;
|
|
||||||
case 'DELIVERED':
|
|
||||||
return 3;
|
|
||||||
default:
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
export const OrderStatusTimeline: React.FC<OrderStatusTimelineProps> = ({
|
|
||||||
tracking = [],
|
|
||||||
currentStatus,
|
|
||||||
styles: propStyles,
|
|
||||||
}) => {
|
|
||||||
const { colors } = useAppTheme();
|
|
||||||
const localStyles = getStyles(colors);
|
|
||||||
|
|
||||||
// If the caller passes the screen styles, they won't have timelineRow etc, so we fallback to localStyles
|
|
||||||
const styles =
|
|
||||||
propStyles && propStyles.timelineRow ? propStyles : localStyles;
|
|
||||||
|
|
||||||
const [expanded, setExpanded] = useState(false);
|
|
||||||
|
|
||||||
const toggleExpand = () => {
|
|
||||||
LayoutAnimation.configureNext(LayoutAnimation.Presets.easeInEaseOut);
|
|
||||||
setExpanded(!expanded);
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleCall = () => {
|
|
||||||
Linking.openURL('tel:+919876543210');
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleMessage = () => {
|
|
||||||
Linking.openURL('sms:+919876543210');
|
|
||||||
};
|
|
||||||
|
|
||||||
const trackingMap = tracking.reduce<Record<string, string>>((acc, t) => {
|
|
||||||
acc[t.status] = t.time;
|
|
||||||
return acc;
|
|
||||||
}, {});
|
|
||||||
|
|
||||||
const details = STATUS_DETAILS[currentStatus] || STATUS_DETAILS.PENDING;
|
|
||||||
|
|
||||||
if (currentStatus === 'CANCELLED') {
|
|
||||||
return (
|
|
||||||
<View style={localStyles.cardContainer}>
|
|
||||||
{/* Cancelled Status Banner */}
|
|
||||||
<View style={localStyles.headerSection}>
|
|
||||||
<View
|
|
||||||
style={[
|
|
||||||
localStyles.headerIconContainer,
|
|
||||||
{ backgroundColor: '#FFEBEE' },
|
|
||||||
]}
|
|
||||||
>
|
|
||||||
<Text style={localStyles.headerIconText}>❌</Text>
|
|
||||||
</View>
|
|
||||||
<View style={localStyles.headerTextContainer}>
|
|
||||||
<Text
|
|
||||||
style={[localStyles.statusHeaderTitle, { color: colors.error }]}
|
|
||||||
>
|
|
||||||
{details.title}
|
|
||||||
</Text>
|
|
||||||
<Text style={localStyles.statusHeaderDesc}>{details.desc}</Text>
|
|
||||||
</View>
|
|
||||||
</View>
|
|
||||||
|
|
||||||
{/* Collapsible detailed view */}
|
|
||||||
<View style={localStyles.toggleSection}>
|
|
||||||
<TouchableOpacity
|
|
||||||
onPress={toggleExpand}
|
|
||||||
style={localStyles.toggleButton}
|
|
||||||
activeOpacity={0.7}
|
|
||||||
>
|
|
||||||
<Text style={localStyles.toggleButtonText}>
|
|
||||||
{expanded ? 'Hide Details' : 'View Detailed History'}
|
|
||||||
</Text>
|
|
||||||
<Text style={localStyles.toggleChevron}>
|
|
||||||
{expanded ? '▲' : '▼'}
|
|
||||||
</Text>
|
|
||||||
</TouchableOpacity>
|
|
||||||
</View>
|
|
||||||
|
|
||||||
{expanded && (
|
|
||||||
<View style={localStyles.detailedTimelineWrapper}>
|
|
||||||
<View style={styles.timelineRow}>
|
|
||||||
<View style={styles.timelineDotColumn}>
|
|
||||||
<View
|
|
||||||
style={[styles.timelineDot, styles.timelineDotCancelled]}
|
|
||||||
/>
|
|
||||||
</View>
|
|
||||||
<View style={styles.timelineContent}>
|
|
||||||
<Text style={styles.timelineLabelCancelled}>
|
|
||||||
Order Cancelled
|
|
||||||
</Text>
|
|
||||||
{trackingMap['CANCELLED'] && (
|
|
||||||
<Text style={styles.timelineTime}>
|
|
||||||
{formatDate(trackingMap['CANCELLED'])} at{' '}
|
|
||||||
{formatTime(trackingMap['CANCELLED'])}
|
|
||||||
</Text>
|
|
||||||
)}
|
|
||||||
</View>
|
|
||||||
</View>
|
|
||||||
</View>
|
|
||||||
)}
|
|
||||||
</View>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
const activeStep = getStepIndex(currentStatus);
|
|
||||||
const steps = [
|
|
||||||
{ label: 'Placed', status: 'PENDING' },
|
|
||||||
{ label: 'Preparing', status: 'PREPARING' },
|
|
||||||
{ label: 'On the Way', status: 'OUT_FOR_DELIVERY' },
|
|
||||||
{ label: 'Delivered', status: 'DELIVERED' },
|
|
||||||
];
|
|
||||||
|
|
||||||
const showDriver = [
|
|
||||||
'PREPARING',
|
|
||||||
'READY_FOR_PICKUP',
|
|
||||||
'OUT_FOR_DELIVERY',
|
|
||||||
'DELIVERED',
|
|
||||||
].includes(currentStatus);
|
|
||||||
const currentIndex = STATUS_FLOW.indexOf(currentStatus);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<View style={localStyles.cardContainer}>
|
|
||||||
{/* Current Status Header Banner */}
|
|
||||||
<View style={localStyles.headerSection}>
|
|
||||||
<View style={localStyles.headerIconContainer}>
|
|
||||||
<Text style={localStyles.headerIconText}>{details.icon}</Text>
|
|
||||||
</View>
|
|
||||||
<View style={localStyles.headerTextContainer}>
|
|
||||||
<Text style={localStyles.statusHeaderTitle}>{details.title}</Text>
|
|
||||||
<Text style={localStyles.statusHeaderDesc}>{details.desc}</Text>
|
|
||||||
</View>
|
|
||||||
</View>
|
|
||||||
|
|
||||||
{/* Horizontal Progress Stepper */}
|
|
||||||
<View style={localStyles.stepperWrapper}>
|
|
||||||
<View style={localStyles.stepperRow}>
|
|
||||||
<View style={localStyles.stepLineBackground} />
|
|
||||||
<View
|
|
||||||
style={[
|
|
||||||
localStyles.stepLineActive,
|
|
||||||
{ width: `${(activeStep / (steps.length - 1)) * 76}%` },
|
|
||||||
]}
|
|
||||||
/>
|
|
||||||
|
|
||||||
{steps.map((step, idx) => {
|
|
||||||
const isCompleted = idx < activeStep;
|
|
||||||
const isActive = idx === activeStep;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<View key={step.label} style={localStyles.stepItem}>
|
|
||||||
<View
|
|
||||||
style={[
|
|
||||||
localStyles.stepDot,
|
|
||||||
isCompleted && localStyles.stepDotCompleted,
|
|
||||||
isActive && localStyles.stepDotActive,
|
|
||||||
]}
|
|
||||||
>
|
|
||||||
{isCompleted ? (
|
|
||||||
<Text style={localStyles.stepCheckmark}>✓</Text>
|
|
||||||
) : isActive ? (
|
|
||||||
<View style={localStyles.stepInnerDotActive} />
|
|
||||||
) : null}
|
|
||||||
</View>
|
|
||||||
<Text
|
|
||||||
style={[
|
|
||||||
localStyles.stepLabel,
|
|
||||||
(isCompleted || isActive) && localStyles.stepLabelActive,
|
|
||||||
]}
|
|
||||||
>
|
|
||||||
{step.label}
|
|
||||||
</Text>
|
|
||||||
</View>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
</View>
|
|
||||||
</View>
|
|
||||||
|
|
||||||
{/* Delivery Partner Details (if driver assigned) */}
|
|
||||||
{showDriver && (
|
|
||||||
<View style={localStyles.driverSection}>
|
|
||||||
<View style={localStyles.driverInfoRow}>
|
|
||||||
<View style={localStyles.driverAvatarContainer}>
|
|
||||||
<Text style={localStyles.driverAvatarText}>RS</Text>
|
|
||||||
</View>
|
|
||||||
<View style={localStyles.driverMeta}>
|
|
||||||
<Text style={localStyles.driverRoleText}>Your Delivery Hero</Text>
|
|
||||||
<Text style={localStyles.driverNameText}>Rahul Sharma</Text>
|
|
||||||
<Text style={localStyles.driverVehicleText}>
|
|
||||||
KA-01-AB-1234 • Honda Activa
|
|
||||||
</Text>
|
|
||||||
</View>
|
|
||||||
<View style={localStyles.ratingBadge}>
|
|
||||||
<Text style={localStyles.ratingText}>⭐ 4.8</Text>
|
|
||||||
</View>
|
|
||||||
</View>
|
|
||||||
<View style={localStyles.driverActionsRow}>
|
|
||||||
<TouchableOpacity
|
|
||||||
style={[localStyles.actionButton, localStyles.callButton]}
|
|
||||||
onPress={handleCall}
|
|
||||||
activeOpacity={0.8}
|
|
||||||
>
|
|
||||||
<Text style={localStyles.actionIcon}>📞</Text>
|
|
||||||
<Text style={localStyles.callButtonText}>Call Partner</Text>
|
|
||||||
</TouchableOpacity>
|
|
||||||
<TouchableOpacity
|
|
||||||
style={[localStyles.actionButton, localStyles.messageButton]}
|
|
||||||
onPress={handleMessage}
|
|
||||||
activeOpacity={0.8}
|
|
||||||
>
|
|
||||||
<Text style={localStyles.actionIcon}>💬</Text>
|
|
||||||
<Text style={localStyles.messageButtonText}>Chat</Text>
|
|
||||||
</TouchableOpacity>
|
|
||||||
</View>
|
|
||||||
</View>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{/* View Detailed History Toggle */}
|
|
||||||
<View style={localStyles.toggleSection}>
|
|
||||||
<TouchableOpacity
|
|
||||||
onPress={toggleExpand}
|
|
||||||
style={localStyles.toggleButton}
|
|
||||||
activeOpacity={0.7}
|
|
||||||
>
|
|
||||||
<Text style={localStyles.toggleButtonText}>
|
|
||||||
{expanded ? 'Hide Details' : 'View Detailed History'}
|
|
||||||
</Text>
|
|
||||||
<Text style={localStyles.toggleChevron}>{expanded ? '▲' : '▼'}</Text>
|
|
||||||
</TouchableOpacity>
|
|
||||||
</View>
|
|
||||||
|
|
||||||
{/* Detailed Vertical Logs */}
|
|
||||||
{expanded && (
|
|
||||||
<View style={localStyles.detailedTimelineWrapper}>
|
|
||||||
{STATUS_FLOW.map((status, index) => {
|
|
||||||
const isCompleted = index <= currentIndex;
|
|
||||||
const isLast = index === STATUS_FLOW.length - 1;
|
|
||||||
const time = trackingMap[status];
|
|
||||||
|
|
||||||
return (
|
|
||||||
<View key={status} style={styles.timelineRow}>
|
|
||||||
<View style={styles.timelineDotColumn}>
|
|
||||||
<View
|
|
||||||
style={[
|
|
||||||
styles.timelineDot,
|
|
||||||
isCompleted && styles.timelineDotCompleted,
|
|
||||||
]}
|
|
||||||
>
|
|
||||||
{isCompleted && <Text style={styles.timelineCheck}>✓</Text>}
|
|
||||||
</View>
|
|
||||||
{!isLast && (
|
|
||||||
<View
|
|
||||||
style={[
|
|
||||||
styles.timelineLine,
|
|
||||||
index < currentIndex && styles.timelineLineCompleted,
|
|
||||||
]}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</View>
|
|
||||||
<View style={styles.timelineContent}>
|
|
||||||
<Text
|
|
||||||
style={
|
|
||||||
isCompleted
|
|
||||||
? styles.timelineLabelCompleted
|
|
||||||
: styles.timelineLabelPending
|
|
||||||
}
|
|
||||||
>
|
|
||||||
{STATUS_LABEL[status]}
|
|
||||||
</Text>
|
|
||||||
{time && (
|
|
||||||
<Text style={styles.timelineTime}>
|
|
||||||
{formatDate(time)} at {formatTime(time)}
|
|
||||||
</Text>
|
|
||||||
)}
|
|
||||||
</View>
|
|
||||||
</View>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
</View>
|
|
||||||
)}
|
|
||||||
</View>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
@ -1,2 +0,0 @@
|
|||||||
export * from './OrderStatusTimeline';
|
|
||||||
export type * from './OrderStatusTimeline.props';
|
|
||||||
@ -4,7 +4,6 @@ export * from './socialButton';
|
|||||||
export * from './header';
|
export * from './header';
|
||||||
export * from './searchBar';
|
export * from './searchBar';
|
||||||
export * from './providerCard';
|
export * from './providerCard';
|
||||||
export * from './productCard';
|
|
||||||
export * from './catalogItemRow';
|
export * from './catalogItemRow';
|
||||||
export * from './stepProgress';
|
export * from './stepProgress';
|
||||||
export * from './badge';
|
export * from './badge';
|
||||||
@ -13,4 +12,3 @@ export * from './categoryChip';
|
|||||||
export * from './ratingStars';
|
export * from './ratingStars';
|
||||||
export * from './orderHistoryCard';
|
export * from './orderHistoryCard';
|
||||||
export * from './paymentOption';
|
export * from './paymentOption';
|
||||||
export * from './OrderStatusTimeline';
|
|
||||||
|
|||||||
@ -1,5 +1,3 @@
|
|||||||
import { OrderItem } from '@interfaces';
|
|
||||||
|
|
||||||
export interface OrderHistoryCardProps {
|
export interface OrderHistoryCardProps {
|
||||||
providerName: string;
|
providerName: string;
|
||||||
providerImage: string;
|
providerImage: string;
|
||||||
@ -8,5 +6,4 @@ export interface OrderHistoryCardProps {
|
|||||||
total: number;
|
total: number;
|
||||||
onReorder?: () => void;
|
onReorder?: () => void;
|
||||||
onDetails?: () => void;
|
onDetails?: () => void;
|
||||||
items?: OrderItem[];
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -53,42 +53,12 @@ export const getStyles = (colors: any) => StyleSheet.create({
|
|||||||
fontWeight: typography.fontWeight.medium,
|
fontWeight: typography.fontWeight.medium,
|
||||||
color: colors.primary,
|
color: colors.primary,
|
||||||
},
|
},
|
||||||
itemsContainer: {
|
|
||||||
paddingVertical: 12,
|
|
||||||
borderTopWidth: 1,
|
|
||||||
borderTopColor: colors.border,
|
|
||||||
},
|
|
||||||
itemRow: {
|
|
||||||
flexDirection: 'row',
|
|
||||||
alignItems: 'flex-start',
|
|
||||||
marginBottom: 8,
|
|
||||||
},
|
|
||||||
itemQuantityBadge: {
|
|
||||||
backgroundColor: colors.surface,
|
|
||||||
borderWidth: 1,
|
|
||||||
borderColor: colors.border,
|
|
||||||
borderRadius: 4,
|
|
||||||
paddingHorizontal: 6,
|
|
||||||
paddingVertical: 2,
|
|
||||||
marginRight: 8,
|
|
||||||
marginTop: 2,
|
|
||||||
},
|
|
||||||
itemQuantityText: {
|
|
||||||
fontSize: typography.fontSize.xs,
|
|
||||||
color: colors.text,
|
|
||||||
fontWeight: typography.fontWeight.medium,
|
|
||||||
},
|
|
||||||
itemName: {
|
|
||||||
flex: 1,
|
|
||||||
fontSize: typography.fontSize.sm,
|
|
||||||
color: colors.text,
|
|
||||||
lineHeight: 20,
|
|
||||||
},
|
|
||||||
footer: {
|
footer: {
|
||||||
flexDirection: 'row',
|
flexDirection: 'row',
|
||||||
justifyContent: 'space-between',
|
justifyContent: 'space-between',
|
||||||
alignItems: 'center',
|
alignItems: 'center',
|
||||||
paddingTop: 12,
|
marginTop: 10,
|
||||||
|
paddingTop: 10,
|
||||||
borderTopWidth: 1,
|
borderTopWidth: 1,
|
||||||
borderTopColor: colors.border,
|
borderTopColor: colors.border,
|
||||||
},
|
},
|
||||||
|
|||||||
@ -12,7 +12,6 @@ export const OrderHistoryCard: React.FC<OrderHistoryCardProps> = ({
|
|||||||
total,
|
total,
|
||||||
onReorder,
|
onReorder,
|
||||||
onDetails,
|
onDetails,
|
||||||
items,
|
|
||||||
}) => {
|
}) => {
|
||||||
const { colors } = useAppTheme();
|
const { colors } = useAppTheme();
|
||||||
const styles = getStyles(colors);
|
const styles = getStyles(colors);
|
||||||
@ -31,20 +30,6 @@ export const OrderHistoryCard: React.FC<OrderHistoryCardProps> = ({
|
|||||||
<Text style={styles.statusText}>{status}</Text>
|
<Text style={styles.statusText}>{status}</Text>
|
||||||
</View>
|
</View>
|
||||||
</View>
|
</View>
|
||||||
{items && items.length > 0 && (
|
|
||||||
<View style={styles.itemsContainer}>
|
|
||||||
{items.map((item, index) => (
|
|
||||||
<View key={item.id || index} style={styles.itemRow}>
|
|
||||||
<View style={styles.itemQuantityBadge}>
|
|
||||||
<Text style={styles.itemQuantityText}>{item.quantity}x</Text>
|
|
||||||
</View>
|
|
||||||
<Text style={styles.itemName} numberOfLines={1}>
|
|
||||||
{item.name}
|
|
||||||
</Text>
|
|
||||||
</View>
|
|
||||||
))}
|
|
||||||
</View>
|
|
||||||
)}
|
|
||||||
<View style={styles.footer}>
|
<View style={styles.footer}>
|
||||||
<Text style={styles.total}>₹{total}</Text>
|
<Text style={styles.total}>₹{total}</Text>
|
||||||
<View style={styles.actions}>
|
<View style={styles.actions}>
|
||||||
|
|||||||
@ -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,50 +1,48 @@
|
|||||||
import { StyleSheet } from 'react-native';
|
import { StyleSheet } from 'react-native';
|
||||||
import { typography } from '@theme';
|
import { typography } from '@theme';
|
||||||
|
|
||||||
export const getStyles = (colors: any) =>
|
export const getStyles = (colors: any) => StyleSheet.create({
|
||||||
StyleSheet.create({
|
container: {
|
||||||
container: {
|
flexDirection: 'row',
|
||||||
flexDirection: 'row',
|
alignItems: 'center',
|
||||||
alignItems: 'center',
|
paddingVertical: 14,
|
||||||
paddingVertical: 14,
|
paddingHorizontal: 16,
|
||||||
paddingHorizontal: 16,
|
borderWidth: 1.5,
|
||||||
borderWidth: 1.5,
|
borderColor: colors.border,
|
||||||
borderColor: colors.border,
|
borderRadius: 12,
|
||||||
borderRadius: 12,
|
marginBottom: 10,
|
||||||
marginBottom: 10,
|
backgroundColor: colors.background,
|
||||||
backgroundColor: colors.background,
|
},
|
||||||
},
|
selected: {
|
||||||
selected: {
|
borderColor: colors.primary,
|
||||||
borderColor: colors.primary,
|
backgroundColor: '#E8F5E9',
|
||||||
backgroundColor: '#E8F5E9',
|
},
|
||||||
},
|
icon: {
|
||||||
icon: {
|
fontSize: 24,
|
||||||
width: 24,
|
marginRight: 12,
|
||||||
height: 24,
|
},
|
||||||
marginRight: 12,
|
label: {
|
||||||
},
|
flex: 1,
|
||||||
label: {
|
fontSize: typography.fontSize.md,
|
||||||
flex: 1,
|
fontWeight: typography.fontWeight.medium,
|
||||||
fontSize: typography.fontSize.md,
|
color: colors.text,
|
||||||
fontWeight: typography.fontWeight.medium,
|
},
|
||||||
color: colors.text,
|
radio: {
|
||||||
},
|
width: 22,
|
||||||
radio: {
|
height: 22,
|
||||||
width: 22,
|
borderRadius: 11,
|
||||||
height: 22,
|
borderWidth: 2,
|
||||||
borderRadius: 11,
|
borderColor: colors.border,
|
||||||
borderWidth: 2,
|
justifyContent: 'center',
|
||||||
borderColor: colors.border,
|
alignItems: 'center',
|
||||||
justifyContent: 'center',
|
},
|
||||||
alignItems: 'center',
|
radioSelected: {
|
||||||
},
|
borderColor: colors.primary,
|
||||||
radioSelected: {
|
},
|
||||||
borderColor: colors.primary,
|
radioInner: {
|
||||||
},
|
width: 12,
|
||||||
radioInner: {
|
height: 12,
|
||||||
width: 12,
|
borderRadius: 6,
|
||||||
height: 12,
|
backgroundColor: colors.primary,
|
||||||
borderRadius: 6,
|
},
|
||||||
backgroundColor: colors.primary,
|
});
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|||||||
@ -1,18 +1,17 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { View, Text, TouchableOpacity, Image } from 'react-native';
|
import { View, Text, TouchableOpacity } from 'react-native';
|
||||||
import { PaymentOptionProps } from './paymentOption.props';
|
import { PaymentOptionProps } from './paymentOption.props';
|
||||||
import { getStyles } from './paymentOption.styles';
|
import { getStyles } from './paymentOption.styles';
|
||||||
import { useAppTheme } from '@theme';
|
import { useAppTheme } from '@theme';
|
||||||
|
|
||||||
export const PaymentOption: React.FC<PaymentOptionProps> = ({
|
export const PaymentOption: React.FC<PaymentOptionProps> = ({
|
||||||
|
type,
|
||||||
label,
|
label,
|
||||||
isSelected,
|
isSelected,
|
||||||
icon,
|
|
||||||
onSelect,
|
onSelect,
|
||||||
}) => {
|
}) => {
|
||||||
const { colors } = useAppTheme();
|
const { colors } = useAppTheme();
|
||||||
const styles = getStyles(colors);
|
const styles = getStyles(colors);
|
||||||
// console.log(icon);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<TouchableOpacity
|
<TouchableOpacity
|
||||||
@ -20,7 +19,7 @@ export const PaymentOption: React.FC<PaymentOptionProps> = ({
|
|||||||
onPress={onSelect}
|
onPress={onSelect}
|
||||||
activeOpacity={0.7}
|
activeOpacity={0.7}
|
||||||
>
|
>
|
||||||
<Image source={{ uri: icon }} style={styles.icon} />
|
<Text style={styles.icon}>{type === 'UPI' ? '📱' : type === 'Card' ? '💳' : type === 'Wallet' ? '👛' : '💵'}</Text>
|
||||||
<Text style={styles.label}>{label}</Text>
|
<Text style={styles.label}>{label}</Text>
|
||||||
<View style={[styles.radio, isSelected && styles.radioSelected]}>
|
<View style={[styles.radio, isSelected && styles.radioSelected]}>
|
||||||
{isSelected && <View style={styles.radioInner} />}
|
{isSelected && <View style={styles.radioInner} />}
|
||||||
|
|||||||
@ -1 +0,0 @@
|
|||||||
export * from './productCard';
|
|
||||||
@ -1,109 +0,0 @@
|
|||||||
import { StyleSheet, Dimensions, Platform } from 'react-native';
|
|
||||||
import { typography } from '@theme';
|
|
||||||
|
|
||||||
const { width } = Dimensions.get('window');
|
|
||||||
// Screen padding = 16 * 2 = 32
|
|
||||||
// Gap between cards = 16
|
|
||||||
// Total cards width = width - 32 - 16 = width - 48
|
|
||||||
const CARD_WIDTH = (width - 48) / 2;
|
|
||||||
|
|
||||||
export const getStyles = (colors: any) =>
|
|
||||||
StyleSheet.create({
|
|
||||||
cardContainer: {
|
|
||||||
width: CARD_WIDTH,
|
|
||||||
backgroundColor: colors.surface ?? '#FFFFFF',
|
|
||||||
borderRadius: 12,
|
|
||||||
marginBottom: 16,
|
|
||||||
overflow: 'hidden',
|
|
||||||
...Platform.select({
|
|
||||||
ios: {
|
|
||||||
shadowColor: '#000',
|
|
||||||
shadowOffset: { width: 0, height: 2 },
|
|
||||||
shadowOpacity: 0.05,
|
|
||||||
shadowRadius: 8,
|
|
||||||
},
|
|
||||||
android: { elevation: 2 },
|
|
||||||
}),
|
|
||||||
},
|
|
||||||
imageContainer: {
|
|
||||||
width: '100%',
|
|
||||||
height: CARD_WIDTH, // Square image
|
|
||||||
backgroundColor: colors.background ?? '#F8F9FA',
|
|
||||||
position: 'relative',
|
|
||||||
},
|
|
||||||
image: {
|
|
||||||
width: '100%',
|
|
||||||
height: '100%',
|
|
||||||
resizeMode: 'cover',
|
|
||||||
},
|
|
||||||
discountBadge: {
|
|
||||||
position: 'absolute',
|
|
||||||
top: 8,
|
|
||||||
left: 8,
|
|
||||||
backgroundColor: colors.error ?? '#E53935',
|
|
||||||
paddingHorizontal: 6,
|
|
||||||
paddingVertical: 2,
|
|
||||||
borderRadius: 4,
|
|
||||||
},
|
|
||||||
discountText: {
|
|
||||||
color: '#FFFFFF',
|
|
||||||
fontSize: 10,
|
|
||||||
fontWeight: typography.fontWeight.bold,
|
|
||||||
},
|
|
||||||
favoriteButton: {
|
|
||||||
position: 'absolute',
|
|
||||||
top: 8,
|
|
||||||
right: 8,
|
|
||||||
backgroundColor: 'rgba(255,255,255,0.9)',
|
|
||||||
width: 28,
|
|
||||||
height: 28,
|
|
||||||
borderRadius: 14,
|
|
||||||
alignItems: 'center',
|
|
||||||
justifyContent: 'center',
|
|
||||||
},
|
|
||||||
infoContainer: {
|
|
||||||
padding: 10,
|
|
||||||
},
|
|
||||||
brandText: {
|
|
||||||
fontSize: 10,
|
|
||||||
color: colors.textSecondary,
|
|
||||||
textTransform: 'uppercase',
|
|
||||||
marginBottom: 2,
|
|
||||||
fontWeight: typography.fontWeight.semibold,
|
|
||||||
},
|
|
||||||
titleText: {
|
|
||||||
fontSize: 13,
|
|
||||||
color: colors.text,
|
|
||||||
fontWeight: typography.fontWeight.medium,
|
|
||||||
marginBottom: 4,
|
|
||||||
},
|
|
||||||
priceRow: {
|
|
||||||
flexDirection: 'row',
|
|
||||||
alignItems: 'center',
|
|
||||||
marginBottom: 8,
|
|
||||||
},
|
|
||||||
priceText: {
|
|
||||||
fontSize: 14,
|
|
||||||
color: colors.text,
|
|
||||||
fontWeight: typography.fontWeight.bold,
|
|
||||||
marginRight: 6,
|
|
||||||
},
|
|
||||||
comparePriceText: {
|
|
||||||
fontSize: 11,
|
|
||||||
color: colors.textSecondary,
|
|
||||||
textDecorationLine: 'line-through',
|
|
||||||
},
|
|
||||||
addButton: {
|
|
||||||
flexDirection: 'row',
|
|
||||||
alignItems: 'center',
|
|
||||||
justifyContent: 'center',
|
|
||||||
backgroundColor: colors.primaryMuted ?? '#E9F7EF',
|
|
||||||
paddingVertical: 6,
|
|
||||||
borderRadius: 6,
|
|
||||||
},
|
|
||||||
addButtonText: {
|
|
||||||
color: colors.primary ?? '#05824C',
|
|
||||||
fontSize: 12,
|
|
||||||
fontWeight: typography.fontWeight.bold,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
@ -1,111 +0,0 @@
|
|||||||
import React from 'react';
|
|
||||||
import {
|
|
||||||
View,
|
|
||||||
Text,
|
|
||||||
Image,
|
|
||||||
TouchableOpacity,
|
|
||||||
StyleProp,
|
|
||||||
ViewStyle,
|
|
||||||
} from 'react-native';
|
|
||||||
import { getStyles } from './productCard.styles';
|
|
||||||
import { useAppTheme } from '@theme';
|
|
||||||
import { getFullUrl } from '@utils';
|
|
||||||
|
|
||||||
export interface ProductCardProps {
|
|
||||||
id: string;
|
|
||||||
name: string;
|
|
||||||
imageUrl: string;
|
|
||||||
price: string;
|
|
||||||
compareAtPrice?: string;
|
|
||||||
brand?: string | null;
|
|
||||||
currency?: string;
|
|
||||||
onPress?: () => void;
|
|
||||||
onAddPress?: () => void;
|
|
||||||
style?: StyleProp<ViewStyle>;
|
|
||||||
}
|
|
||||||
|
|
||||||
export const getDiscountPercentage = (price: string, comparePrice?: string) => {
|
|
||||||
if (!comparePrice || !price) return null;
|
|
||||||
const p = parseFloat(price);
|
|
||||||
const c = parseFloat(comparePrice);
|
|
||||||
if (c <= p || c <= 0) return null;
|
|
||||||
const discount = Math.round(((c - p) / c) * 100);
|
|
||||||
return discount > 0 ? `${discount}% OFF` : null;
|
|
||||||
};
|
|
||||||
|
|
||||||
export const formatPrice = (price: string, currency: string = '₹') => {
|
|
||||||
const num = parseFloat(price);
|
|
||||||
return isNaN(num) ? price : `${currency}${num.toFixed(0)}`;
|
|
||||||
};
|
|
||||||
|
|
||||||
export const ProductCard: React.FC<ProductCardProps> = ({
|
|
||||||
id,
|
|
||||||
name,
|
|
||||||
imageUrl,
|
|
||||||
price,
|
|
||||||
compareAtPrice,
|
|
||||||
brand,
|
|
||||||
currency = '₹',
|
|
||||||
onPress,
|
|
||||||
onAddPress,
|
|
||||||
style,
|
|
||||||
}) => {
|
|
||||||
const { colors } = useAppTheme();
|
|
||||||
const styles = getStyles(colors);
|
|
||||||
|
|
||||||
const discountText = getDiscountPercentage(price, compareAtPrice);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<TouchableOpacity
|
|
||||||
style={[styles.cardContainer, style]}
|
|
||||||
activeOpacity={0.7}
|
|
||||||
onPress={onPress}
|
|
||||||
>
|
|
||||||
<View style={styles.imageContainer}>
|
|
||||||
<Image
|
|
||||||
source={{
|
|
||||||
uri: getFullUrl(imageUrl),
|
|
||||||
}}
|
|
||||||
style={styles.image}
|
|
||||||
// fallback source can be handled here if needed
|
|
||||||
/>
|
|
||||||
{discountText && (
|
|
||||||
<View style={styles.discountBadge}>
|
|
||||||
<Text style={styles.discountText}>{discountText}</Text>
|
|
||||||
</View>
|
|
||||||
)}
|
|
||||||
<TouchableOpacity style={styles.favoriteButton}>
|
|
||||||
<Text style={{ fontSize: 14 }}>🤍</Text>
|
|
||||||
</TouchableOpacity>
|
|
||||||
</View>
|
|
||||||
|
|
||||||
<View style={styles.infoContainer}>
|
|
||||||
{brand && (
|
|
||||||
<Text style={styles.brandText} numberOfLines={1}>
|
|
||||||
{brand}
|
|
||||||
</Text>
|
|
||||||
)}
|
|
||||||
<Text style={styles.titleText} numberOfLines={2}>
|
|
||||||
{name}
|
|
||||||
</Text>
|
|
||||||
|
|
||||||
<View style={styles.priceRow}>
|
|
||||||
<Text style={styles.priceText}>{formatPrice(price, currency)}</Text>
|
|
||||||
{compareAtPrice && parseFloat(compareAtPrice) > parseFloat(price) && (
|
|
||||||
<Text style={styles.comparePriceText}>
|
|
||||||
{formatPrice(compareAtPrice, currency)}
|
|
||||||
</Text>
|
|
||||||
)}
|
|
||||||
</View>
|
|
||||||
|
|
||||||
{/* <TouchableOpacity
|
|
||||||
style={styles.addButton}
|
|
||||||
onPress={onAddPress || onPress}
|
|
||||||
activeOpacity={0.7}
|
|
||||||
>
|
|
||||||
<Text style={styles.addButtonText}>+ ADD</Text>
|
|
||||||
</TouchableOpacity> */}
|
|
||||||
</View>
|
|
||||||
</TouchableOpacity>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
@ -9,8 +9,7 @@ import {
|
|||||||
import { getStyles } from './loginScreen.styles';
|
import { getStyles } from './loginScreen.styles';
|
||||||
import { CustomInput, PrimaryButton } from '@components';
|
import { CustomInput, PrimaryButton } from '@components';
|
||||||
import { useAppTheme } from '@theme';
|
import { useAppTheme } from '@theme';
|
||||||
import { useLoginScreen } from './hooks/useLoginScreen';
|
import { useLoginScreen } from '../../../hooks';
|
||||||
import { RootState, useAppSelector } from '@store';
|
|
||||||
|
|
||||||
export const LoginScreen: React.FC = () => {
|
export const LoginScreen: React.FC = () => {
|
||||||
const { colors } = useAppTheme();
|
const { colors } = useAppTheme();
|
||||||
@ -19,8 +18,6 @@ export const LoginScreen: React.FC = () => {
|
|||||||
const { mobileNumber, error, onChangeMobileNumber, handleLogin } =
|
const { mobileNumber, error, onChangeMobileNumber, handleLogin } =
|
||||||
useLoginScreen();
|
useLoginScreen();
|
||||||
|
|
||||||
const { isLoading } = useAppSelector((state: RootState) => state.auth);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<KeyboardAvoidingView
|
<KeyboardAvoidingView
|
||||||
style={styles.container}
|
style={styles.container}
|
||||||
@ -72,10 +69,9 @@ export const LoginScreen: React.FC = () => {
|
|||||||
)}
|
)}
|
||||||
|
|
||||||
<PrimaryButton
|
<PrimaryButton
|
||||||
title={isLoading ? 'Loading...' : 'Get OTP'}
|
title="Get OTP"
|
||||||
onPress={handleLogin}
|
onPress={handleLogin}
|
||||||
style={{ marginTop: 12 }}
|
style={{ marginTop: 12 }}
|
||||||
disabled={isLoading}
|
|
||||||
/>
|
/>
|
||||||
</View>
|
</View>
|
||||||
|
|
||||||
|
|||||||
@ -1 +1,2 @@
|
|||||||
export * from './loginScreen';
|
export * from './loginScreen';
|
||||||
|
export * from './loginScreen.styles';
|
||||||
|
|||||||
@ -9,10 +9,10 @@ import { StackNavigationProp } from '@react-navigation/stack';
|
|||||||
import { getStyles } from './accountScreen.styles';
|
import { getStyles } from './accountScreen.styles';
|
||||||
import { Header, PrimaryButton } from '@components';
|
import { Header, PrimaryButton } from '@components';
|
||||||
import { useAppTheme } from '@theme';
|
import { useAppTheme } from '@theme';
|
||||||
|
import { useAppDispatch, useAppSelector } from '../../../store';
|
||||||
import { logout } from '../../../store/commonreducers/auth';
|
import { logout } from '../../../store/commonreducers/auth';
|
||||||
import { AppStackParamList } from '../../../navigation/appStack';
|
import { AppStackParamList } from '../../../navigation/appStack';
|
||||||
import { MainTabParamList } from '../../../navigation/mainTabNavigator';
|
import { MainTabParamList } from '../../../navigation/mainTabNavigator';
|
||||||
import { useAppDispatch, useAppSelector } from '@store';
|
|
||||||
|
|
||||||
type AccountNavProp = CompositeNavigationProp<
|
type AccountNavProp = CompositeNavigationProp<
|
||||||
BottomTabNavigationProp<MainTabParamList, 'AccountScreen'>,
|
BottomTabNavigationProp<MainTabParamList, 'AccountScreen'>,
|
||||||
@ -121,7 +121,7 @@ export const AccountScreen: React.FC = () => {
|
|||||||
<View style={styles.profilePhoneRow}>
|
<View style={styles.profilePhoneRow}>
|
||||||
<Text style={styles.profilePhoneIcon}>📞</Text>
|
<Text style={styles.profilePhoneIcon}>📞</Text>
|
||||||
<Text style={styles.profilePhone}>
|
<Text style={styles.profilePhone}>
|
||||||
{user?.phone || '+91 98765 43210'}
|
{user?.mobileNumber || '+91 98765 43210'}
|
||||||
</Text>
|
</Text>
|
||||||
</View>
|
</View>
|
||||||
|
|
||||||
|
|||||||
@ -1,19 +1,24 @@
|
|||||||
import React, { useEffect } from 'react';
|
import React, { useState } from 'react';
|
||||||
import { View, Text, ScrollView, TouchableOpacity, Image } from 'react-native';
|
import {
|
||||||
|
View,
|
||||||
|
Text,
|
||||||
|
ScrollView,
|
||||||
|
TextInput,
|
||||||
|
TouchableOpacity,
|
||||||
|
} from 'react-native';
|
||||||
import { useNavigation } from '@react-navigation/native';
|
import { useNavigation } from '@react-navigation/native';
|
||||||
import { StackNavigationProp } from '@react-navigation/stack';
|
import { StackNavigationProp } from '@react-navigation/stack';
|
||||||
import { getStyles } from './cartScreen.styles';
|
import { getStyles } from './cartScreen.styles';
|
||||||
import { Header, QuantitySelector, PrimaryButton } from '@components';
|
import { Header, QuantitySelector, PrimaryButton } from '@components';
|
||||||
import { useAppTheme } from '@theme';
|
import { useAppTheme } from '@theme';
|
||||||
|
import { useAppDispatch, useAppSelector } from '../../../store';
|
||||||
import {
|
import {
|
||||||
|
removeItem,
|
||||||
|
applyCoupon,
|
||||||
|
removeCoupon,
|
||||||
selectCartTotal,
|
selectCartTotal,
|
||||||
getCartThunk,
|
|
||||||
updateCartItemThunk,
|
|
||||||
} from '../../../store/commonreducers/cart';
|
} from '../../../store/commonreducers/cart';
|
||||||
import { AppStackParamList } from '../../../navigation/appStack';
|
import { AppStackParamList } from '../../../navigation/appStack';
|
||||||
import { useAppDispatch, useAppSelector } from '@store';
|
|
||||||
import { getFullUrl } from '@utils';
|
|
||||||
// import { getFullUrl } from '@utils/helperr';
|
|
||||||
|
|
||||||
type CartScreenNavProp = StackNavigationProp<AppStackParamList, 'CartScreen'>;
|
type CartScreenNavProp = StackNavigationProp<AppStackParamList, 'CartScreen'>;
|
||||||
|
|
||||||
@ -22,14 +27,20 @@ export const CartScreen: React.FC = () => {
|
|||||||
const styles = getStyles(colors);
|
const styles = getStyles(colors);
|
||||||
const dispatch = useAppDispatch();
|
const dispatch = useAppDispatch();
|
||||||
const navigation = useNavigation<CartScreenNavProp>();
|
const navigation = useNavigation<CartScreenNavProp>();
|
||||||
const { items, isLoading } = useAppSelector(state => state.cart);
|
const { items, couponCode } = useAppSelector(state => state.cart);
|
||||||
const totals = useAppSelector(selectCartTotal);
|
const totals = useAppSelector(selectCartTotal);
|
||||||
|
const [couponInput, setCouponInput] = useState('');
|
||||||
|
|
||||||
useEffect(() => {
|
const handleCouponPress = () => {
|
||||||
dispatch(getCartThunk());
|
if (couponCode) {
|
||||||
}, [dispatch]);
|
dispatch(removeCoupon());
|
||||||
|
setCouponInput('');
|
||||||
|
} else if (couponInput) {
|
||||||
|
dispatch(applyCoupon(couponInput));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
if (!isLoading && items.length === 0) {
|
if (items.length === 0) {
|
||||||
return (
|
return (
|
||||||
<View style={styles.container}>
|
<View style={styles.container}>
|
||||||
<Header title="Cart" onBack={() => navigation.goBack()} />
|
<Header title="Cart" onBack={() => navigation.goBack()} />
|
||||||
@ -74,36 +85,19 @@ export const CartScreen: React.FC = () => {
|
|||||||
index < items.length - 1 && styles.cartItemDivider,
|
index < items.length - 1 && styles.cartItemDivider,
|
||||||
]}
|
]}
|
||||||
>
|
>
|
||||||
<Image
|
<View style={styles.itemThumb}>
|
||||||
style={styles.itemThumb}
|
<Text style={styles.itemThumbEmoji}>🍕</Text>
|
||||||
source={{ uri: getFullUrl(item.product.imageUrl) }}
|
</View>
|
||||||
/>
|
|
||||||
<View style={styles.itemInfo}>
|
<View style={styles.itemInfo}>
|
||||||
<Text style={styles.itemTitle} numberOfLines={2}>
|
<Text style={styles.itemTitle} numberOfLines={2}>
|
||||||
{item.product.name}
|
{item.item.title}
|
||||||
</Text>
|
</Text>
|
||||||
<Text style={styles.itemPrice}>₹{item.product.price}</Text>
|
<Text style={styles.itemPrice}>₹{item.item.price}</Text>
|
||||||
</View>
|
</View>
|
||||||
<QuantitySelector
|
<QuantitySelector
|
||||||
value={item.quantity}
|
value={item.quantity}
|
||||||
onIncrement={() =>
|
onIncrement={() => {}}
|
||||||
dispatch(
|
onDecrement={() => dispatch(removeItem(item.item.id))}
|
||||||
updateCartItemThunk({
|
|
||||||
productId: item.productId,
|
|
||||||
quantity: item.quantity + 1,
|
|
||||||
}),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
onDecrement={() => {
|
|
||||||
if (item.quantity >= 1) {
|
|
||||||
dispatch(
|
|
||||||
updateCartItemThunk({
|
|
||||||
productId: item.productId,
|
|
||||||
quantity: item.quantity - 1,
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
/>
|
/>
|
||||||
</View>
|
</View>
|
||||||
))}
|
))}
|
||||||
@ -119,12 +113,48 @@ export const CartScreen: React.FC = () => {
|
|||||||
</TouchableOpacity>
|
</TouchableOpacity>
|
||||||
|
|
||||||
<View style={styles.footer}>
|
<View style={styles.footer}>
|
||||||
{/* Coupon logic removed/commented out for now as requested
|
|
||||||
<Text style={styles.sectionTitle}>Apply Coupon</Text>
|
<Text style={styles.sectionTitle}>Apply Coupon</Text>
|
||||||
<View style={styles.couponCard}>
|
<View style={styles.couponCard}>
|
||||||
...
|
{couponCode ? (
|
||||||
|
<View style={styles.couponAppliedRow}>
|
||||||
|
<View style={styles.couponAppliedLeft}>
|
||||||
|
<View style={styles.couponCheckBadge}>
|
||||||
|
<Text style={styles.couponCheckIcon}>✓</Text>
|
||||||
|
</View>
|
||||||
|
<View>
|
||||||
|
<Text style={styles.couponAppliedCode}>{couponCode}</Text>
|
||||||
|
<Text style={styles.couponAppliedSub}>Coupon applied</Text>
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
<TouchableOpacity
|
||||||
|
onPress={handleCouponPress}
|
||||||
|
activeOpacity={0.7}
|
||||||
|
>
|
||||||
|
<Text style={styles.couponRemoveText}>Remove</Text>
|
||||||
|
</TouchableOpacity>
|
||||||
|
</View>
|
||||||
|
) : (
|
||||||
|
<View style={styles.couponRow}>
|
||||||
|
<Text style={styles.couponIcon}>🏷️</Text>
|
||||||
|
<TextInput
|
||||||
|
style={styles.couponInput}
|
||||||
|
placeholder="Enter coupon code"
|
||||||
|
placeholderTextColor={colors.placeholder}
|
||||||
|
value={couponInput}
|
||||||
|
onChangeText={setCouponInput}
|
||||||
|
autoCapitalize="characters"
|
||||||
|
/>
|
||||||
|
<TouchableOpacity
|
||||||
|
style={styles.applyButton}
|
||||||
|
onPress={handleCouponPress}
|
||||||
|
activeOpacity={0.7}
|
||||||
|
disabled={!couponInput}
|
||||||
|
>
|
||||||
|
<Text style={styles.applyButtonText}>Apply</Text>
|
||||||
|
</TouchableOpacity>
|
||||||
|
</View>
|
||||||
|
)}
|
||||||
</View>
|
</View>
|
||||||
*/}
|
|
||||||
|
|
||||||
<Text style={styles.sectionTitle}>Bill Details</Text>
|
<Text style={styles.sectionTitle}>Bill Details</Text>
|
||||||
<View style={styles.billCard}>
|
<View style={styles.billCard}>
|
||||||
|
|||||||
@ -1,130 +1,92 @@
|
|||||||
import { StyleSheet } from 'react-native';
|
import { StyleSheet } from 'react-native';
|
||||||
import { typography } from '@theme';
|
import { typography } from '@theme';
|
||||||
|
|
||||||
export const getStyles = (colors: any) =>
|
export const getStyles = (colors: any) => StyleSheet.create({
|
||||||
StyleSheet.create({
|
container: {
|
||||||
container: {
|
flex: 1,
|
||||||
flex: 1,
|
backgroundColor: colors.background,
|
||||||
backgroundColor: colors.background,
|
},
|
||||||
},
|
content: {
|
||||||
content: {
|
padding: 16,
|
||||||
padding: 16,
|
},
|
||||||
paddingBottom: 24,
|
addressCard: {
|
||||||
},
|
backgroundColor: colors.cardBg,
|
||||||
sectionTitle: {
|
borderRadius: 12,
|
||||||
fontSize: typography.fontSize.md,
|
padding: 16,
|
||||||
fontWeight: typography.fontWeight.semibold,
|
borderWidth: 1,
|
||||||
color: colors.text,
|
borderColor: colors.border,
|
||||||
marginBottom: 12,
|
marginBottom: 24,
|
||||||
},
|
},
|
||||||
emptyState: {
|
addressLabel: {
|
||||||
padding: 24,
|
fontSize: typography.fontSize.xs,
|
||||||
alignItems: 'center',
|
color: colors.textSecondary,
|
||||||
justifyContent: 'center',
|
marginBottom: 4,
|
||||||
},
|
},
|
||||||
emptyStateText: {
|
addressText: {
|
||||||
fontSize: typography.fontSize.sm,
|
fontSize: typography.fontSize.md,
|
||||||
color: colors.textSecondary,
|
fontWeight: typography.fontWeight.semibold,
|
||||||
},
|
color: colors.text,
|
||||||
addressCard: {
|
marginBottom: 4,
|
||||||
backgroundColor: colors.cardBg,
|
},
|
||||||
borderRadius: 12,
|
addressSubtext: {
|
||||||
padding: 16,
|
fontSize: typography.fontSize.sm,
|
||||||
borderWidth: 1.5,
|
color: colors.primary,
|
||||||
borderColor: colors.border,
|
fontWeight: typography.fontWeight.medium,
|
||||||
marginBottom: 12,
|
},
|
||||||
},
|
sectionTitle: {
|
||||||
addressCardSelected: {
|
fontSize: typography.fontSize.md,
|
||||||
borderColor: colors.primary,
|
fontWeight: typography.fontWeight.semibold,
|
||||||
backgroundColor: '#E8F5E9',
|
color: colors.text,
|
||||||
},
|
marginBottom: 12,
|
||||||
addressCardHeader: {
|
},
|
||||||
flexDirection: 'row',
|
optionCard: {
|
||||||
alignItems: 'center',
|
flexDirection: 'row',
|
||||||
marginBottom: 8,
|
alignItems: 'center',
|
||||||
},
|
padding: 16,
|
||||||
labelBadge: {
|
borderRadius: 12,
|
||||||
flexDirection: 'row',
|
borderWidth: 1.5,
|
||||||
alignItems: 'center',
|
borderColor: colors.border,
|
||||||
backgroundColor: colors.background,
|
marginBottom: 10,
|
||||||
borderRadius: 20,
|
backgroundColor: colors.background,
|
||||||
paddingVertical: 4,
|
},
|
||||||
paddingHorizontal: 10,
|
optionCardSelected: {
|
||||||
marginRight: 8,
|
borderColor: colors.primary,
|
||||||
},
|
backgroundColor: '#E8F5E9',
|
||||||
labelBadgeIcon: {
|
},
|
||||||
fontSize: 12,
|
optionInfo: {
|
||||||
marginRight: 4,
|
flex: 1,
|
||||||
},
|
},
|
||||||
labelBadgeText: {
|
optionTitle: {
|
||||||
fontSize: typography.fontSize.xs,
|
fontSize: typography.fontSize.md,
|
||||||
fontWeight: typography.fontWeight.semibold,
|
fontWeight: typography.fontWeight.medium,
|
||||||
color: colors.text,
|
color: colors.text,
|
||||||
},
|
marginBottom: 2,
|
||||||
defaultBadge: {
|
},
|
||||||
backgroundColor: colors.primary,
|
optionSubtext: {
|
||||||
borderRadius: 20,
|
fontSize: typography.fontSize.sm,
|
||||||
paddingVertical: 3,
|
color: colors.textSecondary,
|
||||||
paddingHorizontal: 8,
|
},
|
||||||
},
|
radio: {
|
||||||
defaultBadgeText: {
|
width: 22,
|
||||||
fontSize: 10,
|
height: 22,
|
||||||
fontWeight: typography.fontWeight.bold,
|
borderRadius: 11,
|
||||||
color: '#fff',
|
borderWidth: 2,
|
||||||
letterSpacing: 0.5,
|
borderColor: colors.border,
|
||||||
},
|
justifyContent: 'center',
|
||||||
radio: {
|
alignItems: 'center',
|
||||||
width: 22,
|
},
|
||||||
height: 22,
|
radioSelected: {
|
||||||
borderRadius: 11,
|
borderColor: colors.primary,
|
||||||
borderWidth: 2,
|
},
|
||||||
borderColor: colors.border,
|
radioInner: {
|
||||||
justifyContent: 'center',
|
width: 12,
|
||||||
alignItems: 'center',
|
height: 12,
|
||||||
marginLeft: 'auto',
|
borderRadius: 6,
|
||||||
},
|
backgroundColor: colors.primary,
|
||||||
radioSelected: {
|
},
|
||||||
borderColor: colors.primary,
|
footer: {
|
||||||
},
|
padding: 16,
|
||||||
radioInner: {
|
borderTopWidth: 1,
|
||||||
width: 12,
|
borderTopColor: colors.border,
|
||||||
height: 12,
|
},
|
||||||
borderRadius: 6,
|
});
|
||||||
backgroundColor: colors.primary,
|
|
||||||
},
|
|
||||||
addressText: {
|
|
||||||
fontSize: typography.fontSize.md,
|
|
||||||
fontWeight: typography.fontWeight.medium,
|
|
||||||
color: colors.text,
|
|
||||||
marginBottom: 4,
|
|
||||||
},
|
|
||||||
addressSubtext: {
|
|
||||||
fontSize: typography.fontSize.sm,
|
|
||||||
color: colors.textSecondary,
|
|
||||||
marginBottom: 2,
|
|
||||||
},
|
|
||||||
addressPhone: {
|
|
||||||
fontSize: typography.fontSize.sm,
|
|
||||||
color: colors.textSecondary,
|
|
||||||
marginTop: 4,
|
|
||||||
},
|
|
||||||
addAddressButton: {
|
|
||||||
borderWidth: 1.5,
|
|
||||||
borderColor: colors.primary,
|
|
||||||
borderStyle: 'dashed',
|
|
||||||
borderRadius: 12,
|
|
||||||
paddingVertical: 14,
|
|
||||||
alignItems: 'center',
|
|
||||||
marginTop: 4,
|
|
||||||
},
|
|
||||||
addAddressButtonText: {
|
|
||||||
fontSize: typography.fontSize.md,
|
|
||||||
fontWeight: typography.fontWeight.semibold,
|
|
||||||
color: colors.primary,
|
|
||||||
},
|
|
||||||
footer: {
|
|
||||||
padding: 16,
|
|
||||||
borderTopWidth: 1,
|
|
||||||
borderTopColor: colors.border,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|||||||
@ -1,18 +1,18 @@
|
|||||||
import React, { useState, useMemo } from 'react';
|
import React, { useState } from 'react';
|
||||||
import { View, Text, TouchableOpacity, ScrollView } from 'react-native';
|
import {
|
||||||
|
View,
|
||||||
|
Text,
|
||||||
|
TouchableOpacity,
|
||||||
|
ScrollView,
|
||||||
|
} from 'react-native';
|
||||||
import { useNavigation } from '@react-navigation/native';
|
import { useNavigation } from '@react-navigation/native';
|
||||||
import { StackNavigationProp } from '@react-navigation/stack';
|
import { StackNavigationProp } from '@react-navigation/stack';
|
||||||
import { getStyles } from './checkoutAddressScreen.styles';
|
import { getStyles } from './checkoutAddressScreen.styles';
|
||||||
import { Header, StepProgress, PrimaryButton } from '@components';
|
import { Header, StepProgress, PrimaryButton } from '@components';
|
||||||
import { useAppTheme } from '@theme';
|
import { useAppTheme } from '@theme';
|
||||||
import { useAppSelector } from '@store';
|
import { AppStackParamList } from '../../../navigation/appStack';
|
||||||
import { AddressLabel, CustomerAddress } from '@interfaces';
|
|
||||||
import { AppStackParamList } from '@navigation/appStack';
|
|
||||||
|
|
||||||
type CheckoutAddressNavProp = StackNavigationProp<
|
type CheckoutAddressNavProp = StackNavigationProp<AppStackParamList, 'CheckoutAddressScreen'>;
|
||||||
AppStackParamList,
|
|
||||||
'CheckoutAddressScreen'
|
|
||||||
>;
|
|
||||||
|
|
||||||
const CHECKOUT_STEPS = [
|
const CHECKOUT_STEPS = [
|
||||||
{ key: 'address', label: 'Address' },
|
{ key: 'address', label: 'Address' },
|
||||||
@ -20,118 +20,62 @@ const CHECKOUT_STEPS = [
|
|||||||
{ key: 'confirm', label: 'Confirm' },
|
{ key: 'confirm', label: 'Confirm' },
|
||||||
];
|
];
|
||||||
|
|
||||||
const LABEL_ICON: Record<AddressLabel, string> = {
|
|
||||||
Home: '🏠',
|
|
||||||
Work: '💼',
|
|
||||||
Other: '📍',
|
|
||||||
};
|
|
||||||
|
|
||||||
export const CheckoutAddressScreen: React.FC = () => {
|
export const CheckoutAddressScreen: React.FC = () => {
|
||||||
const { colors } = useAppTheme();
|
const { colors } = useAppTheme();
|
||||||
const styles = getStyles(colors);
|
const styles = getStyles(colors);
|
||||||
const navigation = useNavigation<CheckoutAddressNavProp>();
|
const navigation = useNavigation<CheckoutAddressNavProp>();
|
||||||
const { customerDetails } = useAppSelector(state => state.customerProfile);
|
const [deliveryOption, setDeliveryOption] = useState<'standard' | 'express'>('standard');
|
||||||
|
|
||||||
const addresses: CustomerAddress[] = customerDetails?.addresses ?? [];
|
|
||||||
|
|
||||||
const defaultAddressId = useMemo(() => {
|
|
||||||
const def = addresses.find(a => a.isDefault);
|
|
||||||
return def?.id ?? addresses[0]?.id ?? null;
|
|
||||||
}, [addresses]);
|
|
||||||
|
|
||||||
const [selectedAddressId, setSelectedAddressId] = useState<string | null>(
|
|
||||||
defaultAddressId,
|
|
||||||
);
|
|
||||||
|
|
||||||
const handleContinue = () => {
|
|
||||||
if (!selectedAddressId) return;
|
|
||||||
navigation.navigate('CheckoutPaymentScreen', {
|
|
||||||
selectedAddressId,
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<View style={styles.container}>
|
<View style={styles.container}>
|
||||||
<Header title="Checkout" onBack={() => navigation.goBack()} />
|
<Header title="Checkout" onBack={() => navigation.goBack()} />
|
||||||
<StepProgress steps={CHECKOUT_STEPS} activeStep={0} />
|
<StepProgress steps={CHECKOUT_STEPS} activeStep={0} />
|
||||||
<ScrollView contentContainerStyle={styles.content}>
|
<ScrollView contentContainerStyle={styles.content}>
|
||||||
<Text style={styles.sectionTitle}>Deliver to</Text>
|
<View style={styles.addressCard}>
|
||||||
|
<Text style={styles.addressLabel}>Deliver to</Text>
|
||||||
|
<Text style={styles.addressText}>
|
||||||
|
Koramangala 4th Block, Bengaluru, 560034
|
||||||
|
</Text>
|
||||||
|
<Text style={styles.addressSubtext}>Home</Text>
|
||||||
|
</View>
|
||||||
|
|
||||||
{addresses.length === 0 && (
|
<Text style={styles.sectionTitle}>Delivery Option</Text>
|
||||||
<View style={styles.emptyState}>
|
<TouchableOpacity
|
||||||
<Text style={styles.emptyStateText}>No saved addresses yet.</Text>
|
style={[
|
||||||
|
styles.optionCard,
|
||||||
|
deliveryOption === 'standard' && styles.optionCardSelected,
|
||||||
|
]}
|
||||||
|
onPress={() => setDeliveryOption('standard')}
|
||||||
|
activeOpacity={0.7}
|
||||||
|
>
|
||||||
|
<View style={styles.optionInfo}>
|
||||||
|
<Text style={styles.optionTitle}>Standard Delivery</Text>
|
||||||
|
<Text style={styles.optionSubtext}>Free • 25-30 min</Text>
|
||||||
</View>
|
</View>
|
||||||
)}
|
<View style={[styles.radio, deliveryOption === 'standard' && styles.radioSelected]}>
|
||||||
|
{deliveryOption === 'standard' && <View style={styles.radioInner} />}
|
||||||
{addresses.map(address => {
|
</View>
|
||||||
const isSelected = selectedAddressId === address.id;
|
</TouchableOpacity>
|
||||||
return (
|
|
||||||
<TouchableOpacity
|
|
||||||
key={address.id}
|
|
||||||
style={[
|
|
||||||
styles.addressCard,
|
|
||||||
isSelected && styles.addressCardSelected,
|
|
||||||
]}
|
|
||||||
activeOpacity={0.7}
|
|
||||||
onPress={() => setSelectedAddressId(address.id)}
|
|
||||||
>
|
|
||||||
<View style={styles.addressCardHeader}>
|
|
||||||
<View style={styles.labelBadge}>
|
|
||||||
<Text style={styles.labelBadgeIcon}>
|
|
||||||
{LABEL_ICON[address.label] ?? '📍'}
|
|
||||||
</Text>
|
|
||||||
<Text style={styles.labelBadgeText}>{address.label}</Text>
|
|
||||||
</View>
|
|
||||||
|
|
||||||
{address.isDefault && (
|
|
||||||
<View style={styles.defaultBadge}>
|
|
||||||
<Text style={styles.defaultBadgeText}>DEFAULT</Text>
|
|
||||||
</View>
|
|
||||||
)}
|
|
||||||
|
|
||||||
<View
|
|
||||||
style={[styles.radio, isSelected && styles.radioSelected]}
|
|
||||||
>
|
|
||||||
{isSelected && <View style={styles.radioInner} />}
|
|
||||||
</View>
|
|
||||||
</View>
|
|
||||||
|
|
||||||
<Text style={styles.addressText} numberOfLines={2}>
|
|
||||||
{address.houseNumber ? `${address.houseNumber}, ` : ''}
|
|
||||||
{address.addressLine1}
|
|
||||||
</Text>
|
|
||||||
|
|
||||||
{!!address.landmark && (
|
|
||||||
<Text style={styles.addressSubtext} numberOfLines={1}>
|
|
||||||
Landmark: {address.landmark}
|
|
||||||
</Text>
|
|
||||||
)}
|
|
||||||
|
|
||||||
<Text style={styles.addressSubtext} numberOfLines={1}>
|
|
||||||
{address.city}, {address.state} - {address.postalCode}
|
|
||||||
</Text>
|
|
||||||
|
|
||||||
<Text style={styles.addressPhone}>📞 {address.phone}</Text>
|
|
||||||
</TouchableOpacity>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
|
|
||||||
<TouchableOpacity
|
<TouchableOpacity
|
||||||
style={styles.addAddressButton}
|
style={[
|
||||||
|
styles.optionCard,
|
||||||
|
deliveryOption === 'express' && styles.optionCardSelected,
|
||||||
|
]}
|
||||||
|
onPress={() => setDeliveryOption('express')}
|
||||||
activeOpacity={0.7}
|
activeOpacity={0.7}
|
||||||
onPress={() => {
|
|
||||||
// navigation.navigate('AddAddressScreen');
|
|
||||||
}}
|
|
||||||
>
|
>
|
||||||
<Text style={styles.addAddressButtonText}>+ Add New Address</Text>
|
<View style={styles.optionInfo}>
|
||||||
|
<Text style={styles.optionTitle}>Express Delivery</Text>
|
||||||
|
<Text style={styles.optionSubtext}>₹40 • 10-15 min</Text>
|
||||||
|
</View>
|
||||||
|
<View style={[styles.radio, deliveryOption === 'express' && styles.radioSelected]}>
|
||||||
|
{deliveryOption === 'express' && <View style={styles.radioInner} />}
|
||||||
|
</View>
|
||||||
</TouchableOpacity>
|
</TouchableOpacity>
|
||||||
</ScrollView>
|
</ScrollView>
|
||||||
<View style={styles.footer}>
|
<View style={styles.footer}>
|
||||||
<PrimaryButton
|
<PrimaryButton title="Continue to Payment" onPress={() => navigation.navigate('CheckoutPaymentScreen')} />
|
||||||
title="Continue to Payment"
|
|
||||||
onPress={handleContinue}
|
|
||||||
disabled={!selectedAddressId}
|
|
||||||
/>
|
|
||||||
</View>
|
</View>
|
||||||
</View>
|
</View>
|
||||||
);
|
);
|
||||||
|
|||||||
@ -1,30 +1,13 @@
|
|||||||
import React, { useEffect, useState } from 'react';
|
import React, { useState } from 'react';
|
||||||
import { View, Text, ScrollView, Alert } from 'react-native';
|
import { View, Text, ScrollView } from 'react-native';
|
||||||
import { RouteProp, useNavigation, useRoute } from '@react-navigation/native';
|
import { useNavigation } from '@react-navigation/native';
|
||||||
import { StackNavigationProp } from '@react-navigation/stack';
|
import { StackNavigationProp } from '@react-navigation/stack';
|
||||||
import { getStyles } from './checkoutPaymentScreen.styles';
|
import { getStyles } from './checkoutPaymentScreen.styles';
|
||||||
import {
|
import { Header, StepProgress, PaymentOption, PrimaryButton } from '@components';
|
||||||
Header,
|
|
||||||
StepProgress,
|
|
||||||
PaymentOption,
|
|
||||||
PrimaryButton,
|
|
||||||
} from '@components';
|
|
||||||
import { useAppTheme } from '@theme';
|
import { useAppTheme } from '@theme';
|
||||||
import { AppStackParamList } from '../../../navigation/appStack';
|
import { AppStackParamList } from '../../../navigation/appStack';
|
||||||
import { RootState, useAppDispatch, useAppSelector } from '@store';
|
|
||||||
import { getAllPaymentMethodsThunk, placeOrderThunk } from './thunk';
|
|
||||||
import { useSelector } from 'react-redux';
|
|
||||||
import uuid from 'react-native-uuid';
|
|
||||||
import { v4 } from 'react-native-uuid/dist/v4';
|
|
||||||
|
|
||||||
type CheckoutPaymentNavProp = StackNavigationProp<
|
type CheckoutPaymentNavProp = StackNavigationProp<AppStackParamList, 'CheckoutPaymentScreen'>;
|
||||||
AppStackParamList,
|
|
||||||
'CheckoutPaymentScreen'
|
|
||||||
>;
|
|
||||||
type CheckoutPaymentRouteProp = RouteProp<
|
|
||||||
AppStackParamList,
|
|
||||||
'CheckoutPaymentScreen'
|
|
||||||
>;
|
|
||||||
|
|
||||||
const CHECKOUT_STEPS = [
|
const CHECKOUT_STEPS = [
|
||||||
{ key: 'address', label: 'Address' },
|
{ key: 'address', label: 'Address' },
|
||||||
@ -42,59 +25,8 @@ const PAYMENT_METHODS = [
|
|||||||
export const CheckoutPaymentScreen: React.FC = () => {
|
export const CheckoutPaymentScreen: React.FC = () => {
|
||||||
const { colors } = useAppTheme();
|
const { colors } = useAppTheme();
|
||||||
const styles = getStyles(colors);
|
const styles = getStyles(colors);
|
||||||
const route = useRoute<CheckoutPaymentRouteProp>();
|
|
||||||
const navigation = useNavigation<CheckoutPaymentNavProp>();
|
const navigation = useNavigation<CheckoutPaymentNavProp>();
|
||||||
const dispatch = useAppDispatch();
|
|
||||||
const [selectedMethod, setSelectedMethod] = useState('upi');
|
const [selectedMethod, setSelectedMethod] = useState('upi');
|
||||||
const selectedAddressId = route.params?.selectedAddressId;
|
|
||||||
// console.log(selectedAddressId);
|
|
||||||
const uuid = v4();
|
|
||||||
// console.log(uuid);
|
|
||||||
const {
|
|
||||||
paymentMethods,
|
|
||||||
placeOrderSuccess,
|
|
||||||
placeOrderLoading,
|
|
||||||
placeOrderError,
|
|
||||||
} = useAppSelector((state: RootState) => state.paymentMethods);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
dispatch(getAllPaymentMethodsThunk());
|
|
||||||
}, [dispatch]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (placeOrderSuccess) {
|
|
||||||
navigation.navigate('OrderConfirmedScreen', {
|
|
||||||
orderId: 'ORD-' + Math.floor(Math.random() * 900000 + 100000),
|
|
||||||
});
|
|
||||||
} else if (placeOrderError) {
|
|
||||||
Alert.alert('Error', placeOrderError);
|
|
||||||
}
|
|
||||||
}, [placeOrderSuccess, placeOrderError]);
|
|
||||||
|
|
||||||
// console.log('paymentMethods', paymentMethods);
|
|
||||||
|
|
||||||
const selectedPaymentMethod = paymentMethods.find(
|
|
||||||
item => item.id === selectedMethod,
|
|
||||||
);
|
|
||||||
|
|
||||||
const methodName = selectedPaymentMethod?.code;
|
|
||||||
// console.log('methodName', methodName);
|
|
||||||
|
|
||||||
// useEffect(() => {
|
|
||||||
|
|
||||||
// },);
|
|
||||||
|
|
||||||
const handlePlaceOrder = () => {
|
|
||||||
dispatch(
|
|
||||||
placeOrderThunk({
|
|
||||||
addressId: selectedAddressId || '',
|
|
||||||
paymentMethodId: selectedMethod,
|
|
||||||
paymentMethod: methodName || '',
|
|
||||||
orderType: 'DELIVERY',
|
|
||||||
idempotencyKey: `idemp-key-${uuid}`,
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<View style={styles.container}>
|
<View style={styles.container}>
|
||||||
@ -102,22 +34,18 @@ export const CheckoutPaymentScreen: React.FC = () => {
|
|||||||
<StepProgress steps={CHECKOUT_STEPS} activeStep={1} />
|
<StepProgress steps={CHECKOUT_STEPS} activeStep={1} />
|
||||||
<ScrollView contentContainerStyle={styles.content}>
|
<ScrollView contentContainerStyle={styles.content}>
|
||||||
<Text style={styles.sectionTitle}>Select Payment Method</Text>
|
<Text style={styles.sectionTitle}>Select Payment Method</Text>
|
||||||
{paymentMethods?.map(method => (
|
{PAYMENT_METHODS.map((method) => (
|
||||||
<PaymentOption
|
<PaymentOption
|
||||||
key={method.id}
|
key={method.id}
|
||||||
label={method.code}
|
type={method.type}
|
||||||
icon={method.iconUrl}
|
label={method.label}
|
||||||
isSelected={selectedMethod === method.id}
|
isSelected={selectedMethod === method.id}
|
||||||
onSelect={() => setSelectedMethod(method.id)}
|
onSelect={() => setSelectedMethod(method.id)}
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
</ScrollView>
|
</ScrollView>
|
||||||
<View style={styles.footer}>
|
<View style={styles.footer}>
|
||||||
<PrimaryButton
|
<PrimaryButton title="Place Order" onPress={() => navigation.navigate('OrderConfirmedScreen', { orderId: 'ORD-' + Math.floor(Math.random() * 900000 + 100000) })} />
|
||||||
title={placeOrderLoading ? 'Placing...' : 'Place Order'}
|
|
||||||
onPress={handlePlaceOrder}
|
|
||||||
disabled={placeOrderLoading}
|
|
||||||
/>
|
|
||||||
</View>
|
</View>
|
||||||
</View>
|
</View>
|
||||||
);
|
);
|
||||||
|
|||||||
@ -1,3 +1 @@
|
|||||||
export * from './checkoutPaymentScreen';
|
export * from './checkoutPaymentScreen';
|
||||||
export * from './thunk';
|
|
||||||
export * from './reducer';
|
|
||||||
|
|||||||
@ -1,94 +0,0 @@
|
|||||||
import { PaymentMethodsResponse, PlaceOrderResponse } from '@interfaces';
|
|
||||||
import { createReducer } from '@reduxjs/toolkit';
|
|
||||||
import {
|
|
||||||
getAllPaymentMethodsThunk,
|
|
||||||
getOrderByIdThunk,
|
|
||||||
getOrderHistoryThunk,
|
|
||||||
placeOrderThunk,
|
|
||||||
} from './thunk';
|
|
||||||
import { Order } from '@interfaces/order';
|
|
||||||
|
|
||||||
export interface PaymentMethodsState {
|
|
||||||
paymentMethods: PaymentMethodsResponse;
|
|
||||||
isLoading: boolean;
|
|
||||||
error: string | null;
|
|
||||||
placeOrderLoading: boolean;
|
|
||||||
placeOrderSuccess: boolean;
|
|
||||||
placeOrderError: string | null;
|
|
||||||
orderHistory: Order[] | null;
|
|
||||||
placeOrderResponse: PlaceOrderResponse | null;
|
|
||||||
orderDetails: Order | null;
|
|
||||||
orderDetailsLoading: boolean;
|
|
||||||
orderDetailsError: string | null;
|
|
||||||
}
|
|
||||||
|
|
||||||
const initialState: PaymentMethodsState = {
|
|
||||||
paymentMethods: [],
|
|
||||||
isLoading: false,
|
|
||||||
error: null,
|
|
||||||
placeOrderLoading: false,
|
|
||||||
placeOrderSuccess: false,
|
|
||||||
placeOrderError: null,
|
|
||||||
orderHistory: null,
|
|
||||||
placeOrderResponse: null,
|
|
||||||
orderDetails: null,
|
|
||||||
orderDetailsLoading: false,
|
|
||||||
orderDetailsError: null,
|
|
||||||
};
|
|
||||||
|
|
||||||
const paymentMethodsReducer = createReducer(initialState, builder => {
|
|
||||||
builder
|
|
||||||
.addCase(getAllPaymentMethodsThunk.pending, state => {
|
|
||||||
state.isLoading = true;
|
|
||||||
state.error = null;
|
|
||||||
})
|
|
||||||
.addCase(getAllPaymentMethodsThunk.fulfilled, (state, action) => {
|
|
||||||
state.isLoading = false;
|
|
||||||
state.paymentMethods = action.payload;
|
|
||||||
})
|
|
||||||
.addCase(getAllPaymentMethodsThunk.rejected, (state, action) => {
|
|
||||||
state.isLoading = false;
|
|
||||||
state.error = action.payload as string;
|
|
||||||
})
|
|
||||||
.addCase(placeOrderThunk.pending, state => {
|
|
||||||
state.placeOrderLoading = true;
|
|
||||||
state.placeOrderSuccess = false;
|
|
||||||
state.placeOrderError = null;
|
|
||||||
})
|
|
||||||
.addCase(placeOrderThunk.fulfilled, (state, action) => {
|
|
||||||
state.placeOrderLoading = false;
|
|
||||||
state.placeOrderSuccess = true;
|
|
||||||
state.placeOrderResponse = action.payload;
|
|
||||||
})
|
|
||||||
.addCase(placeOrderThunk.rejected, (state, action) => {
|
|
||||||
state.placeOrderLoading = false;
|
|
||||||
state.placeOrderSuccess = false;
|
|
||||||
state.placeOrderError = action.payload as string;
|
|
||||||
})
|
|
||||||
.addCase(getOrderHistoryThunk.pending, state => {
|
|
||||||
state.isLoading = true;
|
|
||||||
state.error = null;
|
|
||||||
})
|
|
||||||
.addCase(getOrderHistoryThunk.fulfilled, (state, action) => {
|
|
||||||
state.isLoading = false;
|
|
||||||
state.orderHistory = action.payload;
|
|
||||||
})
|
|
||||||
.addCase(getOrderHistoryThunk.rejected, (state, action) => {
|
|
||||||
state.isLoading = false;
|
|
||||||
state.error = action.payload as string;
|
|
||||||
})
|
|
||||||
.addCase(getOrderByIdThunk.pending, state => {
|
|
||||||
state.orderDetailsLoading = true;
|
|
||||||
state.orderDetailsError = null;
|
|
||||||
})
|
|
||||||
.addCase(getOrderByIdThunk.fulfilled, (state, action) => {
|
|
||||||
state.orderDetailsLoading = false;
|
|
||||||
state.orderDetails = action.payload;
|
|
||||||
})
|
|
||||||
.addCase(getOrderByIdThunk.rejected, (state, action) => {
|
|
||||||
state.orderDetailsLoading = false;
|
|
||||||
state.orderDetailsError = action.payload as string;
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
export default paymentMethodsReducer;
|
|
||||||
@ -1,74 +0,0 @@
|
|||||||
import {
|
|
||||||
OrderRequest,
|
|
||||||
PaymentMethodsResponse,
|
|
||||||
PlaceOrderResponse,
|
|
||||||
Order,
|
|
||||||
} from '@interfaces';
|
|
||||||
import { createAsyncThunk } from '@reduxjs/toolkit';
|
|
||||||
import {
|
|
||||||
getAllPaymentMethodsApi,
|
|
||||||
getOrderByIdApi,
|
|
||||||
getOrderHistoryApi,
|
|
||||||
placeOrderApi,
|
|
||||||
} from '@api';
|
|
||||||
|
|
||||||
export const getAllPaymentMethodsThunk = createAsyncThunk<
|
|
||||||
PaymentMethodsResponse,
|
|
||||||
void,
|
|
||||||
{ rejectValue: string }
|
|
||||||
>('paymentMethods/getAll', async (_, { rejectWithValue }) => {
|
|
||||||
try {
|
|
||||||
const response = await getAllPaymentMethodsApi();
|
|
||||||
// console.log(response.data);
|
|
||||||
return response;
|
|
||||||
} catch (error: any) {
|
|
||||||
return rejectWithValue(
|
|
||||||
error.response?.data?.error || 'Failed to get payment methods',
|
|
||||||
);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
export const placeOrderThunk = createAsyncThunk<
|
|
||||||
PlaceOrderResponse,
|
|
||||||
OrderRequest,
|
|
||||||
{ rejectValue: string }
|
|
||||||
>('orders/placeOrder', async (payload: OrderRequest, { rejectWithValue }) => {
|
|
||||||
try {
|
|
||||||
const response = await placeOrderApi(payload);
|
|
||||||
return response;
|
|
||||||
} catch (error: any) {
|
|
||||||
return rejectWithValue(
|
|
||||||
error.response?.data?.error || 'Failed to place order',
|
|
||||||
);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
export const getOrderHistoryThunk = createAsyncThunk<
|
|
||||||
Order[],
|
|
||||||
void,
|
|
||||||
{ rejectValue: string }
|
|
||||||
>('orders/getOrderHistory', async (_, { rejectWithValue }) => {
|
|
||||||
try {
|
|
||||||
const response = await getOrderHistoryApi();
|
|
||||||
return response;
|
|
||||||
} catch (error: any) {
|
|
||||||
return rejectWithValue(
|
|
||||||
error.response?.data?.error || 'Failed to get order history',
|
|
||||||
);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
export const getOrderByIdThunk = createAsyncThunk<
|
|
||||||
Order,
|
|
||||||
string,
|
|
||||||
{ rejectValue: string }
|
|
||||||
>('orders/getOrderById', async (orderId: string, { rejectWithValue }) => {
|
|
||||||
try {
|
|
||||||
const response = await getOrderByIdApi(orderId);
|
|
||||||
return response;
|
|
||||||
} catch (error: any) {
|
|
||||||
return rejectWithValue(
|
|
||||||
error.response?.data?.error || 'Failed to get order',
|
|
||||||
);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
@ -12,15 +12,11 @@ import { StackNavigationProp } from '@react-navigation/stack';
|
|||||||
import { getStyles } from './completeProfileScreen.styles';
|
import { getStyles } from './completeProfileScreen.styles';
|
||||||
import { CustomInput, PrimaryButton } from '@components';
|
import { CustomInput, PrimaryButton } from '@components';
|
||||||
import { useAppTheme } from '@theme';
|
import { useAppTheme } from '@theme';
|
||||||
|
import { useAppDispatch } from '../../../store';
|
||||||
|
import { completeProfile } from '../../../store/commonreducers/auth';
|
||||||
import { AuthStackParamList } from '../../../navigation/authStack';
|
import { AuthStackParamList } from '../../../navigation/authStack';
|
||||||
import { useAppDispatch } from '@store';
|
|
||||||
import { saveProfileData } from './reducer';
|
|
||||||
import { OnboardingStackParamList } from '@navigation/onboardingStack';
|
|
||||||
|
|
||||||
type NavProp = StackNavigationProp<
|
type NavProp = StackNavigationProp<AuthStackParamList, 'CompleteProfileScreen'>;
|
||||||
OnboardingStackParamList,
|
|
||||||
'CompleteProfileScreen'
|
|
||||||
>;
|
|
||||||
|
|
||||||
const LOCATION_LABELS = ['Home', 'Work', 'Other'];
|
const LOCATION_LABELS = ['Home', 'Work', 'Other'];
|
||||||
|
|
||||||
@ -32,34 +28,10 @@ export const CompleteProfileScreen: React.FC = () => {
|
|||||||
|
|
||||||
const [name, setName] = useState('');
|
const [name, setName] = useState('');
|
||||||
const [email, setEmail] = useState('');
|
const [email, setEmail] = useState('');
|
||||||
const [gender, setGender] = useState<'MALE' | 'FEMALE' | 'OTHER'>('MALE');
|
|
||||||
|
|
||||||
const [addressLine1, setAddressLine1] = useState('');
|
|
||||||
const [houseNumber, setHouseNumber] = useState('');
|
|
||||||
const [landmark, setLandmark] = useState('');
|
|
||||||
const [city, setCity] = useState('');
|
|
||||||
const [state, setState] = useState('');
|
|
||||||
const [postalCode, setPostalCode] = useState('');
|
|
||||||
const [addressPhone, setAddressPhone] = useState('');
|
|
||||||
|
|
||||||
const [selectedLabel, setSelectedLabel] = useState('Home');
|
const [selectedLabel, setSelectedLabel] = useState('Home');
|
||||||
|
|
||||||
const handleSave = () => {
|
const handleSave = () => {
|
||||||
dispatch(
|
dispatch(completeProfile({ name, email, locationLabels: [selectedLabel] }));
|
||||||
saveProfileData({
|
|
||||||
name,
|
|
||||||
email,
|
|
||||||
gender,
|
|
||||||
addressLine1,
|
|
||||||
houseNumber,
|
|
||||||
landmark,
|
|
||||||
city,
|
|
||||||
state,
|
|
||||||
postalCode,
|
|
||||||
addressPhone,
|
|
||||||
addressLabel: selectedLabel,
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
navigation.navigate('PreferencesScreen');
|
navigation.navigate('PreferencesScreen');
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -68,117 +40,28 @@ export const CompleteProfileScreen: React.FC = () => {
|
|||||||
style={styles.container}
|
style={styles.container}
|
||||||
behavior={Platform.OS === 'ios' ? 'padding' : 'height'}
|
behavior={Platform.OS === 'ios' ? 'padding' : 'height'}
|
||||||
>
|
>
|
||||||
<ScrollView
|
<ScrollView contentContainerStyle={styles.scrollContainer}>
|
||||||
contentContainerStyle={styles.scrollContainer}
|
|
||||||
showsVerticalScrollIndicator={false}
|
|
||||||
>
|
|
||||||
<Text style={styles.title}>Complete Profile</Text>
|
<Text style={styles.title}>Complete Profile</Text>
|
||||||
<Text style={styles.subtitle}>Tell us about yourself</Text>
|
<Text style={styles.subtitle}>Tell us about yourself</Text>
|
||||||
|
|
||||||
<View style={styles.form}>
|
<View style={styles.form}>
|
||||||
{/* Personal Information */}
|
|
||||||
|
|
||||||
<CustomInput
|
<CustomInput
|
||||||
label="Full Name"
|
label="Name"
|
||||||
placeholder="John Doe"
|
placeholder="John Doe"
|
||||||
value={name}
|
value={name}
|
||||||
onChangeText={setName}
|
onChangeText={setName}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<CustomInput
|
<CustomInput
|
||||||
label="Email"
|
label="Email"
|
||||||
placeholder="john@example.com"
|
placeholder="john@example.com"
|
||||||
keyboardType="email-address"
|
keyboardType="email-address"
|
||||||
autoCapitalize="none"
|
|
||||||
value={email}
|
value={email}
|
||||||
onChangeText={setEmail}
|
onChangeText={setEmail}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{/* Gender */}
|
|
||||||
|
|
||||||
<Text style={styles.labelText}>Gender</Text>
|
|
||||||
|
|
||||||
<View style={styles.labelRow}>
|
|
||||||
{['MALE', 'FEMALE', 'OTHER'].map(item => (
|
|
||||||
<TouchableOpacity
|
|
||||||
key={item}
|
|
||||||
style={[
|
|
||||||
styles.labelChip,
|
|
||||||
gender === item && styles.labelChipSelected,
|
|
||||||
]}
|
|
||||||
onPress={() => setGender(item as 'MALE' | 'FEMALE' | 'OTHER')}
|
|
||||||
>
|
|
||||||
<Text
|
|
||||||
style={[
|
|
||||||
styles.labelChipText,
|
|
||||||
gender === item && styles.labelChipTextSelected,
|
|
||||||
]}
|
|
||||||
>
|
|
||||||
{item.charAt(0) + item.slice(1).toLowerCase()}
|
|
||||||
</Text>
|
|
||||||
</TouchableOpacity>
|
|
||||||
))}
|
|
||||||
</View>
|
|
||||||
|
|
||||||
{/* Address */}
|
|
||||||
|
|
||||||
<CustomInput
|
|
||||||
label="Address Line 1"
|
|
||||||
placeholder="123 Main Street"
|
|
||||||
value={addressLine1}
|
|
||||||
onChangeText={setAddressLine1}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<CustomInput
|
|
||||||
label="House / Flat Number"
|
|
||||||
placeholder="Apt 4B"
|
|
||||||
value={houseNumber}
|
|
||||||
onChangeText={setHouseNumber}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<CustomInput
|
|
||||||
label="Landmark"
|
|
||||||
placeholder="Near Central Park"
|
|
||||||
value={landmark}
|
|
||||||
onChangeText={setLandmark}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<CustomInput
|
|
||||||
label="City"
|
|
||||||
placeholder="New York"
|
|
||||||
value={city}
|
|
||||||
onChangeText={setCity}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<CustomInput
|
|
||||||
label="State"
|
|
||||||
placeholder="NY"
|
|
||||||
value={state}
|
|
||||||
onChangeText={setState}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<CustomInput
|
|
||||||
label="Postal Code"
|
|
||||||
placeholder="10001"
|
|
||||||
keyboardType="number-pad"
|
|
||||||
value={postalCode}
|
|
||||||
onChangeText={setPostalCode}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<CustomInput
|
|
||||||
label="Phone Number"
|
|
||||||
placeholder="+1 9999999999"
|
|
||||||
keyboardType="phone-pad"
|
|
||||||
value={addressPhone}
|
|
||||||
onChangeText={setAddressPhone}
|
|
||||||
/>
|
|
||||||
|
|
||||||
{/* Location Label */}
|
|
||||||
|
|
||||||
<Text style={styles.labelText}>Location Label</Text>
|
<Text style={styles.labelText}>Location Label</Text>
|
||||||
|
|
||||||
<View style={styles.labelRow}>
|
<View style={styles.labelRow}>
|
||||||
{LOCATION_LABELS.map(label => (
|
{LOCATION_LABELS.map((label) => (
|
||||||
<TouchableOpacity
|
<TouchableOpacity
|
||||||
key={label}
|
key={label}
|
||||||
style={[
|
style={[
|
||||||
@ -186,6 +69,7 @@ export const CompleteProfileScreen: React.FC = () => {
|
|||||||
selectedLabel === label && styles.labelChipSelected,
|
selectedLabel === label && styles.labelChipSelected,
|
||||||
]}
|
]}
|
||||||
onPress={() => setSelectedLabel(label)}
|
onPress={() => setSelectedLabel(label)}
|
||||||
|
activeOpacity={0.7}
|
||||||
>
|
>
|
||||||
<Text
|
<Text
|
||||||
style={[
|
style={[
|
||||||
|
|||||||
@ -1,2 +1 @@
|
|||||||
export * from './completeProfileScreen';
|
export * from './completeProfileScreen';
|
||||||
export * from './reducer';
|
|
||||||
|
|||||||
@ -1,71 +0,0 @@
|
|||||||
import { createReducer, createAction } from '@reduxjs/toolkit';
|
|
||||||
|
|
||||||
// ─── Actions ──────────────────────────────────────────────────────────────────
|
|
||||||
|
|
||||||
export const saveProfileData = createAction<{
|
|
||||||
name: string;
|
|
||||||
email: string;
|
|
||||||
gender: 'MALE' | 'FEMALE' | 'OTHER';
|
|
||||||
addressLine1: string;
|
|
||||||
houseNumber: string;
|
|
||||||
landmark: string;
|
|
||||||
city: string;
|
|
||||||
state: string;
|
|
||||||
postalCode: string;
|
|
||||||
addressPhone: string;
|
|
||||||
addressLabel: string;
|
|
||||||
}>('completeProfile/saveProfileData');
|
|
||||||
|
|
||||||
export const clearProfileData = createAction('completeProfile/clearProfileData');
|
|
||||||
|
|
||||||
// ─── State ────────────────────────────────────────────────────────────────────
|
|
||||||
|
|
||||||
export interface CompleteProfileState {
|
|
||||||
name: string;
|
|
||||||
email: string;
|
|
||||||
gender: 'MALE' | 'FEMALE' | 'OTHER';
|
|
||||||
addressLine1: string;
|
|
||||||
houseNumber: string;
|
|
||||||
landmark: string;
|
|
||||||
city: string;
|
|
||||||
state: string;
|
|
||||||
postalCode: string;
|
|
||||||
addressPhone: string;
|
|
||||||
addressLabel: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
const initialState: CompleteProfileState = {
|
|
||||||
name: '',
|
|
||||||
email: '',
|
|
||||||
gender: 'MALE',
|
|
||||||
addressLine1: '',
|
|
||||||
houseNumber: '',
|
|
||||||
landmark: '',
|
|
||||||
city: '',
|
|
||||||
state: '',
|
|
||||||
postalCode: '',
|
|
||||||
addressPhone: '',
|
|
||||||
addressLabel: 'Home',
|
|
||||||
};
|
|
||||||
|
|
||||||
// ─── Reducer ──────────────────────────────────────────────────────────────────
|
|
||||||
|
|
||||||
const completeProfileReducer = createReducer(initialState, builder => {
|
|
||||||
builder
|
|
||||||
.addCase(saveProfileData, (state, action) => {
|
|
||||||
state.name = action.payload.name;
|
|
||||||
state.email = action.payload.email;
|
|
||||||
state.gender = action.payload.gender;
|
|
||||||
state.addressLine1 = action.payload.addressLine1;
|
|
||||||
state.houseNumber = action.payload.houseNumber;
|
|
||||||
state.landmark = action.payload.landmark;
|
|
||||||
state.city = action.payload.city;
|
|
||||||
state.state = action.payload.state;
|
|
||||||
state.postalCode = action.payload.postalCode;
|
|
||||||
state.addressPhone = action.payload.addressPhone;
|
|
||||||
state.addressLabel = action.payload.addressLabel;
|
|
||||||
})
|
|
||||||
.addCase(clearProfileData, () => initialState);
|
|
||||||
});
|
|
||||||
|
|
||||||
export default completeProfileReducer;
|
|
||||||
@ -250,12 +250,9 @@ export const getStyles = (colors: any) =>
|
|||||||
color: colors.primary ?? '#05824C',
|
color: colors.primary ?? '#05824C',
|
||||||
},
|
},
|
||||||
|
|
||||||
// ---------- Product Grid ----------
|
// ---------- Provider cards ----------
|
||||||
providerList: {
|
providerList: {
|
||||||
paddingHorizontal: 16,
|
paddingHorizontal: 16,
|
||||||
flexDirection: 'row',
|
|
||||||
flexWrap: 'wrap',
|
|
||||||
justifyContent: 'space-between',
|
|
||||||
},
|
},
|
||||||
providerCardWrap: {
|
providerCardWrap: {
|
||||||
backgroundColor: colors.surface ?? '#FFFFFF',
|
backgroundColor: colors.surface ?? '#FFFFFF',
|
||||||
|
|||||||
@ -17,13 +17,12 @@ import {
|
|||||||
import { BottomTabNavigationProp } from '@react-navigation/bottom-tabs';
|
import { BottomTabNavigationProp } from '@react-navigation/bottom-tabs';
|
||||||
import { StackNavigationProp } from '@react-navigation/stack';
|
import { StackNavigationProp } from '@react-navigation/stack';
|
||||||
import { getStyles } from './homeScreen.styles';
|
import { getStyles } from './homeScreen.styles';
|
||||||
import { ProductCard, SearchBar } from '@components';
|
import { ProviderCard, SearchBar } from '@components';
|
||||||
import { useAppTheme } from '@theme';
|
import { useAppTheme } from '@theme';
|
||||||
import { Product } from '../../../interfaces';
|
import { getProvidersApi } from '../../../api/deliveryApi';
|
||||||
|
import { Provider } from '../../../interfaces';
|
||||||
import { AppStackParamList } from '../../../navigation/appStack';
|
import { AppStackParamList } from '../../../navigation/appStack';
|
||||||
import { MainTabParamList } from '../../../navigation/mainTabNavigator';
|
import { MainTabParamList } from '../../../navigation/mainTabNavigator';
|
||||||
import { fetchCustomerDetails, useAppDispatch, useAppSelector } from '@store';
|
|
||||||
import { getAllProductsThunk } from './thunk';
|
|
||||||
|
|
||||||
type NavProp = CompositeNavigationProp<
|
type NavProp = CompositeNavigationProp<
|
||||||
BottomTabNavigationProp<MainTabParamList, 'HomeScreen'>,
|
BottomTabNavigationProp<MainTabParamList, 'HomeScreen'>,
|
||||||
@ -75,21 +74,18 @@ export const HomeScreen: React.FC = () => {
|
|||||||
const styles = getStyles(colors);
|
const styles = getStyles(colors);
|
||||||
const navigation = useNavigation<NavProp>();
|
const navigation = useNavigation<NavProp>();
|
||||||
|
|
||||||
const dispatch = useAppDispatch();
|
const [providers, setProviders] = useState<Provider[]>([]);
|
||||||
const { products, isLoading } = useAppSelector(state => state.home);
|
|
||||||
|
|
||||||
const [selectedCategory, setSelectedCategory] = useState('all');
|
const [selectedCategory, setSelectedCategory] = useState('all');
|
||||||
const [activeBanner, setActiveBanner] = useState(0);
|
const [activeBanner, setActiveBanner] = useState(0);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
dispatch(getAllProductsThunk());
|
getProvidersApi().then(setProviders);
|
||||||
dispatch(fetchCustomerDetails());
|
}, []);
|
||||||
}, [dispatch]);
|
|
||||||
|
|
||||||
const filteredProducts =
|
const filteredProviders =
|
||||||
selectedCategory === 'all'
|
selectedCategory === 'all'
|
||||||
? products
|
? providers
|
||||||
: products.filter(p => p.category?.name === selectedCategory);
|
: providers.filter(p => p.tag === selectedCategory);
|
||||||
|
|
||||||
const handleSearchFocus = useCallback(() => {
|
const handleSearchFocus = useCallback(() => {
|
||||||
navigation.navigate('SearchScreen');
|
navigation.navigate('SearchScreen');
|
||||||
@ -122,12 +118,8 @@ export const HomeScreen: React.FC = () => {
|
|||||||
</View>
|
</View>
|
||||||
</TouchableOpacity>
|
</TouchableOpacity>
|
||||||
|
|
||||||
<TouchableOpacity
|
<TouchableOpacity style={styles.avatarButton} activeOpacity={0.7}>
|
||||||
style={styles.avatarButton}
|
<Text style={styles.avatarEmoji}>🔔</Text>
|
||||||
activeOpacity={0.7}
|
|
||||||
onPress={() => navigation.navigate('CartScreen')}
|
|
||||||
>
|
|
||||||
<Text style={styles.avatarEmoji}>🧺</Text>
|
|
||||||
<View style={styles.notifDot} />
|
<View style={styles.notifDot} />
|
||||||
</TouchableOpacity>
|
</TouchableOpacity>
|
||||||
</View>
|
</View>
|
||||||
@ -232,8 +224,8 @@ export const HomeScreen: React.FC = () => {
|
|||||||
{selectedCategory === 'all' ? 'Popular near you' : selectedCategory}
|
{selectedCategory === 'all' ? 'Popular near you' : selectedCategory}
|
||||||
</Text>
|
</Text>
|
||||||
<Text style={styles.sectionSubtitle}>
|
<Text style={styles.sectionSubtitle}>
|
||||||
{filteredProducts.length} product
|
{filteredProviders.length} place
|
||||||
{filteredProducts.length === 1 ? '' : 's'} available
|
{filteredProviders.length === 1 ? '' : 's'} delivering to you
|
||||||
</Text>
|
</Text>
|
||||||
</View>
|
</View>
|
||||||
<TouchableOpacity activeOpacity={0.7}>
|
<TouchableOpacity activeOpacity={0.7}>
|
||||||
@ -241,31 +233,31 @@ export const HomeScreen: React.FC = () => {
|
|||||||
</TouchableOpacity>
|
</TouchableOpacity>
|
||||||
</View>
|
</View>
|
||||||
|
|
||||||
{/* Product list */}
|
{/* Provider list */}
|
||||||
<View style={styles.providerList}>
|
<View style={styles.providerList}>
|
||||||
{filteredProducts.length === 0 && (
|
{filteredProviders.length === 0 && (
|
||||||
<View style={[styles.emptyWrap, { width: '100%' }]}>
|
<View style={styles.emptyWrap}>
|
||||||
<Text style={styles.emptyEmoji}>🛍️</Text>
|
<Text style={styles.emptyEmoji}>🍽️</Text>
|
||||||
<Text style={styles.emptyText}>No products found</Text>
|
<Text style={styles.emptyText}>
|
||||||
|
No providers in this category yet
|
||||||
|
</Text>
|
||||||
</View>
|
</View>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{filteredProducts.map(product => (
|
{filteredProviders.map(provider => (
|
||||||
<ProductCard
|
<ProviderCard
|
||||||
key={product.id}
|
key={provider.id}
|
||||||
id={product.id}
|
imageUrl={provider.imageUrl}
|
||||||
imageUrl={product.imageUrl}
|
name={provider.name}
|
||||||
name={product.name}
|
rating={provider.rating}
|
||||||
price={product.price}
|
deliveryTime={provider.deliveryTime}
|
||||||
compareAtPrice={product.compareAtPrice}
|
tag={provider.tag}
|
||||||
brand={product.brand || product.merchant?.name}
|
discountText={provider.discountText}
|
||||||
currency={product.currency === 'INR' ? '₹' : product.currency}
|
onPress={() =>
|
||||||
onPress={() => {
|
|
||||||
navigation.navigate('ProviderDetailsScreen', {
|
navigation.navigate('ProviderDetailsScreen', {
|
||||||
providerId: product.id,
|
providerId: provider.id,
|
||||||
providerName: product.name,
|
})
|
||||||
});
|
}
|
||||||
}}
|
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
</View>
|
</View>
|
||||||
|
|||||||
@ -1,3 +1 @@
|
|||||||
export * from './homeScreen';
|
export * from './homeScreen';
|
||||||
export { default as homeReducer } from './reducer';
|
|
||||||
export * from './thunk';
|
|
||||||
|
|||||||
@ -1,36 +0,0 @@
|
|||||||
import { createReducer } from '@reduxjs/toolkit';
|
|
||||||
import { Product, Products } from '@interfaces';
|
|
||||||
import { getAllProductsThunk } from './thunk';
|
|
||||||
|
|
||||||
export interface HomeState {
|
|
||||||
products: Products[];
|
|
||||||
isLoading: boolean;
|
|
||||||
error: string | null;
|
|
||||||
}
|
|
||||||
|
|
||||||
const initialState: HomeState = {
|
|
||||||
products: [],
|
|
||||||
isLoading: false,
|
|
||||||
error: null,
|
|
||||||
};
|
|
||||||
|
|
||||||
const homeReducer = createReducer(initialState, builder => {
|
|
||||||
builder
|
|
||||||
.addCase(getAllProductsThunk.pending, state => {
|
|
||||||
state.isLoading = true;
|
|
||||||
state.error = null;
|
|
||||||
})
|
|
||||||
.addCase(getAllProductsThunk.fulfilled, (state, action) => {
|
|
||||||
// Handle the API returning { products, meta }
|
|
||||||
state.products =
|
|
||||||
action.payload?.products ||
|
|
||||||
(Array.isArray(action.payload) ? action.payload : []);
|
|
||||||
state.isLoading = false;
|
|
||||||
})
|
|
||||||
.addCase(getAllProductsThunk.rejected, (state, action) => {
|
|
||||||
state.isLoading = false;
|
|
||||||
state.error = action.payload || 'Failed to fetch products';
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
export default homeReducer;
|
|
||||||
@ -1,17 +0,0 @@
|
|||||||
import { createAsyncThunk } from '@reduxjs/toolkit';
|
|
||||||
import { getProductsApi, GetProductsResponse } from '@api';
|
|
||||||
|
|
||||||
export const getAllProductsThunk = createAsyncThunk<
|
|
||||||
GetProductsResponse,
|
|
||||||
void,
|
|
||||||
{ rejectValue: string }
|
|
||||||
>('home/getAllProducts', async (_, { rejectWithValue }) => {
|
|
||||||
try {
|
|
||||||
const response = await getProductsApi();
|
|
||||||
return response;
|
|
||||||
} catch (error: any) {
|
|
||||||
return rejectWithValue(
|
|
||||||
error.response?.data?.message || 'Failed to fetch products',
|
|
||||||
);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
@ -19,5 +19,3 @@ export * from './myOrdersScreen';
|
|||||||
export * from './offersScreen';
|
export * from './offersScreen';
|
||||||
export * from './helpSupportScreen';
|
export * from './helpSupportScreen';
|
||||||
export * from './accountScreen';
|
export * from './accountScreen';
|
||||||
export * from './writeReviewScreen';
|
|
||||||
export * from './orderDetailsScreen';
|
|
||||||
|
|||||||
@ -1,9 +1,11 @@
|
|||||||
import React, { useEffect, useState } from 'react';
|
import React, { useState } from 'react';
|
||||||
import { View, Text, FlatList, TouchableOpacity } from 'react-native';
|
|
||||||
import {
|
import {
|
||||||
useNavigation,
|
View,
|
||||||
CompositeNavigationProp,
|
Text,
|
||||||
} from '@react-navigation/native';
|
FlatList,
|
||||||
|
TouchableOpacity,
|
||||||
|
} from 'react-native';
|
||||||
|
import { useNavigation, CompositeNavigationProp } from '@react-navigation/native';
|
||||||
import { BottomTabNavigationProp } from '@react-navigation/bottom-tabs';
|
import { BottomTabNavigationProp } from '@react-navigation/bottom-tabs';
|
||||||
import { StackNavigationProp } from '@react-navigation/stack';
|
import { StackNavigationProp } from '@react-navigation/stack';
|
||||||
import { getStyles } from './myOrdersScreen.styles';
|
import { getStyles } from './myOrdersScreen.styles';
|
||||||
@ -11,9 +13,6 @@ import { Header, OrderHistoryCard } from '@components';
|
|||||||
import { useAppTheme } from '@theme';
|
import { useAppTheme } from '@theme';
|
||||||
import { AppStackParamList } from '../../../navigation/appStack';
|
import { AppStackParamList } from '../../../navigation/appStack';
|
||||||
import { MainTabParamList } from '../../../navigation/mainTabNavigator';
|
import { MainTabParamList } from '../../../navigation/mainTabNavigator';
|
||||||
import { RootState, useAppDispatch, useAppSelector } from '@store';
|
|
||||||
import { getOrderHistoryThunk } from '../checkoutPaymentScreen';
|
|
||||||
import { formatDate } from '@utils/helper';
|
|
||||||
|
|
||||||
type MyOrdersNavProp = CompositeNavigationProp<
|
type MyOrdersNavProp = CompositeNavigationProp<
|
||||||
BottomTabNavigationProp<MainTabParamList, 'MyOrdersScreen'>,
|
BottomTabNavigationProp<MainTabParamList, 'MyOrdersScreen'>,
|
||||||
@ -22,30 +21,9 @@ type MyOrdersNavProp = CompositeNavigationProp<
|
|||||||
|
|
||||||
const TABS = ['All', 'Ongoing', 'Completed'];
|
const TABS = ['All', 'Ongoing', 'Completed'];
|
||||||
const mockOrders = [
|
const mockOrders = [
|
||||||
{
|
{ id: 'ORD-591283', providerName: 'Pizza Planet', providerImage: '', orderDate: '29 Jun 2026', status: 'Delivered', total: 599 },
|
||||||
id: 'ORD-591283',
|
{ id: 'ORD-771239', providerName: 'Fresh Mart', providerImage: '', orderDate: '28 Jun 2026', status: 'Ongoing', total: 349 },
|
||||||
providerName: 'Pizza Planet',
|
{ id: 'ORD-108239', providerName: 'Burger Barn', providerImage: '', orderDate: '27 Jun 2026', status: 'Delivered', total: 449 },
|
||||||
providerImage: '',
|
|
||||||
orderDate: '29 Jun 2026',
|
|
||||||
status: 'Delivered',
|
|
||||||
total: 599,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'ORD-771239',
|
|
||||||
providerName: 'Fresh Mart',
|
|
||||||
providerImage: '',
|
|
||||||
orderDate: '28 Jun 2026',
|
|
||||||
status: 'Ongoing',
|
|
||||||
total: 349,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'ORD-108239',
|
|
||||||
providerName: 'Burger Barn',
|
|
||||||
providerImage: '',
|
|
||||||
orderDate: '27 Jun 2026',
|
|
||||||
status: 'Delivered',
|
|
||||||
total: 449,
|
|
||||||
},
|
|
||||||
];
|
];
|
||||||
|
|
||||||
export const MyOrdersScreen: React.FC = () => {
|
export const MyOrdersScreen: React.FC = () => {
|
||||||
@ -53,42 +31,24 @@ export const MyOrdersScreen: React.FC = () => {
|
|||||||
const styles = getStyles(colors);
|
const styles = getStyles(colors);
|
||||||
const navigation = useNavigation<MyOrdersNavProp>();
|
const navigation = useNavigation<MyOrdersNavProp>();
|
||||||
const [activeTab, setActiveTab] = useState('All');
|
const [activeTab, setActiveTab] = useState('All');
|
||||||
const dispatch = useAppDispatch();
|
|
||||||
const { orderHistory } = useAppSelector(
|
|
||||||
(state: RootState) => state.paymentMethods,
|
|
||||||
);
|
|
||||||
console.log(orderHistory);
|
|
||||||
|
|
||||||
useEffect(() => {
|
const filteredOrders = activeTab === 'All'
|
||||||
dispatch(getOrderHistoryThunk());
|
? mockOrders
|
||||||
}, []);
|
: activeTab === 'Ongoing'
|
||||||
|
? mockOrders.filter((o) => o.status === 'Ongoing')
|
||||||
const ONGOING_STATUSES = [
|
: mockOrders.filter((o) => o.status === 'Delivered');
|
||||||
'PENDING',
|
|
||||||
'CONFIRMED',
|
|
||||||
'PREPARING',
|
|
||||||
'READY_FOR_PICKUP',
|
|
||||||
'OUT_FOR_DELIVERY',
|
|
||||||
];
|
|
||||||
const COMPLETED_STATUSES = ['DELIVERED', 'CANCELLED'];
|
|
||||||
|
|
||||||
const orders = Array.isArray(orderHistory) ? orderHistory : [];
|
|
||||||
|
|
||||||
const filteredOrders =
|
|
||||||
activeTab === 'All'
|
|
||||||
? orders
|
|
||||||
: activeTab === 'Ongoing'
|
|
||||||
? orders.filter(o => ONGOING_STATUSES.includes(o.status))
|
|
||||||
: orders.filter(o => COMPLETED_STATUSES.includes(o.status));
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<View style={styles.container}>
|
<View style={styles.container}>
|
||||||
<Header title="My Orders" />
|
<Header title="My Orders" />
|
||||||
<View style={styles.tabRow}>
|
<View style={styles.tabRow}>
|
||||||
{TABS.map(tab => (
|
{TABS.map((tab) => (
|
||||||
<TouchableOpacity
|
<TouchableOpacity
|
||||||
key={tab}
|
key={tab}
|
||||||
style={[styles.tab, activeTab === tab && styles.tabActive]}
|
style={[
|
||||||
|
styles.tab,
|
||||||
|
activeTab === tab && styles.tabActive,
|
||||||
|
]}
|
||||||
onPress={() => setActiveTab(tab)}
|
onPress={() => setActiveTab(tab)}
|
||||||
activeOpacity={0.7}
|
activeOpacity={0.7}
|
||||||
>
|
>
|
||||||
@ -105,36 +65,25 @@ export const MyOrdersScreen: React.FC = () => {
|
|||||||
</View>
|
</View>
|
||||||
<FlatList
|
<FlatList
|
||||||
data={filteredOrders}
|
data={filteredOrders}
|
||||||
keyExtractor={item => item.id}
|
keyExtractor={(item) => item.id}
|
||||||
renderItem={({ item }) => (
|
renderItem={({ item }) => (
|
||||||
<OrderHistoryCard
|
<OrderHistoryCard
|
||||||
providerName={item?.merchant?.name || ''}
|
providerName={item.providerName}
|
||||||
providerImage={item?.merchant?.imageUrl || ''}
|
providerImage={item.providerImage}
|
||||||
orderDate={formatDate(item?.createdAt)}
|
orderDate={item.orderDate}
|
||||||
status={item.status}
|
status={item.status}
|
||||||
total={Number(item?.totalAmount) || 0}
|
total={item.total}
|
||||||
items={item.orderItems}
|
|
||||||
onReorder={() => {
|
onReorder={() => {
|
||||||
navigation.navigate('ProviderDetailsScreen', {
|
navigation.navigate('ProviderDetailsScreen', {
|
||||||
providerId: 'p1',
|
providerId: 'p1',
|
||||||
providerName: item?.merchant?.name || '',
|
providerName: item.providerName,
|
||||||
});
|
});
|
||||||
}}
|
}}
|
||||||
onDetails={() => {
|
onDetails={() => {
|
||||||
if (
|
if (item.status === 'Ongoing') {
|
||||||
item?.status === 'PENDING' ||
|
navigation.navigate('OrderTrackingScreen', { orderId: item.id });
|
||||||
item?.status === 'CONFIRMED' ||
|
|
||||||
item?.status === 'PREPARING' ||
|
|
||||||
item?.status === 'READY_FOR_PICKUP' ||
|
|
||||||
item?.status === 'OUT_FOR_DELIVERY'
|
|
||||||
) {
|
|
||||||
navigation.navigate('OrderDetailsScreen', {
|
|
||||||
orderId: item.id,
|
|
||||||
});
|
|
||||||
} else {
|
} else {
|
||||||
navigation.navigate('OrderDeliveredScreen', {
|
navigation.navigate('OrderDeliveredScreen', { orderId: item.id });
|
||||||
orderId: item.id,
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@ -3,9 +3,8 @@ import { View, Text } from 'react-native';
|
|||||||
import { getStyles } from './onboardingCompleteScreen.styles';
|
import { getStyles } from './onboardingCompleteScreen.styles';
|
||||||
import { PrimaryButton } from '@components';
|
import { PrimaryButton } from '@components';
|
||||||
import { useAppTheme } from '@theme';
|
import { useAppTheme } from '@theme';
|
||||||
|
import { useAppDispatch } from '../../../store';
|
||||||
import { useAppDispatch } from '@store';
|
import { completeOnboarding } from '../../../store/commonreducers/auth';
|
||||||
import { completeOnboarding } from '@store/commonreducers/auth';
|
|
||||||
|
|
||||||
export const OnboardingCompleteScreen: React.FC = () => {
|
export const OnboardingCompleteScreen: React.FC = () => {
|
||||||
const { colors } = useAppTheme();
|
const { colors } = useAppTheme();
|
||||||
@ -13,9 +12,6 @@ export const OnboardingCompleteScreen: React.FC = () => {
|
|||||||
const dispatch = useAppDispatch();
|
const dispatch = useAppDispatch();
|
||||||
|
|
||||||
const handleExplore = () => {
|
const handleExplore = () => {
|
||||||
// Dispatching completeOnboarding sets user.status = 'ACTIVE' in Redux.
|
|
||||||
// RootNavigator watches this value and automatically swaps OnboardingStack
|
|
||||||
// → AppStack. No direct navigation call needed or possible here.
|
|
||||||
dispatch(completeOnboarding());
|
dispatch(completeOnboarding());
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -29,7 +25,10 @@ export const OnboardingCompleteScreen: React.FC = () => {
|
|||||||
</Text>
|
</Text>
|
||||||
</View>
|
</View>
|
||||||
<View style={styles.footer}>
|
<View style={styles.footer}>
|
||||||
<PrimaryButton title="Explore Now" onPress={handleExplore} />
|
<PrimaryButton
|
||||||
|
title="Explore Now"
|
||||||
|
onPress={handleExplore}
|
||||||
|
/>
|
||||||
</View>
|
</View>
|
||||||
</View>
|
</View>
|
||||||
);
|
);
|
||||||
|
|||||||
@ -1 +0,0 @@
|
|||||||
export * from './orderDetailsScreen';
|
|
||||||
@ -1,194 +0,0 @@
|
|||||||
import { StyleSheet } from 'react-native';
|
|
||||||
import { typography } from '@theme';
|
|
||||||
|
|
||||||
export const getStyles = (colors: any) =>
|
|
||||||
StyleSheet.create({
|
|
||||||
container: {
|
|
||||||
flex: 1,
|
|
||||||
backgroundColor: colors.background,
|
|
||||||
},
|
|
||||||
loadingContainer: {
|
|
||||||
flex: 1,
|
|
||||||
justifyContent: 'center',
|
|
||||||
alignItems: 'center',
|
|
||||||
},
|
|
||||||
loadingText: {
|
|
||||||
marginTop: 12,
|
|
||||||
fontSize: typography.fontSize.md,
|
|
||||||
color: colors.textSecondary,
|
|
||||||
},
|
|
||||||
errorContainer: {
|
|
||||||
flex: 1,
|
|
||||||
justifyContent: 'center',
|
|
||||||
alignItems: 'center',
|
|
||||||
padding: 20,
|
|
||||||
},
|
|
||||||
errorText: {
|
|
||||||
fontSize: typography.fontSize.md,
|
|
||||||
color: colors.error,
|
|
||||||
textAlign: 'center',
|
|
||||||
},
|
|
||||||
content: {
|
|
||||||
padding: 16,
|
|
||||||
paddingBottom: 40,
|
|
||||||
},
|
|
||||||
section: {
|
|
||||||
backgroundColor: colors.cardBg,
|
|
||||||
borderRadius: 12,
|
|
||||||
padding: 16,
|
|
||||||
marginBottom: 16,
|
|
||||||
borderWidth: 1,
|
|
||||||
borderColor: colors.border,
|
|
||||||
},
|
|
||||||
// Used for the OrderStatusTimeline section — no inner padding as the
|
|
||||||
// component renders its own self-contained card with rounded borders.
|
|
||||||
timelineSection: {
|
|
||||||
marginBottom: 16,
|
|
||||||
},
|
|
||||||
sectionTitle: {
|
|
||||||
fontSize: typography.fontSize.md,
|
|
||||||
fontWeight: typography.fontWeight.bold,
|
|
||||||
color: colors.text,
|
|
||||||
marginBottom: 12,
|
|
||||||
},
|
|
||||||
headerRow: {
|
|
||||||
flexDirection: 'row',
|
|
||||||
justifyContent: 'space-between',
|
|
||||||
alignItems: 'center',
|
|
||||||
marginBottom: 12,
|
|
||||||
},
|
|
||||||
orderNumber: {
|
|
||||||
fontSize: typography.fontSize.lg,
|
|
||||||
fontWeight: typography.fontWeight.bold,
|
|
||||||
color: colors.text,
|
|
||||||
},
|
|
||||||
statusBadge: {
|
|
||||||
paddingHorizontal: 10,
|
|
||||||
paddingVertical: 4,
|
|
||||||
borderRadius: 12,
|
|
||||||
backgroundColor: '#E8F5E9',
|
|
||||||
},
|
|
||||||
statusText: {
|
|
||||||
fontSize: typography.fontSize.xs,
|
|
||||||
fontWeight: typography.fontWeight.bold,
|
|
||||||
color: colors.primary,
|
|
||||||
},
|
|
||||||
dateText: {
|
|
||||||
fontSize: typography.fontSize.sm,
|
|
||||||
color: colors.textSecondary,
|
|
||||||
marginBottom: 4,
|
|
||||||
},
|
|
||||||
merchantRow: {
|
|
||||||
flexDirection: 'row',
|
|
||||||
alignItems: 'center',
|
|
||||||
marginTop: 8,
|
|
||||||
paddingTop: 12,
|
|
||||||
borderTopWidth: 1,
|
|
||||||
borderTopColor: colors.border,
|
|
||||||
},
|
|
||||||
merchantIcon: {
|
|
||||||
width: 32,
|
|
||||||
height: 32,
|
|
||||||
borderRadius: 16,
|
|
||||||
backgroundColor: colors.surface,
|
|
||||||
justifyContent: 'center',
|
|
||||||
alignItems: 'center',
|
|
||||||
marginRight: 12,
|
|
||||||
},
|
|
||||||
merchantName: {
|
|
||||||
fontSize: typography.fontSize.md,
|
|
||||||
fontWeight: typography.fontWeight.semibold,
|
|
||||||
color: colors.text,
|
|
||||||
},
|
|
||||||
itemRow: {
|
|
||||||
flexDirection: 'row',
|
|
||||||
justifyContent: 'space-between',
|
|
||||||
alignItems: 'flex-start',
|
|
||||||
marginBottom: 12,
|
|
||||||
},
|
|
||||||
itemInfo: {
|
|
||||||
flex: 1,
|
|
||||||
flexDirection: 'row',
|
|
||||||
alignItems: 'flex-start',
|
|
||||||
marginRight: 16,
|
|
||||||
},
|
|
||||||
itemQuantityBadge: {
|
|
||||||
backgroundColor: colors.surface,
|
|
||||||
borderWidth: 1,
|
|
||||||
borderColor: colors.border,
|
|
||||||
borderRadius: 4,
|
|
||||||
paddingHorizontal: 6,
|
|
||||||
paddingVertical: 2,
|
|
||||||
marginRight: 10,
|
|
||||||
marginTop: 2,
|
|
||||||
},
|
|
||||||
itemQuantityText: {
|
|
||||||
fontSize: typography.fontSize.xs,
|
|
||||||
fontWeight: typography.fontWeight.bold,
|
|
||||||
color: colors.text,
|
|
||||||
},
|
|
||||||
itemName: {
|
|
||||||
fontSize: typography.fontSize.sm,
|
|
||||||
color: colors.text,
|
|
||||||
lineHeight: 20,
|
|
||||||
},
|
|
||||||
itemPrice: {
|
|
||||||
fontSize: typography.fontSize.sm,
|
|
||||||
fontWeight: typography.fontWeight.semibold,
|
|
||||||
color: colors.text,
|
|
||||||
marginTop: 2,
|
|
||||||
},
|
|
||||||
billRow: {
|
|
||||||
flexDirection: 'row',
|
|
||||||
justifyContent: 'space-between',
|
|
||||||
marginBottom: 8,
|
|
||||||
},
|
|
||||||
billLabel: {
|
|
||||||
fontSize: typography.fontSize.sm,
|
|
||||||
color: colors.textSecondary,
|
|
||||||
},
|
|
||||||
billValue: {
|
|
||||||
fontSize: typography.fontSize.sm,
|
|
||||||
color: colors.text,
|
|
||||||
fontWeight: typography.fontWeight.medium,
|
|
||||||
},
|
|
||||||
billDivider: {
|
|
||||||
height: 1,
|
|
||||||
backgroundColor: colors.border,
|
|
||||||
marginVertical: 12,
|
|
||||||
},
|
|
||||||
billTotalLabel: {
|
|
||||||
fontSize: typography.fontSize.md,
|
|
||||||
fontWeight: typography.fontWeight.bold,
|
|
||||||
color: colors.text,
|
|
||||||
},
|
|
||||||
billTotalValue: {
|
|
||||||
fontSize: typography.fontSize.md,
|
|
||||||
fontWeight: typography.fontWeight.bold,
|
|
||||||
color: colors.primary,
|
|
||||||
},
|
|
||||||
addressType: {
|
|
||||||
fontSize: typography.fontSize.sm,
|
|
||||||
fontWeight: typography.fontWeight.semibold,
|
|
||||||
color: colors.text,
|
|
||||||
marginBottom: 4,
|
|
||||||
},
|
|
||||||
addressText: {
|
|
||||||
fontSize: typography.fontSize.sm,
|
|
||||||
color: colors.textSecondary,
|
|
||||||
lineHeight: 20,
|
|
||||||
},
|
|
||||||
paymentRow: {
|
|
||||||
flexDirection: 'row',
|
|
||||||
alignItems: 'center',
|
|
||||||
},
|
|
||||||
paymentIcon: {
|
|
||||||
marginRight: 8,
|
|
||||||
fontSize: 20,
|
|
||||||
},
|
|
||||||
paymentText: {
|
|
||||||
fontSize: typography.fontSize.sm,
|
|
||||||
color: colors.text,
|
|
||||||
fontWeight: typography.fontWeight.medium,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
@ -1,191 +0,0 @@
|
|||||||
import React, { useEffect } from 'react';
|
|
||||||
import { View, Text, ScrollView, ActivityIndicator } from 'react-native';
|
|
||||||
import { useRoute, useNavigation } from '@react-navigation/native';
|
|
||||||
import { StackNavigationProp } from '@react-navigation/stack';
|
|
||||||
import { RouteProp } from '@react-navigation/native';
|
|
||||||
import { Header, OrderStatusTimeline } from '@components';
|
|
||||||
import { useAppTheme } from '@theme';
|
|
||||||
import { getStyles } from './orderDetailsScreen.styles';
|
|
||||||
import { RootState, useAppDispatch, useAppSelector } from '@store';
|
|
||||||
import { getOrderByIdThunk } from '../checkoutPaymentScreen';
|
|
||||||
import { formatDate, formatTime } from '@utils/helper';
|
|
||||||
import { AppStackParamList } from '../../../navigation/appStack';
|
|
||||||
|
|
||||||
type OrderDetailsRouteProp = RouteProp<AppStackParamList, 'OrderDetailsScreen'>;
|
|
||||||
type OrderDetailsNavProp = StackNavigationProp<AppStackParamList>;
|
|
||||||
|
|
||||||
export const OrderDetailsScreen: React.FC = () => {
|
|
||||||
const { colors } = useAppTheme();
|
|
||||||
const styles = getStyles(colors);
|
|
||||||
const route = useRoute<OrderDetailsRouteProp>();
|
|
||||||
const navigation = useNavigation<OrderDetailsNavProp>();
|
|
||||||
const dispatch = useAppDispatch();
|
|
||||||
|
|
||||||
const orderId = route.params?.orderId;
|
|
||||||
|
|
||||||
const { orderDetails, orderDetailsLoading, orderDetailsError } =
|
|
||||||
useAppSelector((state: RootState) => state.paymentMethods);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (orderId) {
|
|
||||||
dispatch(getOrderByIdThunk(orderId));
|
|
||||||
}
|
|
||||||
}, [dispatch, orderId]);
|
|
||||||
|
|
||||||
if (orderDetailsLoading) {
|
|
||||||
return (
|
|
||||||
<View style={styles.container}>
|
|
||||||
<Header title="Order Details" onBack={() => navigation.goBack()} />
|
|
||||||
<View style={styles.loadingContainer}>
|
|
||||||
<ActivityIndicator size="large" color={colors.primary} />
|
|
||||||
<Text style={styles.loadingText}>Fetching order details...</Text>
|
|
||||||
</View>
|
|
||||||
</View>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (orderDetailsError || !orderDetails) {
|
|
||||||
return (
|
|
||||||
<View style={styles.container}>
|
|
||||||
<Header title="Order Details" onBack={() => navigation.goBack()} />
|
|
||||||
<View style={styles.errorContainer}>
|
|
||||||
<Text style={styles.errorText}>
|
|
||||||
{orderDetailsError || 'Could not load order details.'}
|
|
||||||
</Text>
|
|
||||||
</View>
|
|
||||||
</View>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
const orderDate = orderDetails.createdAt
|
|
||||||
? `${formatDate(orderDetails.createdAt)} at ${formatTime(
|
|
||||||
orderDetails.createdAt,
|
|
||||||
)}`
|
|
||||||
: '';
|
|
||||||
|
|
||||||
return (
|
|
||||||
<View style={styles.container}>
|
|
||||||
<Header title="Order Details" onBack={() => navigation.goBack()} />
|
|
||||||
<ScrollView contentContainerStyle={styles.content}>
|
|
||||||
{/* Top Info Section */}
|
|
||||||
<View style={styles.section}>
|
|
||||||
<View style={styles.headerRow}>
|
|
||||||
<Text style={styles.orderNumber}>
|
|
||||||
Order {orderDetails.orderNumber?.split('-').pop() || ''}
|
|
||||||
</Text>
|
|
||||||
<View style={styles.statusBadge}>
|
|
||||||
<Text style={styles.statusText}>{orderDetails.status}</Text>
|
|
||||||
</View>
|
|
||||||
</View>
|
|
||||||
<Text style={styles.dateText}>Placed on {orderDate}</Text>
|
|
||||||
<Text style={styles.dateText}>
|
|
||||||
Payment: {orderDetails.paymentMethod}
|
|
||||||
</Text>
|
|
||||||
|
|
||||||
{/* Merchant Info */}
|
|
||||||
<View style={styles.merchantRow}>
|
|
||||||
<View style={styles.merchantIcon}>
|
|
||||||
<Text>🏪</Text>
|
|
||||||
</View>
|
|
||||||
<Text style={styles.merchantName}>
|
|
||||||
{orderDetails.merchant?.name || 'Store'}
|
|
||||||
</Text>
|
|
||||||
</View>
|
|
||||||
</View>
|
|
||||||
|
|
||||||
{/* Order Status Timeline Section */}
|
|
||||||
<View style={styles.timelineSection}>
|
|
||||||
<Text style={styles.sectionTitle}>Order Status</Text>
|
|
||||||
<OrderStatusTimeline
|
|
||||||
tracking={orderDetails.tracking}
|
|
||||||
currentStatus={orderDetails.status}
|
|
||||||
/>
|
|
||||||
</View>
|
|
||||||
|
|
||||||
{/* Order Items Section */}
|
|
||||||
<View style={styles.section}>
|
|
||||||
<Text style={styles.sectionTitle}>Order Items</Text>
|
|
||||||
{orderDetails.orderItems?.map((item, index) => (
|
|
||||||
<View key={item.id || index} style={styles.itemRow}>
|
|
||||||
<View style={styles.itemInfo}>
|
|
||||||
<View style={styles.itemQuantityBadge}>
|
|
||||||
<Text style={styles.itemQuantityText}>{item.quantity}x</Text>
|
|
||||||
</View>
|
|
||||||
<Text style={styles.itemName}>{item.name}</Text>
|
|
||||||
</View>
|
|
||||||
<Text style={styles.itemPrice}>₹{item.totalAmount}</Text>
|
|
||||||
</View>
|
|
||||||
))}
|
|
||||||
</View>
|
|
||||||
|
|
||||||
{/* Bill Summary Section */}
|
|
||||||
<View style={styles.section}>
|
|
||||||
<Text style={styles.sectionTitle}>Bill Summary</Text>
|
|
||||||
<View style={styles.billRow}>
|
|
||||||
<Text style={styles.billLabel}>Item Total</Text>
|
|
||||||
<Text style={styles.billValue}>₹{orderDetails.subtotal}</Text>
|
|
||||||
</View>
|
|
||||||
<View style={styles.billRow}>
|
|
||||||
<Text style={styles.billLabel}>Delivery Fee</Text>
|
|
||||||
<Text style={styles.billValue}>₹{orderDetails.deliveryFee}</Text>
|
|
||||||
</View>
|
|
||||||
<View style={styles.billRow}>
|
|
||||||
<Text style={styles.billLabel}>Platform Fee</Text>
|
|
||||||
<Text style={styles.billValue}>₹{orderDetails.platformFee}</Text>
|
|
||||||
</View>
|
|
||||||
<View style={styles.billRow}>
|
|
||||||
<Text style={styles.billLabel}>Taxes</Text>
|
|
||||||
<Text style={styles.billValue}>₹{orderDetails.taxAmount}</Text>
|
|
||||||
</View>
|
|
||||||
|
|
||||||
<View style={styles.billDivider} />
|
|
||||||
|
|
||||||
<View style={styles.billRow}>
|
|
||||||
<Text style={styles.billTotalLabel}>Grand Total</Text>
|
|
||||||
<Text style={styles.billTotalValue}>
|
|
||||||
₹{orderDetails.totalAmount}
|
|
||||||
</Text>
|
|
||||||
</View>
|
|
||||||
</View>
|
|
||||||
|
|
||||||
{/* Delivery Address Section */}
|
|
||||||
<View style={styles.section}>
|
|
||||||
<Text style={styles.sectionTitle}>Delivery Details</Text>
|
|
||||||
<Text style={styles.addressType}>
|
|
||||||
{orderDetails.dropAddress?.label || 'Home'}
|
|
||||||
</Text>
|
|
||||||
<Text style={styles.addressText}>
|
|
||||||
{orderDetails.dropAddress?.houseNumber
|
|
||||||
? `${orderDetails.dropAddress.houseNumber}, `
|
|
||||||
: ''}
|
|
||||||
{orderDetails.dropAddress?.addressLine1}
|
|
||||||
</Text>
|
|
||||||
<Text style={styles.addressText}>
|
|
||||||
{orderDetails.dropAddress?.landmark
|
|
||||||
? `Landmark: ${orderDetails.dropAddress.landmark}`
|
|
||||||
: ''}
|
|
||||||
</Text>
|
|
||||||
<Text style={styles.addressText}>
|
|
||||||
Phone: {orderDetails.dropAddress?.phone}
|
|
||||||
</Text>
|
|
||||||
</View>
|
|
||||||
|
|
||||||
{/* Payment Info */}
|
|
||||||
{orderDetails.payments && orderDetails.payments.length > 0 && (
|
|
||||||
<View style={styles.section}>
|
|
||||||
<Text style={styles.sectionTitle}>Payment Details</Text>
|
|
||||||
<View style={styles.paymentRow}>
|
|
||||||
<Text style={styles.paymentIcon}>💳</Text>
|
|
||||||
<Text style={styles.paymentText}>
|
|
||||||
Paid via {orderDetails.payments[0].method} (
|
|
||||||
{orderDetails.payments[0].status})
|
|
||||||
</Text>
|
|
||||||
</View>
|
|
||||||
</View>
|
|
||||||
)}
|
|
||||||
</ScrollView>
|
|
||||||
</View>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default OrderDetailsScreen;
|
|
||||||
@ -1,108 +0,0 @@
|
|||||||
import { useState, useEffect, useCallback } from 'react';
|
|
||||||
import { Alert } from 'react-native';
|
|
||||||
import { useNavigation, useRoute, RouteProp } from '@react-navigation/native';
|
|
||||||
import { StackNavigationProp } from '@react-navigation/stack';
|
|
||||||
import { RootState, useAppDispatch, useAppSelector, verifyOtp } from '@store';
|
|
||||||
import { AuthStackParamList } from '@navigation/authStack';
|
|
||||||
|
|
||||||
type OtpNavProp = StackNavigationProp<AuthStackParamList, 'OtpScreen'>;
|
|
||||||
type OtpRouteProp = RouteProp<AuthStackParamList, 'OtpScreen'>;
|
|
||||||
|
|
||||||
const OTP_LENGTH = 6;
|
|
||||||
const RESEND_TIMER = 30;
|
|
||||||
|
|
||||||
export const useOtpScreen = () => {
|
|
||||||
const navigation = useNavigation<OtpNavProp>();
|
|
||||||
const route = useRoute<OtpRouteProp>();
|
|
||||||
const dispatch = useAppDispatch();
|
|
||||||
const { mobileNumber } = route.params;
|
|
||||||
|
|
||||||
const { loginData, user } = useAppSelector((state: RootState) => state.auth);
|
|
||||||
|
|
||||||
const [otp, setOtp] = useState<string[]>(Array(OTP_LENGTH).fill(''));
|
|
||||||
const [activeIndex, setActiveIndex] = useState(0);
|
|
||||||
const [timer, setTimer] = useState(RESEND_TIMER);
|
|
||||||
const [isLoading, setIsLoading] = useState(false);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (timer > 0) {
|
|
||||||
const interval = setInterval(() => setTimer(t => t - 1), 1000);
|
|
||||||
return () => clearInterval(interval);
|
|
||||||
}
|
|
||||||
}, [timer]);
|
|
||||||
|
|
||||||
const resetOtp = useCallback(() => {
|
|
||||||
setOtp(Array(OTP_LENGTH).fill(''));
|
|
||||||
setActiveIndex(0);
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
const handleKeyPress = useCallback(
|
|
||||||
(key: string) => {
|
|
||||||
if (key === 'backspace') {
|
|
||||||
if (otp[activeIndex] || activeIndex > 0) {
|
|
||||||
const newOtp = [...otp];
|
|
||||||
if (otp[activeIndex]) {
|
|
||||||
newOtp[activeIndex] = '';
|
|
||||||
} else if (activeIndex > 0) {
|
|
||||||
newOtp[activeIndex - 1] = '';
|
|
||||||
setActiveIndex(activeIndex - 1);
|
|
||||||
}
|
|
||||||
setOtp(newOtp);
|
|
||||||
}
|
|
||||||
} else if (key >= '0' && key <= '9' && activeIndex < OTP_LENGTH) {
|
|
||||||
const newOtp = [...otp];
|
|
||||||
newOtp[activeIndex] = key;
|
|
||||||
setOtp(newOtp);
|
|
||||||
if (activeIndex < OTP_LENGTH - 1) {
|
|
||||||
setActiveIndex(activeIndex + 1);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
[otp, activeIndex],
|
|
||||||
);
|
|
||||||
|
|
||||||
const handleResend = useCallback(() => {
|
|
||||||
setTimer(RESEND_TIMER);
|
|
||||||
resetOtp();
|
|
||||||
// TODO: dispatch resend/loginWithPhone thunk here if needed
|
|
||||||
}, [resetOtp]);
|
|
||||||
|
|
||||||
const handleVerify = useCallback(async () => {
|
|
||||||
const otpString = otp.join('');
|
|
||||||
if (otpString.length !== OTP_LENGTH) return;
|
|
||||||
|
|
||||||
setIsLoading(true);
|
|
||||||
try {
|
|
||||||
await dispatch(
|
|
||||||
verifyOtp({ phone: mobileNumber, code: otpString, role: 'CUSTOMER' }),
|
|
||||||
).unwrap();
|
|
||||||
|
|
||||||
navigation.navigate('SetLocationScreen');
|
|
||||||
} catch (error) {
|
|
||||||
const message =
|
|
||||||
typeof error === 'string' ? error : 'Invalid OTP. Please try again.';
|
|
||||||
Alert.alert('Verification Failed', message);
|
|
||||||
resetOtp();
|
|
||||||
} finally {
|
|
||||||
setIsLoading(false);
|
|
||||||
}
|
|
||||||
}, [dispatch, mobileNumber, navigation, otp, resetOtp]);
|
|
||||||
|
|
||||||
return {
|
|
||||||
// data
|
|
||||||
mobileNumber,
|
|
||||||
loginData,
|
|
||||||
user,
|
|
||||||
otp,
|
|
||||||
activeIndex,
|
|
||||||
timer,
|
|
||||||
isLoading,
|
|
||||||
otpLength: OTP_LENGTH,
|
|
||||||
isOtpComplete: otp.join('').length === OTP_LENGTH,
|
|
||||||
|
|
||||||
// actions
|
|
||||||
handleKeyPress,
|
|
||||||
handleVerify,
|
|
||||||
handleResend,
|
|
||||||
};
|
|
||||||
};
|
|
||||||
@ -1,4 +1,4 @@
|
|||||||
import React from 'react';
|
import React, { useState, useEffect } from 'react';
|
||||||
import {
|
import {
|
||||||
View,
|
View,
|
||||||
Text,
|
Text,
|
||||||
@ -6,27 +6,71 @@ import {
|
|||||||
KeyboardAvoidingView,
|
KeyboardAvoidingView,
|
||||||
Platform,
|
Platform,
|
||||||
} from 'react-native';
|
} from 'react-native';
|
||||||
|
import { useNavigation, useRoute, RouteProp } from '@react-navigation/native';
|
||||||
|
import { StackNavigationProp } from '@react-navigation/stack';
|
||||||
import { getStyles } from './otpScreen.styles';
|
import { getStyles } from './otpScreen.styles';
|
||||||
import { PrimaryButton } from '@components';
|
import { PrimaryButton } from '@components';
|
||||||
import { useAppTheme } from '@theme';
|
import { useAppTheme } from '@theme';
|
||||||
import { useOtpScreen } from './hooks/useOtpScreen';
|
import { useAppDispatch } from '../../../store';
|
||||||
|
import { verifyOtp } from '../../../store/commonreducers/auth';
|
||||||
|
import { AuthStackParamList } from '../../../navigation/authStack';
|
||||||
|
|
||||||
|
type OtpNavProp = StackNavigationProp<AuthStackParamList, 'OtpScreen'>;
|
||||||
|
type OtpRouteProp = RouteProp<AuthStackParamList, 'OtpScreen'>;
|
||||||
|
|
||||||
|
const OTP_LENGTH = 6;
|
||||||
|
const RESEND_TIMER = 30;
|
||||||
|
|
||||||
export const OtpScreen: React.FC = () => {
|
export const OtpScreen: React.FC = () => {
|
||||||
const { colors } = useAppTheme();
|
const { colors } = useAppTheme();
|
||||||
const styles = getStyles(colors);
|
const styles = getStyles(colors);
|
||||||
|
const navigation = useNavigation<OtpNavProp>();
|
||||||
|
const route = useRoute<OtpRouteProp>();
|
||||||
|
const dispatch = useAppDispatch();
|
||||||
|
const { mobileNumber } = route.params;
|
||||||
|
|
||||||
const {
|
const [otp, setOtp] = useState<string[]>(Array(OTP_LENGTH).fill(''));
|
||||||
mobileNumber,
|
const [activeIndex, setActiveIndex] = useState(0);
|
||||||
otp,
|
const [timer, setTimer] = useState(RESEND_TIMER);
|
||||||
activeIndex,
|
const [isLoading, setIsLoading] = useState(false);
|
||||||
timer,
|
|
||||||
isLoading,
|
useEffect(() => {
|
||||||
otpLength,
|
if (timer > 0) {
|
||||||
isOtpComplete,
|
const interval = setInterval(() => setTimer((t) => t - 1), 1000);
|
||||||
handleKeyPress,
|
return () => clearInterval(interval);
|
||||||
handleVerify,
|
}
|
||||||
handleResend,
|
}, [timer]);
|
||||||
} = useOtpScreen();
|
|
||||||
|
const handleKeyPress = (key: string) => {
|
||||||
|
if (key === 'backspace') {
|
||||||
|
if (otp[activeIndex] || activeIndex > 0) {
|
||||||
|
const newOtp = [...otp];
|
||||||
|
if (otp[activeIndex]) {
|
||||||
|
newOtp[activeIndex] = '';
|
||||||
|
} else if (activeIndex > 0) {
|
||||||
|
newOtp[activeIndex - 1] = '';
|
||||||
|
setActiveIndex(activeIndex - 1);
|
||||||
|
}
|
||||||
|
setOtp(newOtp);
|
||||||
|
}
|
||||||
|
} else if (key >= '0' && key <= '9' && activeIndex < OTP_LENGTH) {
|
||||||
|
const newOtp = [...otp];
|
||||||
|
newOtp[activeIndex] = key;
|
||||||
|
setOtp(newOtp);
|
||||||
|
if (activeIndex < OTP_LENGTH - 1) {
|
||||||
|
setActiveIndex(activeIndex + 1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleVerify = async () => {
|
||||||
|
const otpString = otp.join('');
|
||||||
|
if (otpString.length !== OTP_LENGTH) return;
|
||||||
|
setIsLoading(true);
|
||||||
|
await dispatch(verifyOtp({ mobileNumber, otp: otpString }));
|
||||||
|
setIsLoading(false);
|
||||||
|
navigation.navigate('SetLocationScreen');
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<KeyboardAvoidingView
|
<KeyboardAvoidingView
|
||||||
@ -35,56 +79,27 @@ export const OtpScreen: React.FC = () => {
|
|||||||
>
|
>
|
||||||
<View style={styles.content}>
|
<View style={styles.content}>
|
||||||
<Text style={styles.title}>Verify OTP</Text>
|
<Text style={styles.title}>Verify OTP</Text>
|
||||||
<Text style={styles.subtitle}>Code sent to {mobileNumber}</Text>
|
<Text style={styles.subtitle}>
|
||||||
|
Code sent to {mobileNumber}
|
||||||
|
</Text>
|
||||||
|
|
||||||
<View style={styles.otpContainer}>
|
<View style={styles.otpContainer}>
|
||||||
{otp.map(
|
{otp.map((digit, index) => (
|
||||||
(
|
<View
|
||||||
digit:
|
key={index}
|
||||||
| string
|
style={[
|
||||||
| number
|
styles.otpCell,
|
||||||
| bigint
|
index === activeIndex && styles.otpCellActive,
|
||||||
| boolean
|
digit ? styles.otpCellFilled : null,
|
||||||
| React.ReactElement<
|
]}
|
||||||
unknown,
|
>
|
||||||
string | React.JSXElementConstructor<any>
|
<Text style={styles.otpCellText}>{digit}</Text>
|
||||||
>
|
</View>
|
||||||
| Iterable<React.ReactNode>
|
))}
|
||||||
| React.ReactPortal
|
|
||||||
| Promise<
|
|
||||||
| string
|
|
||||||
| number
|
|
||||||
| bigint
|
|
||||||
| boolean
|
|
||||||
| React.ReactPortal
|
|
||||||
| React.ReactElement<
|
|
||||||
unknown,
|
|
||||||
string | React.JSXElementConstructor<any>
|
|
||||||
>
|
|
||||||
| Iterable<React.ReactNode>
|
|
||||||
| null
|
|
||||||
| undefined
|
|
||||||
>
|
|
||||||
| null
|
|
||||||
| undefined,
|
|
||||||
index: React.Key | null | undefined,
|
|
||||||
) => (
|
|
||||||
<View
|
|
||||||
key={index}
|
|
||||||
style={[
|
|
||||||
styles.otpCell,
|
|
||||||
index === activeIndex && styles.otpCellActive,
|
|
||||||
digit ? styles.otpCellFilled : null,
|
|
||||||
]}
|
|
||||||
>
|
|
||||||
<Text style={styles.otpCellText}>{digit}</Text>
|
|
||||||
</View>
|
|
||||||
),
|
|
||||||
)}
|
|
||||||
</View>
|
</View>
|
||||||
|
|
||||||
<View style={styles.keypad}>
|
<View style={styles.keypad}>
|
||||||
{[1, 2, 3, 4, 5, 6, 7, 8, 9].map(num => (
|
{[1, 2, 3, 4, 5, 6, 7, 8, 9].map((num) => (
|
||||||
<TouchableOpacity
|
<TouchableOpacity
|
||||||
key={num}
|
key={num}
|
||||||
style={styles.key}
|
style={styles.key}
|
||||||
@ -114,7 +129,14 @@ export const OtpScreen: React.FC = () => {
|
|||||||
{timer > 0 ? (
|
{timer > 0 ? (
|
||||||
<Text style={styles.timerText}>Resend code in {timer}s</Text>
|
<Text style={styles.timerText}>Resend code in {timer}s</Text>
|
||||||
) : (
|
) : (
|
||||||
<TouchableOpacity onPress={handleResend} activeOpacity={0.7}>
|
<TouchableOpacity
|
||||||
|
onPress={() => {
|
||||||
|
setTimer(RESEND_TIMER);
|
||||||
|
setOtp(Array(OTP_LENGTH).fill(''));
|
||||||
|
setActiveIndex(0);
|
||||||
|
}}
|
||||||
|
activeOpacity={0.7}
|
||||||
|
>
|
||||||
<Text style={styles.resendText}>Resend OTP</Text>
|
<Text style={styles.resendText}>Resend OTP</Text>
|
||||||
</TouchableOpacity>
|
</TouchableOpacity>
|
||||||
)}
|
)}
|
||||||
@ -123,7 +145,7 @@ export const OtpScreen: React.FC = () => {
|
|||||||
title="Verify"
|
title="Verify"
|
||||||
onPress={handleVerify}
|
onPress={handleVerify}
|
||||||
isLoading={isLoading}
|
isLoading={isLoading}
|
||||||
disabled={!isOtpComplete}
|
disabled={otp.join('').length !== OTP_LENGTH}
|
||||||
style={{ marginTop: 24 }}
|
style={{ marginTop: 24 }}
|
||||||
/>
|
/>
|
||||||
</View>
|
</View>
|
||||||
|
|||||||
@ -1,3 +1 @@
|
|||||||
export * from './preferencesScreen';
|
export * from './preferencesScreen';
|
||||||
export * from './thunk';
|
|
||||||
export * from './reducer';
|
|
||||||
|
|||||||
@ -1,12 +1,10 @@
|
|||||||
import React, { useEffect, useState } from 'react';
|
import React, { useState } from 'react';
|
||||||
import {
|
import {
|
||||||
View,
|
View,
|
||||||
Text,
|
Text,
|
||||||
TouchableOpacity,
|
TouchableOpacity,
|
||||||
Switch,
|
Switch,
|
||||||
ScrollView,
|
ScrollView,
|
||||||
ActivityIndicator,
|
|
||||||
Alert,
|
|
||||||
} from 'react-native';
|
} from 'react-native';
|
||||||
import { useNavigation } from '@react-navigation/native';
|
import { useNavigation } from '@react-navigation/native';
|
||||||
import { StackNavigationProp } from '@react-navigation/stack';
|
import { StackNavigationProp } from '@react-navigation/stack';
|
||||||
@ -14,138 +12,60 @@ import { getStyles } from './preferencesScreen.styles';
|
|||||||
import { PrimaryButton } from '@components';
|
import { PrimaryButton } from '@components';
|
||||||
import { useAppTheme } from '@theme';
|
import { useAppTheme } from '@theme';
|
||||||
import { AuthStackParamList } from '../../../navigation/authStack';
|
import { AuthStackParamList } from '../../../navigation/authStack';
|
||||||
import { useAppDispatch, useAppSelector } from '@store';
|
|
||||||
import { fetchCategories, completeOnboard } from './thunk';
|
|
||||||
import { OnboardingStackParamList } from '@navigation/onboardingStack';
|
|
||||||
|
|
||||||
type NavProp = StackNavigationProp<
|
type NavProp = StackNavigationProp<AuthStackParamList, 'PreferencesScreen'>;
|
||||||
OnboardingStackParamList,
|
|
||||||
'PreferencesScreen'
|
const CATEGORIES = [
|
||||||
>;
|
{ key: 'food', icon: '🍔', label: 'Food' },
|
||||||
|
{ key: 'groceries', icon: '🛒', label: 'Groceries' },
|
||||||
|
{ key: 'pharmacy', icon: '💊', label: 'Pharmacy' },
|
||||||
|
{ key: 'others', icon: '📦', label: 'Others' },
|
||||||
|
];
|
||||||
|
|
||||||
export const PreferencesScreen: React.FC = () => {
|
export const PreferencesScreen: React.FC = () => {
|
||||||
const { colors } = useAppTheme();
|
const { colors } = useAppTheme();
|
||||||
const styles = getStyles(colors);
|
const styles = getStyles(colors);
|
||||||
const navigation = useNavigation<NavProp>();
|
const navigation = useNavigation<NavProp>();
|
||||||
const dispatch = useAppDispatch();
|
|
||||||
|
|
||||||
// ─── Redux state ──────────────────────────────────────────────────────────
|
const [selectedCategories, setSelectedCategories] = useState<string[]>(['food']);
|
||||||
const { categories, isLoading, error, isSubmitting, submitError } =
|
|
||||||
useAppSelector(state => state.preferences);
|
|
||||||
|
|
||||||
const locationData = useAppSelector(state => state.setLocation);
|
|
||||||
const profileData = useAppSelector(state => state.completeProfile);
|
|
||||||
|
|
||||||
// ─── Local state ──────────────────────────────────────────────────────────
|
|
||||||
const [selectedCategories, setSelectedCategories] = useState<string[]>([]);
|
|
||||||
const [notificationsEnabled, setNotificationsEnabled] = useState(true);
|
const [notificationsEnabled, setNotificationsEnabled] = useState(true);
|
||||||
|
|
||||||
// ─── Fetch categories on mount ────────────────────────────────────────────
|
const toggleCategory = (key: string) => {
|
||||||
useEffect(() => {
|
setSelectedCategories((prev) =>
|
||||||
dispatch(fetchCategories());
|
prev.includes(key)
|
||||||
}, [dispatch]);
|
? prev.filter((c) => c !== key)
|
||||||
|
: [...prev, key],
|
||||||
const toggleCategory = (id: string) => {
|
|
||||||
setSelectedCategories(prev =>
|
|
||||||
prev.includes(id) ? prev.filter(c => c !== id) : [...prev, id],
|
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
// ─── Continue → call onboard API ─────────────────────────────────────────
|
|
||||||
const handleContinue = async () => {
|
|
||||||
if (!locationData.latitude || !locationData.longitude) {
|
|
||||||
Alert.alert('Location missing', 'Please set your location first.');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (!profileData.name || !profileData.email) {
|
|
||||||
Alert.alert('Profile incomplete', 'Please complete your profile first.');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const payload = {
|
|
||||||
name: profileData.name,
|
|
||||||
email: profileData.email,
|
|
||||||
gender: profileData.gender,
|
|
||||||
latitude: locationData.latitude,
|
|
||||||
longitude: locationData.longitude,
|
|
||||||
addressLabel: profileData.addressLabel,
|
|
||||||
addressLine1: profileData.addressLine1,
|
|
||||||
mapAddress: locationData.mapAddress,
|
|
||||||
houseNumber: profileData.houseNumber,
|
|
||||||
landmark: profileData.landmark,
|
|
||||||
city: profileData.city,
|
|
||||||
state: profileData.state,
|
|
||||||
postalCode: profileData.postalCode,
|
|
||||||
addressPhone: profileData.addressPhone,
|
|
||||||
categoryPreferences: selectedCategories,
|
|
||||||
};
|
|
||||||
|
|
||||||
const result = await dispatch(completeOnboard(payload));
|
|
||||||
|
|
||||||
if (completeOnboard.fulfilled.match(result)) {
|
|
||||||
navigation.navigate('OnboardingCompleteScreen');
|
|
||||||
} else {
|
|
||||||
Alert.alert(
|
|
||||||
'Onboarding failed',
|
|
||||||
(result.payload as string) ?? 'Something went wrong. Please try again.',
|
|
||||||
);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ScrollView style={styles.container} contentContainerStyle={styles.content}>
|
<ScrollView style={styles.container} contentContainerStyle={styles.content}>
|
||||||
<Text style={styles.title}>Preferences</Text>
|
<Text style={styles.title}>Preferences</Text>
|
||||||
<Text style={styles.subtitle}>Select your interests</Text>
|
<Text style={styles.subtitle}>Select your interests</Text>
|
||||||
|
|
||||||
{isLoading && (
|
<View style={styles.grid}>
|
||||||
<ActivityIndicator
|
{CATEGORIES.map((cat) => (
|
||||||
size="large"
|
<TouchableOpacity
|
||||||
color={colors.primary}
|
key={cat.key}
|
||||||
style={{ marginVertical: 32 }}
|
style={[
|
||||||
/>
|
styles.gridItem,
|
||||||
)}
|
selectedCategories.includes(cat.key) && styles.gridItemSelected,
|
||||||
|
]}
|
||||||
{!!error && (
|
onPress={() => toggleCategory(cat.key)}
|
||||||
<Text
|
activeOpacity={0.7}
|
||||||
style={{
|
>
|
||||||
color: colors.error ?? 'red',
|
<Text style={styles.gridIcon}>{cat.icon}</Text>
|
||||||
textAlign: 'center',
|
<Text
|
||||||
marginBottom: 16,
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{error}
|
|
||||||
</Text>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{!!submitError && (
|
|
||||||
<Text
|
|
||||||
style={{
|
|
||||||
color: colors.error ?? 'red',
|
|
||||||
textAlign: 'center',
|
|
||||||
marginBottom: 16,
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{submitError}
|
|
||||||
</Text>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{!isLoading && (
|
|
||||||
<View style={styles.grid}>
|
|
||||||
{(categories || []).map(cat => (
|
|
||||||
<TouchableOpacity
|
|
||||||
key={cat.id}
|
|
||||||
style={[
|
style={[
|
||||||
styles.gridItem,
|
styles.gridLabel,
|
||||||
selectedCategories.includes(cat.id) && styles.gridItemSelected,
|
selectedCategories.includes(cat.key) && styles.gridLabelSelected,
|
||||||
]}
|
]}
|
||||||
onPress={() => toggleCategory(cat.id)}
|
|
||||||
activeOpacity={0.7}
|
|
||||||
>
|
>
|
||||||
<Text style={styles.gridLabel}>{cat.name}</Text>
|
{cat.label}
|
||||||
</TouchableOpacity>
|
</Text>
|
||||||
))}
|
</TouchableOpacity>
|
||||||
</View>
|
))}
|
||||||
)}
|
</View>
|
||||||
|
|
||||||
<View style={styles.toggleRow}>
|
<View style={styles.toggleRow}>
|
||||||
<View>
|
<View>
|
||||||
@ -161,9 +81,8 @@ export const PreferencesScreen: React.FC = () => {
|
|||||||
</View>
|
</View>
|
||||||
|
|
||||||
<PrimaryButton
|
<PrimaryButton
|
||||||
title={isSubmitting ? 'Please wait…' : 'Continue'}
|
title="Continue"
|
||||||
onPress={handleContinue}
|
onPress={() => navigation.navigate('OnboardingCompleteScreen')}
|
||||||
disabled={isSubmitting}
|
|
||||||
style={{ marginTop: 32 }}
|
style={{ marginTop: 32 }}
|
||||||
/>
|
/>
|
||||||
</ScrollView>
|
</ScrollView>
|
||||||
|
|||||||
@ -1,59 +0,0 @@
|
|||||||
import { createReducer } from '@reduxjs/toolkit';
|
|
||||||
import { Category } from '@interfaces';
|
|
||||||
import { fetchCategories, completeOnboard } from './thunk';
|
|
||||||
|
|
||||||
// ─── State ────────────────────────────────────────────────────────────────────
|
|
||||||
|
|
||||||
export interface PreferencesState {
|
|
||||||
categories: Category[];
|
|
||||||
selectedCategories: string[];
|
|
||||||
isLoading: boolean;
|
|
||||||
isSubmitting: boolean;
|
|
||||||
error: string | null;
|
|
||||||
submitError: string | null;
|
|
||||||
}
|
|
||||||
|
|
||||||
const initialState: PreferencesState = {
|
|
||||||
categories: [],
|
|
||||||
selectedCategories: [],
|
|
||||||
isLoading: false,
|
|
||||||
isSubmitting: false,
|
|
||||||
error: null,
|
|
||||||
submitError: null,
|
|
||||||
};
|
|
||||||
|
|
||||||
// ─── Reducer ──────────────────────────────────────────────────────────────────
|
|
||||||
|
|
||||||
const preferencesReducer = createReducer(initialState, builder => {
|
|
||||||
builder
|
|
||||||
// Fetch categories
|
|
||||||
.addCase(fetchCategories.pending, state => {
|
|
||||||
state.isLoading = true;
|
|
||||||
state.error = null;
|
|
||||||
})
|
|
||||||
.addCase(fetchCategories.fulfilled, (state, action) => {
|
|
||||||
state.categories = action.payload;
|
|
||||||
state.isLoading = false;
|
|
||||||
state.error = null;
|
|
||||||
})
|
|
||||||
.addCase(fetchCategories.rejected, (state, action) => {
|
|
||||||
state.isLoading = false;
|
|
||||||
state.error = action.payload as string;
|
|
||||||
})
|
|
||||||
|
|
||||||
// Complete onboard
|
|
||||||
.addCase(completeOnboard.pending, state => {
|
|
||||||
state.isSubmitting = true;
|
|
||||||
state.submitError = null;
|
|
||||||
})
|
|
||||||
.addCase(completeOnboard.fulfilled, state => {
|
|
||||||
state.isSubmitting = false;
|
|
||||||
state.submitError = null;
|
|
||||||
})
|
|
||||||
.addCase(completeOnboard.rejected, (state, action) => {
|
|
||||||
state.isSubmitting = false;
|
|
||||||
state.submitError = action.payload as string;
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
export default preferencesReducer;
|
|
||||||
@ -1,33 +0,0 @@
|
|||||||
import { createAsyncThunk } from '@reduxjs/toolkit';
|
|
||||||
import { getCatagories, onBoardComplete } from '../../../api/onboardApi';
|
|
||||||
import { onBoardPayload } from '@interfaces/onboard';
|
|
||||||
|
|
||||||
// ─── Fetch Categories ─────────────────────────────────────────────────────────
|
|
||||||
|
|
||||||
export const fetchCategories = createAsyncThunk(
|
|
||||||
'preferences/fetchCategories',
|
|
||||||
async (_, { rejectWithValue }) => {
|
|
||||||
try {
|
|
||||||
const response = await getCatagories();
|
|
||||||
return response;
|
|
||||||
} catch (error: unknown) {
|
|
||||||
const message =
|
|
||||||
error instanceof Error ? error.message : 'Failed to fetch categories';
|
|
||||||
return rejectWithValue(message);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
export const completeOnboard = createAsyncThunk(
|
|
||||||
'preferences/completeOnboard',
|
|
||||||
async (payload: onBoardPayload, { rejectWithValue }) => {
|
|
||||||
try {
|
|
||||||
const response = await onBoardComplete(payload);
|
|
||||||
return response;
|
|
||||||
} catch (error: unknown) {
|
|
||||||
const message =
|
|
||||||
error instanceof Error ? error.message : 'Failed to complete onboard';
|
|
||||||
return rejectWithValue(message);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
);
|
|
||||||
@ -1,3 +1 @@
|
|||||||
export * from './providerDetailsScreen';
|
export * from './providerDetailsScreen';
|
||||||
export { default as providerDetailsReducer } from './reducer';
|
|
||||||
export * from './thunk';
|
|
||||||
|
|||||||
@ -1,8 +1,8 @@
|
|||||||
import { StyleSheet, Dimensions, Platform } from 'react-native';
|
import { StyleSheet, Dimensions, Platform } from 'react-native';
|
||||||
import { typography } from '@theme';
|
import { typography } from '@theme';
|
||||||
|
|
||||||
const { width, height } = Dimensions.get('window');
|
const { width } = Dimensions.get('window');
|
||||||
const HERO_HEIGHT = height * 0.45;
|
const HERO_HEIGHT = 220;
|
||||||
|
|
||||||
export const getStyles = (colors: any) =>
|
export const getStyles = (colors: any) =>
|
||||||
StyleSheet.create({
|
StyleSheet.create({
|
||||||
@ -14,49 +14,36 @@ export const getStyles = (colors: any) =>
|
|||||||
flex: 1,
|
flex: 1,
|
||||||
},
|
},
|
||||||
contentBody: {
|
contentBody: {
|
||||||
paddingBottom: 100, // Space for sticky bottom bar
|
paddingBottom: 32,
|
||||||
},
|
},
|
||||||
|
|
||||||
// ---------- Image Carousel ----------
|
// ---------- Hero ----------
|
||||||
carouselWrap: {
|
heroWrap: {
|
||||||
width,
|
width,
|
||||||
height: HERO_HEIGHT,
|
height: HERO_HEIGHT,
|
||||||
backgroundColor: colors.background,
|
backgroundColor: colors.inputBg,
|
||||||
},
|
},
|
||||||
carouselImage: {
|
heroImage: {
|
||||||
width,
|
width: '100%',
|
||||||
height: HERO_HEIGHT,
|
height: '100%',
|
||||||
resizeMode: 'contain',
|
|
||||||
},
|
},
|
||||||
paginationDots: {
|
heroFallback: {
|
||||||
position: 'absolute',
|
width: '100%',
|
||||||
bottom: 20,
|
height: '100%',
|
||||||
alignSelf: 'center',
|
|
||||||
flexDirection: 'row',
|
|
||||||
backgroundColor: 'rgba(255,255,255,0.7)',
|
|
||||||
paddingHorizontal: 8,
|
|
||||||
paddingVertical: 4,
|
|
||||||
borderRadius: 12,
|
|
||||||
},
|
|
||||||
dot: {
|
|
||||||
width: 6,
|
|
||||||
height: 6,
|
|
||||||
borderRadius: 3,
|
|
||||||
backgroundColor: colors.border,
|
|
||||||
marginHorizontal: 3,
|
|
||||||
},
|
|
||||||
dotActive: {
|
|
||||||
width: 14,
|
|
||||||
backgroundColor: colors.primary,
|
|
||||||
},
|
|
||||||
fallbackIconWrap: {
|
|
||||||
flex: 1,
|
|
||||||
alignItems: 'center',
|
alignItems: 'center',
|
||||||
justifyContent: 'center',
|
justifyContent: 'center',
|
||||||
backgroundColor: colors.surface,
|
backgroundColor: colors.inputBg,
|
||||||
},
|
},
|
||||||
fallbackIcon: {
|
heroFallbackEmoji: {
|
||||||
fontSize: 60,
|
fontSize: 56,
|
||||||
|
},
|
||||||
|
heroOverlay: {
|
||||||
|
position: 'absolute',
|
||||||
|
left: 0,
|
||||||
|
right: 0,
|
||||||
|
bottom: 0,
|
||||||
|
height: 90,
|
||||||
|
backgroundColor: 'rgba(0,0,0,0.28)',
|
||||||
},
|
},
|
||||||
|
|
||||||
// Floating top icon row (back / share / favorite)
|
// Floating top icon row (back / share / favorite)
|
||||||
@ -68,42 +55,40 @@ export const getStyles = (colors: any) =>
|
|||||||
flexDirection: 'row',
|
flexDirection: 'row',
|
||||||
justifyContent: 'space-between',
|
justifyContent: 'space-between',
|
||||||
alignItems: 'center',
|
alignItems: 'center',
|
||||||
zIndex: 10,
|
|
||||||
},
|
},
|
||||||
iconButton: {
|
iconButton: {
|
||||||
width: 42,
|
width: 38,
|
||||||
height: 42,
|
height: 38,
|
||||||
borderRadius: 21,
|
borderRadius: 19,
|
||||||
backgroundColor: 'rgba(255,255,255,0.95)',
|
backgroundColor: 'rgba(255,255,255,0.92)',
|
||||||
alignItems: 'center',
|
alignItems: 'center',
|
||||||
justifyContent: 'center',
|
justifyContent: 'center',
|
||||||
...Platform.select({
|
...Platform.select({
|
||||||
ios: {
|
ios: {
|
||||||
shadowColor: '#000',
|
shadowColor: '#000',
|
||||||
shadowOffset: { width: 0, height: 3 },
|
shadowOffset: { width: 0, height: 2 },
|
||||||
shadowOpacity: 0.15,
|
shadowOpacity: 0.15,
|
||||||
shadowRadius: 5,
|
shadowRadius: 4,
|
||||||
},
|
},
|
||||||
android: { elevation: 4 },
|
android: { elevation: 3 },
|
||||||
}),
|
}),
|
||||||
},
|
},
|
||||||
iconButtonText: {
|
iconButtonText: {
|
||||||
fontSize: 18,
|
fontSize: 16,
|
||||||
color: colors.text,
|
|
||||||
},
|
},
|
||||||
iconButtonGroup: {
|
iconButtonGroup: {
|
||||||
flexDirection: 'row',
|
flexDirection: 'row',
|
||||||
},
|
},
|
||||||
|
|
||||||
// ---------- Product Info Box ----------
|
// ---------- Info card ----------
|
||||||
infoBox: {
|
infoCard: {
|
||||||
backgroundColor: colors.background,
|
backgroundColor: colors.background,
|
||||||
marginTop: -20,
|
marginTop: -20,
|
||||||
borderTopLeftRadius: 24,
|
borderTopLeftRadius: 24,
|
||||||
borderTopRightRadius: 24,
|
borderTopRightRadius: 24,
|
||||||
paddingTop: 24,
|
paddingTop: 20,
|
||||||
paddingHorizontal: 20,
|
paddingHorizontal: 20,
|
||||||
paddingBottom: 20,
|
paddingBottom: 16,
|
||||||
...Platform.select({
|
...Platform.select({
|
||||||
ios: {
|
ios: {
|
||||||
shadowColor: '#000',
|
shadowColor: '#000',
|
||||||
@ -114,389 +99,247 @@ export const getStyles = (colors: any) =>
|
|||||||
android: { elevation: 4 },
|
android: { elevation: 4 },
|
||||||
}),
|
}),
|
||||||
},
|
},
|
||||||
brandRow: {
|
infoTopRow: {
|
||||||
flexDirection: 'row',
|
flexDirection: 'row',
|
||||||
justifyContent: 'space-between',
|
justifyContent: 'space-between',
|
||||||
alignItems: 'center',
|
alignItems: 'flex-start',
|
||||||
marginBottom: 6,
|
|
||||||
},
|
},
|
||||||
brandText: {
|
heroName: {
|
||||||
fontSize: typography.fontSize.xs,
|
fontSize: typography.fontSize.xl,
|
||||||
fontWeight: typography.fontWeight.bold,
|
fontWeight: typography.fontWeight.bold,
|
||||||
color: colors.textSecondary,
|
color: colors.text,
|
||||||
textTransform: 'uppercase',
|
flex: 1,
|
||||||
letterSpacing: 0.5,
|
marginRight: 12,
|
||||||
},
|
},
|
||||||
ratingBadge: {
|
ratingBadge: {
|
||||||
flexDirection: 'row',
|
flexDirection: 'row',
|
||||||
alignItems: 'center',
|
alignItems: 'center',
|
||||||
backgroundColor: colors.primaryMuted ?? '#E9F7EF',
|
backgroundColor: colors.primary,
|
||||||
borderRadius: 6,
|
borderRadius: 8,
|
||||||
paddingHorizontal: 6,
|
paddingHorizontal: 8,
|
||||||
paddingVertical: 3,
|
paddingVertical: 5,
|
||||||
},
|
},
|
||||||
ratingBadgeText: {
|
ratingBadgeText: {
|
||||||
fontSize: typography.fontSize.xs,
|
fontSize: typography.fontSize.sm,
|
||||||
fontWeight: typography.fontWeight.bold,
|
fontWeight: typography.fontWeight.bold,
|
||||||
color: colors.primary,
|
|
||||||
marginLeft: 4,
|
|
||||||
},
|
|
||||||
titleText: {
|
|
||||||
fontSize: 24,
|
|
||||||
fontWeight: typography.fontWeight.bold,
|
|
||||||
color: colors.text,
|
|
||||||
marginBottom: 12,
|
|
||||||
lineHeight: 32,
|
|
||||||
},
|
|
||||||
priceRow: {
|
|
||||||
flexDirection: 'row',
|
|
||||||
alignItems: 'flex-end',
|
|
||||||
marginBottom: 8,
|
|
||||||
},
|
|
||||||
priceText: {
|
|
||||||
fontSize: 28,
|
|
||||||
fontWeight: typography.fontWeight.bold,
|
|
||||||
color: colors.text,
|
|
||||||
marginRight: 10,
|
|
||||||
},
|
|
||||||
comparePriceText: {
|
|
||||||
fontSize: 16,
|
|
||||||
color: colors.textSecondary,
|
|
||||||
textDecorationLine: 'line-through',
|
|
||||||
marginBottom: 4,
|
|
||||||
},
|
|
||||||
discountBadgeWrap: {
|
|
||||||
marginLeft: 10,
|
|
||||||
marginBottom: 6,
|
|
||||||
backgroundColor: colors.error ?? '#E53935',
|
|
||||||
paddingHorizontal: 8,
|
|
||||||
paddingVertical: 3,
|
|
||||||
borderRadius: 6,
|
|
||||||
},
|
|
||||||
discountBadgeText: {
|
|
||||||
color: '#FFFFFF',
|
color: '#FFFFFF',
|
||||||
fontSize: typography.fontSize.xs,
|
marginLeft: 3,
|
||||||
fontWeight: typography.fontWeight.bold,
|
|
||||||
},
|
},
|
||||||
taxText: {
|
cuisineText: {
|
||||||
fontSize: typography.fontSize.sm,
|
fontSize: typography.fontSize.sm,
|
||||||
color: colors.textSecondary,
|
color: colors.textSecondary,
|
||||||
|
marginTop: 4,
|
||||||
},
|
},
|
||||||
|
|
||||||
// ---------- Section Divider ----------
|
metaRow: {
|
||||||
sectionDivider: {
|
|
||||||
height: 8,
|
|
||||||
backgroundColor: colors.surface ?? '#F5F6F8',
|
|
||||||
},
|
|
||||||
|
|
||||||
// ---------- Details Section ----------
|
|
||||||
detailsSection: {
|
|
||||||
padding: 20,
|
|
||||||
backgroundColor: colors.background,
|
|
||||||
},
|
|
||||||
sectionTitle: {
|
|
||||||
fontSize: 18,
|
|
||||||
fontWeight: typography.fontWeight.bold,
|
|
||||||
color: colors.text,
|
|
||||||
marginBottom: 12,
|
|
||||||
},
|
|
||||||
descriptionText: {
|
|
||||||
fontSize: 15,
|
|
||||||
lineHeight: 24,
|
|
||||||
color: colors.textSecondary,
|
|
||||||
marginBottom: 20,
|
|
||||||
},
|
|
||||||
metaGrid: {
|
|
||||||
flexDirection: 'row',
|
flexDirection: 'row',
|
||||||
flexWrap: 'wrap',
|
alignItems: 'center',
|
||||||
backgroundColor: colors.surface ?? '#F5F6F8',
|
marginTop: 12,
|
||||||
borderRadius: 12,
|
|
||||||
padding: 16,
|
|
||||||
},
|
},
|
||||||
metaItem: {
|
metaItem: {
|
||||||
width: '50%',
|
|
||||||
marginBottom: 12,
|
|
||||||
},
|
|
||||||
metaLabel: {
|
|
||||||
fontSize: 12,
|
|
||||||
color: colors.textSecondary,
|
|
||||||
marginBottom: 2,
|
|
||||||
},
|
|
||||||
metaValue: {
|
|
||||||
fontSize: 14,
|
|
||||||
fontWeight: typography.fontWeight.semibold,
|
|
||||||
color: colors.text,
|
|
||||||
},
|
|
||||||
|
|
||||||
// ---------- Sticky Bottom Bar ----------
|
|
||||||
bottomBarWrap: {
|
|
||||||
position: 'absolute',
|
|
||||||
left: 0,
|
|
||||||
right: 0,
|
|
||||||
bottom: 0,
|
|
||||||
backgroundColor: colors.background,
|
|
||||||
paddingHorizontal: 20,
|
|
||||||
paddingTop: 16,
|
|
||||||
paddingBottom: Platform.OS === 'ios' ? 34 : 20,
|
|
||||||
borderTopWidth: 1,
|
|
||||||
borderTopColor: colors.border ?? '#ECECEC',
|
|
||||||
flexDirection: 'row',
|
flexDirection: 'row',
|
||||||
alignItems: 'center',
|
alignItems: 'center',
|
||||||
},
|
},
|
||||||
qtySelector: {
|
metaIcon: {
|
||||||
|
fontSize: 13,
|
||||||
|
marginRight: 4,
|
||||||
|
},
|
||||||
|
metaText: {
|
||||||
|
fontSize: typography.fontSize.sm,
|
||||||
|
color: colors.text,
|
||||||
|
fontWeight: typography.fontWeight.medium,
|
||||||
|
},
|
||||||
|
metaDivider: {
|
||||||
|
width: 3,
|
||||||
|
height: 3,
|
||||||
|
borderRadius: 1.5,
|
||||||
|
backgroundColor: colors.textSecondary,
|
||||||
|
marginHorizontal: 10,
|
||||||
|
opacity: 0.5,
|
||||||
|
},
|
||||||
|
|
||||||
|
statusRow: {
|
||||||
|
flexDirection: 'row',
|
||||||
|
alignItems: 'center',
|
||||||
|
marginTop: 10,
|
||||||
|
},
|
||||||
|
statusDot: {
|
||||||
|
width: 7,
|
||||||
|
height: 7,
|
||||||
|
borderRadius: 3.5,
|
||||||
|
backgroundColor: colors.primary,
|
||||||
|
marginRight: 6,
|
||||||
|
},
|
||||||
|
statusText: {
|
||||||
|
fontSize: typography.fontSize.xs,
|
||||||
|
color: colors.primary,
|
||||||
|
fontWeight: typography.fontWeight.semibold,
|
||||||
|
},
|
||||||
|
statusTextMuted: {
|
||||||
|
fontSize: typography.fontSize.xs,
|
||||||
|
color: colors.textSecondary,
|
||||||
|
},
|
||||||
|
|
||||||
|
// ---------- Offers ----------
|
||||||
|
offersSection: {
|
||||||
|
marginTop: 18,
|
||||||
|
},
|
||||||
|
offersScrollContent: {
|
||||||
|
paddingHorizontal: 20,
|
||||||
|
},
|
||||||
|
offerChip: {
|
||||||
flexDirection: 'row',
|
flexDirection: 'row',
|
||||||
alignItems: 'center',
|
alignItems: 'center',
|
||||||
borderWidth: 1,
|
borderWidth: 1,
|
||||||
borderColor: colors.border,
|
borderStyle: 'dashed',
|
||||||
borderRadius: 12,
|
borderColor: colors.primary,
|
||||||
marginRight: 16,
|
backgroundColor: colors.primaryMuted ?? '#E9F7EF',
|
||||||
|
borderRadius: 10,
|
||||||
|
paddingVertical: 8,
|
||||||
|
paddingHorizontal: 12,
|
||||||
|
marginRight: 10,
|
||||||
},
|
},
|
||||||
qtyBtn: {
|
offerIcon: {
|
||||||
width: 44,
|
fontSize: 15,
|
||||||
height: 44,
|
marginRight: 6,
|
||||||
|
},
|
||||||
|
offerText: {
|
||||||
|
fontSize: typography.fontSize.xs,
|
||||||
|
fontWeight: typography.fontWeight.semibold,
|
||||||
|
color: colors.text,
|
||||||
|
},
|
||||||
|
|
||||||
|
// ---------- Divider ----------
|
||||||
|
sectionDivider: {
|
||||||
|
height: 8,
|
||||||
|
backgroundColor: colors.inputBg,
|
||||||
|
marginTop: 18,
|
||||||
|
},
|
||||||
|
|
||||||
|
// ---------- Menu search ----------
|
||||||
|
menuHeaderRow: {
|
||||||
|
flexDirection: 'row',
|
||||||
alignItems: 'center',
|
alignItems: 'center',
|
||||||
justifyContent: 'center',
|
justifyContent: 'space-between',
|
||||||
|
paddingHorizontal: 20,
|
||||||
|
paddingTop: 18,
|
||||||
|
paddingBottom: 4,
|
||||||
},
|
},
|
||||||
qtyBtnText: {
|
menuTitle: {
|
||||||
fontSize: 20,
|
fontSize: typography.fontSize.lg,
|
||||||
color: colors.primary,
|
|
||||||
fontWeight: typography.fontWeight.medium,
|
|
||||||
},
|
|
||||||
qtyValue: {
|
|
||||||
fontSize: 16,
|
|
||||||
fontWeight: typography.fontWeight.bold,
|
fontWeight: typography.fontWeight.bold,
|
||||||
color: colors.text,
|
color: colors.text,
|
||||||
width: 30,
|
|
||||||
textAlign: 'center',
|
|
||||||
},
|
},
|
||||||
addBtn: {
|
menuCount: {
|
||||||
flex: 1,
|
fontSize: typography.fontSize.sm,
|
||||||
backgroundColor: colors.primary,
|
color: colors.textSecondary,
|
||||||
height: 48,
|
|
||||||
borderRadius: 14,
|
|
||||||
alignItems: 'center',
|
|
||||||
justifyContent: 'center',
|
|
||||||
...Platform.select({
|
|
||||||
ios: {
|
|
||||||
shadowColor: colors.primary,
|
|
||||||
shadowOffset: { width: 0, height: 4 },
|
|
||||||
shadowOpacity: 0.3,
|
|
||||||
shadowRadius: 8,
|
|
||||||
},
|
|
||||||
android: { elevation: 6 },
|
|
||||||
}),
|
|
||||||
},
|
|
||||||
addBtnText: {
|
|
||||||
fontSize: 16,
|
|
||||||
fontWeight: typography.fontWeight.bold,
|
|
||||||
color: '#FFFFFF',
|
|
||||||
},
|
},
|
||||||
|
|
||||||
// Skeleton loaders
|
// ---------- Category tabs ----------
|
||||||
skeletonTitle: {
|
categoryRow: {
|
||||||
width: '80%',
|
paddingVertical: 14,
|
||||||
height: 32,
|
|
||||||
backgroundColor: colors.surface ?? '#EEEEEE',
|
|
||||||
borderRadius: 8,
|
|
||||||
marginBottom: 12,
|
|
||||||
},
|
|
||||||
skeletonPrice: {
|
|
||||||
width: '40%',
|
|
||||||
height: 28,
|
|
||||||
backgroundColor: colors.surface ?? '#EEEEEE',
|
|
||||||
borderRadius: 6,
|
|
||||||
marginBottom: 10,
|
|
||||||
},
|
|
||||||
skeletonText: {
|
|
||||||
width: '100%',
|
|
||||||
height: 16,
|
|
||||||
backgroundColor: colors.surface ?? '#EEEEEE',
|
|
||||||
borderRadius: 4,
|
|
||||||
marginBottom: 8,
|
|
||||||
},
|
|
||||||
|
|
||||||
// ================= Ratings & Reviews =================
|
|
||||||
ratingsSection: {
|
|
||||||
paddingHorizontal: 20,
|
paddingHorizontal: 20,
|
||||||
paddingTop: 20,
|
flexGrow: 0,
|
||||||
paddingBottom: 24,
|
},
|
||||||
|
categoryTab: {
|
||||||
|
paddingHorizontal: 18,
|
||||||
|
paddingVertical: 9,
|
||||||
|
borderRadius: 22,
|
||||||
|
borderWidth: 1.5,
|
||||||
|
borderColor: colors.border,
|
||||||
|
marginRight: 10,
|
||||||
backgroundColor: colors.background,
|
backgroundColor: colors.background,
|
||||||
},
|
},
|
||||||
ratingsHeaderRow: {
|
categoryTabActive: {
|
||||||
flexDirection: 'row',
|
borderColor: colors.primary,
|
||||||
justifyContent: 'space-between',
|
backgroundColor: colors.primary,
|
||||||
alignItems: 'center',
|
|
||||||
marginBottom: 16,
|
|
||||||
},
|
},
|
||||||
seeAllText: {
|
categoryTabText: {
|
||||||
fontSize: typography.fontSize.sm,
|
fontSize: typography.fontSize.sm,
|
||||||
fontWeight: typography.fontWeight.semibold,
|
color: colors.textSecondary,
|
||||||
color: colors.primary,
|
fontWeight: typography.fontWeight.medium,
|
||||||
|
},
|
||||||
|
categoryTabTextActive: {
|
||||||
|
color: '#FFFFFF',
|
||||||
|
fontWeight: typography.fontWeight.bold,
|
||||||
},
|
},
|
||||||
|
|
||||||
// Summary card (big number + stars + optional breakdown bars)
|
// ---------- Menu list ----------
|
||||||
ratingsSummaryCard: {
|
menuList: {
|
||||||
|
paddingHorizontal: 20,
|
||||||
|
},
|
||||||
|
emptyMenuWrap: {
|
||||||
|
alignItems: 'center',
|
||||||
|
paddingVertical: 40,
|
||||||
|
},
|
||||||
|
emptyMenuEmoji: {
|
||||||
|
fontSize: 34,
|
||||||
|
marginBottom: 8,
|
||||||
|
},
|
||||||
|
emptyMenuText: {
|
||||||
|
color: colors.textSecondary,
|
||||||
|
fontSize: typography.fontSize.sm,
|
||||||
|
},
|
||||||
|
|
||||||
|
// ---------- Cart strip ----------
|
||||||
|
cartStripWrap: {
|
||||||
|
position: 'absolute',
|
||||||
|
left: 16,
|
||||||
|
right: 16,
|
||||||
|
bottom: Platform.OS === 'ios' ? 28 : 16,
|
||||||
|
},
|
||||||
|
cartStrip: {
|
||||||
flexDirection: 'row',
|
flexDirection: 'row',
|
||||||
backgroundColor: colors.surface ?? '#F5F6F8',
|
alignItems: 'center',
|
||||||
|
justifyContent: 'space-between',
|
||||||
|
backgroundColor: colors.primary,
|
||||||
|
paddingHorizontal: 18,
|
||||||
|
paddingVertical: 14,
|
||||||
borderRadius: 16,
|
borderRadius: 16,
|
||||||
padding: 18,
|
|
||||||
marginBottom: 16,
|
|
||||||
...Platform.select({
|
...Platform.select({
|
||||||
ios: {
|
ios: {
|
||||||
shadowColor: '#000',
|
shadowColor: '#000',
|
||||||
shadowOffset: { width: 0, height: 2 },
|
shadowOffset: { width: 0, height: 6 },
|
||||||
shadowOpacity: 0.04,
|
shadowOpacity: 0.25,
|
||||||
shadowRadius: 6,
|
shadowRadius: 12,
|
||||||
},
|
},
|
||||||
android: { elevation: 1 },
|
android: { elevation: 8 },
|
||||||
}),
|
}),
|
||||||
},
|
},
|
||||||
ratingsSummaryLeft: {
|
cartStripLeft: {
|
||||||
alignItems: 'center',
|
|
||||||
justifyContent: 'center',
|
|
||||||
minWidth: 76,
|
|
||||||
},
|
|
||||||
ratingsSummaryLeftWithDivider: {
|
|
||||||
marginRight: 20,
|
|
||||||
paddingRight: 20,
|
|
||||||
borderRightWidth: StyleSheet.hairlineWidth,
|
|
||||||
borderRightColor: colors.border ?? '#E0E0E0',
|
|
||||||
},
|
|
||||||
avgRatingNumber: {
|
|
||||||
fontSize: 34,
|
|
||||||
fontWeight: typography.fontWeight.bold,
|
|
||||||
color: colors.text,
|
|
||||||
marginBottom: 4,
|
|
||||||
},
|
|
||||||
starsRow: {
|
|
||||||
flexDirection: 'row',
|
|
||||||
marginBottom: 6,
|
|
||||||
},
|
|
||||||
starIcon: {
|
|
||||||
marginRight: 1,
|
|
||||||
},
|
|
||||||
totalRatingsText: {
|
|
||||||
fontSize: typography.fontSize.xs,
|
|
||||||
color: colors.textSecondary,
|
|
||||||
},
|
|
||||||
ratingsBarsWrap: {
|
|
||||||
flex: 1,
|
|
||||||
justifyContent: 'center',
|
|
||||||
},
|
|
||||||
barRow: {
|
|
||||||
flexDirection: 'row',
|
flexDirection: 'row',
|
||||||
alignItems: 'center',
|
alignItems: 'center',
|
||||||
marginBottom: 6,
|
|
||||||
},
|
},
|
||||||
barLabel: {
|
cartBagIcon: {
|
||||||
width: 26,
|
fontSize: 18,
|
||||||
fontSize: typography.fontSize.xs,
|
|
||||||
color: colors.textSecondary,
|
|
||||||
textAlign: 'right',
|
|
||||||
marginRight: 8,
|
marginRight: 8,
|
||||||
},
|
},
|
||||||
barTrack: {
|
cartStripTextWrap: {},
|
||||||
flex: 1,
|
cartStripCount: {
|
||||||
height: 6,
|
fontSize: typography.fontSize.xs,
|
||||||
borderRadius: 3,
|
color: 'rgba(255,255,255,0.85)',
|
||||||
backgroundColor: colors.border ?? '#E5E5EA',
|
|
||||||
overflow: 'hidden',
|
|
||||||
},
|
},
|
||||||
barFill: {
|
cartStripPrice: {
|
||||||
height: '100%',
|
fontSize: typography.fontSize.md,
|
||||||
borderRadius: 3,
|
fontWeight: typography.fontWeight.bold,
|
||||||
backgroundColor: colors.warning ?? '#F5A623',
|
color: '#FFFFFF',
|
||||||
},
|
},
|
||||||
|
viewCartButton: {
|
||||||
// Empty state — styled as a card with a CTA, not bare text
|
|
||||||
emptyRatingsCard: {
|
|
||||||
backgroundColor: colors.surface ?? '#F5F6F8',
|
|
||||||
borderRadius: 16,
|
|
||||||
paddingVertical: 28,
|
|
||||||
paddingHorizontal: 20,
|
|
||||||
alignItems: 'center',
|
|
||||||
},
|
|
||||||
emptyRatingsIcon: {
|
|
||||||
fontSize: 30,
|
|
||||||
marginBottom: 10,
|
|
||||||
},
|
|
||||||
emptyRatingsTitle: {
|
|
||||||
fontSize: 15,
|
|
||||||
fontWeight: typography.fontWeight.semibold,
|
|
||||||
color: colors.text,
|
|
||||||
marginBottom: 4,
|
|
||||||
},
|
|
||||||
emptyRatingsSubtitle: {
|
|
||||||
fontSize: 13,
|
|
||||||
lineHeight: 18,
|
|
||||||
color: colors.textSecondary,
|
|
||||||
textAlign: 'center',
|
|
||||||
marginBottom: 16,
|
|
||||||
},
|
|
||||||
rateProductBtn: {
|
|
||||||
borderWidth: 1.5,
|
|
||||||
borderColor: colors.primary,
|
|
||||||
borderRadius: 10,
|
|
||||||
paddingVertical: 10,
|
|
||||||
paddingHorizontal: 24,
|
|
||||||
},
|
|
||||||
rateProductBtnText: {
|
|
||||||
fontSize: 14,
|
|
||||||
fontWeight: typography.fontWeight.semibold,
|
|
||||||
color: colors.primary,
|
|
||||||
},
|
|
||||||
|
|
||||||
// Recent reviews list
|
|
||||||
reviewsList: {
|
|
||||||
marginTop: 4,
|
|
||||||
},
|
|
||||||
reviewCard: {
|
|
||||||
backgroundColor: colors.background,
|
|
||||||
borderWidth: 1,
|
|
||||||
borderColor: colors.border ?? '#ECECEC',
|
|
||||||
borderRadius: 14,
|
|
||||||
padding: 14,
|
|
||||||
marginBottom: 12,
|
|
||||||
},
|
|
||||||
reviewCardHeader: {
|
|
||||||
flexDirection: 'row',
|
flexDirection: 'row',
|
||||||
alignItems: 'center',
|
alignItems: 'center',
|
||||||
marginBottom: 10,
|
backgroundColor: '#FFFFFF',
|
||||||
|
paddingHorizontal: 16,
|
||||||
|
paddingVertical: 9,
|
||||||
|
borderRadius: 10,
|
||||||
},
|
},
|
||||||
reviewerAvatar: {
|
viewCartText: {
|
||||||
width: 36,
|
fontSize: typography.fontSize.sm,
|
||||||
height: 36,
|
|
||||||
borderRadius: 18,
|
|
||||||
backgroundColor: colors.primaryMuted ?? '#E9F7EF',
|
|
||||||
alignItems: 'center',
|
|
||||||
justifyContent: 'center',
|
|
||||||
marginRight: 10,
|
|
||||||
},
|
|
||||||
reviewerAvatarText: {
|
|
||||||
color: colors.primary,
|
|
||||||
fontSize: 15,
|
|
||||||
fontWeight: typography.fontWeight.bold,
|
fontWeight: typography.fontWeight.bold,
|
||||||
|
color: colors.primary,
|
||||||
|
marginRight: 4,
|
||||||
},
|
},
|
||||||
reviewerName: {
|
viewCartArrow: {
|
||||||
fontSize: 13,
|
fontSize: 13,
|
||||||
fontWeight: typography.fontWeight.semibold,
|
color: colors.primary,
|
||||||
color: colors.text,
|
|
||||||
marginBottom: 3,
|
|
||||||
},
|
|
||||||
reviewDate: {
|
|
||||||
fontSize: 11,
|
|
||||||
color: colors.textSecondary,
|
|
||||||
},
|
|
||||||
reviewComment: {
|
|
||||||
fontSize: 13,
|
|
||||||
lineHeight: 19,
|
|
||||||
color: colors.textSecondary,
|
|
||||||
},
|
|
||||||
|
|
||||||
skeletonBlock: {
|
|
||||||
height: 90,
|
|
||||||
borderRadius: 16,
|
|
||||||
backgroundColor: colors.surface ?? '#EEEEEE',
|
|
||||||
marginBottom: 12,
|
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|||||||
@ -1,27 +1,22 @@
|
|||||||
import React, { useEffect, useState, useRef } from 'react';
|
import React, { useEffect, useMemo, useState } from 'react';
|
||||||
import {
|
import {
|
||||||
View,
|
View,
|
||||||
Text,
|
Text,
|
||||||
TouchableOpacity,
|
TouchableOpacity,
|
||||||
ScrollView,
|
ScrollView,
|
||||||
Image,
|
Image,
|
||||||
Dimensions,
|
ImageBackground,
|
||||||
NativeSyntheticEvent,
|
|
||||||
NativeScrollEvent,
|
|
||||||
} from 'react-native';
|
} from 'react-native';
|
||||||
import { useNavigation, useRoute, RouteProp } from '@react-navigation/native';
|
import { useNavigation, useRoute, RouteProp } from '@react-navigation/native';
|
||||||
import { StackNavigationProp } from '@react-navigation/stack';
|
import { StackNavigationProp } from '@react-navigation/stack';
|
||||||
import { getStyles } from './providerDetailsScreen.styles';
|
import { getStyles } from './providerDetailsScreen.styles';
|
||||||
|
import { CatalogItemRow } from '@components';
|
||||||
import { useAppTheme } from '@theme';
|
import { useAppTheme } from '@theme';
|
||||||
import {
|
import { useAppDispatch, useAppSelector } from '../../../store';
|
||||||
addToCartThunk,
|
import { addItem } from '../../../store/commonreducers/cart';
|
||||||
updateCartItemThunk,
|
import { getProviderCatalogApi, getProvidersApi } from '../../../api/deliveryApi';
|
||||||
} from '../../../store/commonreducers/cart';
|
import { CatalogItem, Provider } from '../../../interfaces';
|
||||||
import { AppStackParamList } from '../../../navigation/appStack';
|
import { AppStackParamList } from '../../../navigation/appStack';
|
||||||
import { useAppDispatch, useAppSelector } from '@store';
|
|
||||||
import { getProductDetailsThunk } from './thunk';
|
|
||||||
import { getDiscountPercentage, formatPrice } from '@components';
|
|
||||||
import { getFullUrl } from '@utils';
|
|
||||||
|
|
||||||
type ProviderDetailsNavProp = StackNavigationProp<
|
type ProviderDetailsNavProp = StackNavigationProp<
|
||||||
AppStackParamList,
|
AppStackParamList,
|
||||||
@ -32,19 +27,215 @@ type ProviderDetailsRouteProp = RouteProp<
|
|||||||
'ProviderDetailsScreen'
|
'ProviderDetailsScreen'
|
||||||
>;
|
>;
|
||||||
|
|
||||||
const { width } = Dimensions.get('window');
|
const FALLBACK_CATEGORIES = ['Pizza', 'Sides', 'Beverages'];
|
||||||
|
|
||||||
// TODO: move these into your shared Product type once the backend
|
const OFFERS = [
|
||||||
// response includes them.
|
{ key: 'o1', icon: '🏷️', label: '50% OFF up to ₹100' },
|
||||||
interface ProductReview {
|
{ key: 'o2', icon: '🚚', label: 'Free delivery above ₹299' },
|
||||||
id: string;
|
{ key: 'o3', icon: '🎁', label: 'Buy 1 Get 1 on combos' },
|
||||||
userName?: string;
|
];
|
||||||
rating: number;
|
|
||||||
comment?: string;
|
// ---------------------------------------------------------------------------
|
||||||
createdAt?: string;
|
// Presentational subcomponents
|
||||||
|
// Kept local to this screen since none are reused elsewhere yet. Promote to
|
||||||
|
// @components if a second screen needs them.
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
interface HeroSectionProps {
|
||||||
|
styles: ReturnType<typeof getStyles>;
|
||||||
|
imageUrl?: string;
|
||||||
|
onBack: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
type RatingBreakdown = Partial<Record<1 | 2 | 3 | 4 | 5, number>>;
|
const HeroSection: React.FC<HeroSectionProps> = ({
|
||||||
|
styles,
|
||||||
|
imageUrl,
|
||||||
|
onBack,
|
||||||
|
}) => (
|
||||||
|
<View style={styles.heroWrap}>
|
||||||
|
{imageUrl ? (
|
||||||
|
<ImageBackground source={{ uri: imageUrl }} style={styles.heroImage}>
|
||||||
|
<View style={styles.heroOverlay} />
|
||||||
|
</ImageBackground>
|
||||||
|
) : (
|
||||||
|
<View style={styles.heroFallback}>
|
||||||
|
<Text style={styles.heroFallbackEmoji}>🍽️</Text>
|
||||||
|
<View style={styles.heroOverlay} />
|
||||||
|
</View>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<View style={styles.topIconRow}>
|
||||||
|
<TouchableOpacity
|
||||||
|
style={styles.iconButton}
|
||||||
|
activeOpacity={0.75}
|
||||||
|
onPress={onBack}
|
||||||
|
>
|
||||||
|
<Text style={styles.iconButtonText}>←</Text>
|
||||||
|
</TouchableOpacity>
|
||||||
|
<View style={styles.iconButtonGroup}>
|
||||||
|
<TouchableOpacity
|
||||||
|
style={[styles.iconButton, { marginRight: 10 }]}
|
||||||
|
activeOpacity={0.75}
|
||||||
|
>
|
||||||
|
<Text style={styles.iconButtonText}>🔗</Text>
|
||||||
|
</TouchableOpacity>
|
||||||
|
<TouchableOpacity style={styles.iconButton} activeOpacity={0.75}>
|
||||||
|
<Text style={styles.iconButtonText}>🤍</Text>
|
||||||
|
</TouchableOpacity>
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
);
|
||||||
|
|
||||||
|
interface InfoCardProps {
|
||||||
|
styles: ReturnType<typeof getStyles>;
|
||||||
|
name: string;
|
||||||
|
rating: number;
|
||||||
|
deliveryTime: string;
|
||||||
|
tag: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const InfoCard: React.FC<InfoCardProps> = ({
|
||||||
|
styles,
|
||||||
|
name,
|
||||||
|
rating,
|
||||||
|
deliveryTime,
|
||||||
|
tag,
|
||||||
|
}) => (
|
||||||
|
<View style={styles.infoCard}>
|
||||||
|
<View style={styles.infoTopRow}>
|
||||||
|
<Text style={styles.heroName} numberOfLines={1}>
|
||||||
|
{name}
|
||||||
|
</Text>
|
||||||
|
<View style={styles.ratingBadge}>
|
||||||
|
<Text style={{ fontSize: 11 }}>⭐</Text>
|
||||||
|
<Text style={styles.ratingBadgeText}>{rating.toFixed(1)}</Text>
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
|
||||||
|
<Text style={styles.cuisineText}>{tag} • Multi-cuisine</Text>
|
||||||
|
|
||||||
|
<View style={styles.metaRow}>
|
||||||
|
<View style={styles.metaItem}>
|
||||||
|
<Text style={styles.metaIcon}>🕐</Text>
|
||||||
|
<Text style={styles.metaText}>{deliveryTime}</Text>
|
||||||
|
</View>
|
||||||
|
<View style={styles.metaDivider} />
|
||||||
|
<View style={styles.metaItem}>
|
||||||
|
<Text style={styles.metaIcon}>📍</Text>
|
||||||
|
<Text style={styles.metaText}>2.4 km away</Text>
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
|
||||||
|
<View style={styles.statusRow}>
|
||||||
|
<View style={styles.statusDot} />
|
||||||
|
<Text style={styles.statusText}>Open now</Text>
|
||||||
|
<Text style={styles.statusTextMuted}> • Closes 11:30 PM</Text>
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
);
|
||||||
|
|
||||||
|
interface OffersRowProps {
|
||||||
|
styles: ReturnType<typeof getStyles>;
|
||||||
|
}
|
||||||
|
|
||||||
|
const OffersRow: React.FC<OffersRowProps> = ({ styles }) => (
|
||||||
|
<View style={styles.offersSection}>
|
||||||
|
<ScrollView
|
||||||
|
horizontal
|
||||||
|
showsHorizontalScrollIndicator={false}
|
||||||
|
contentContainerStyle={styles.offersScrollContent}
|
||||||
|
>
|
||||||
|
{OFFERS.map(offer => (
|
||||||
|
<View key={offer.key} style={styles.offerChip}>
|
||||||
|
<Text style={styles.offerIcon}>{offer.icon}</Text>
|
||||||
|
<Text style={styles.offerText}>{offer.label}</Text>
|
||||||
|
</View>
|
||||||
|
))}
|
||||||
|
</ScrollView>
|
||||||
|
</View>
|
||||||
|
);
|
||||||
|
|
||||||
|
interface CategoryTabsProps {
|
||||||
|
styles: ReturnType<typeof getStyles>;
|
||||||
|
categories: string[];
|
||||||
|
activeCategory: string;
|
||||||
|
onSelect: (category: string) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
const CategoryTabs: React.FC<CategoryTabsProps> = ({
|
||||||
|
styles,
|
||||||
|
categories,
|
||||||
|
activeCategory,
|
||||||
|
onSelect,
|
||||||
|
}) => (
|
||||||
|
<ScrollView
|
||||||
|
horizontal
|
||||||
|
showsHorizontalScrollIndicator={false}
|
||||||
|
contentContainerStyle={styles.categoryRow}
|
||||||
|
>
|
||||||
|
{categories.map(cat => {
|
||||||
|
const isActive = activeCategory === cat;
|
||||||
|
return (
|
||||||
|
<TouchableOpacity
|
||||||
|
key={cat}
|
||||||
|
style={[styles.categoryTab, isActive && styles.categoryTabActive]}
|
||||||
|
onPress={() => onSelect(cat)}
|
||||||
|
activeOpacity={0.75}
|
||||||
|
>
|
||||||
|
<Text
|
||||||
|
style={[
|
||||||
|
styles.categoryTabText,
|
||||||
|
isActive && styles.categoryTabTextActive,
|
||||||
|
]}
|
||||||
|
>
|
||||||
|
{cat}
|
||||||
|
</Text>
|
||||||
|
</TouchableOpacity>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</ScrollView>
|
||||||
|
);
|
||||||
|
|
||||||
|
interface CartStripProps {
|
||||||
|
styles: ReturnType<typeof getStyles>;
|
||||||
|
totalItems: number;
|
||||||
|
totalPrice: number;
|
||||||
|
onPress: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
const CartStrip: React.FC<CartStripProps> = ({
|
||||||
|
styles,
|
||||||
|
totalItems,
|
||||||
|
totalPrice,
|
||||||
|
onPress,
|
||||||
|
}) => (
|
||||||
|
<View style={styles.cartStripWrap}>
|
||||||
|
<TouchableOpacity
|
||||||
|
style={styles.cartStrip}
|
||||||
|
activeOpacity={0.85}
|
||||||
|
onPress={onPress}
|
||||||
|
>
|
||||||
|
<View style={styles.cartStripLeft}>
|
||||||
|
<Text style={styles.cartBagIcon}>🛍️</Text>
|
||||||
|
<View style={styles.cartStripTextWrap}>
|
||||||
|
<Text style={styles.cartStripCount}>
|
||||||
|
{totalItems} item{totalItems === 1 ? '' : 's'}
|
||||||
|
</Text>
|
||||||
|
<Text style={styles.cartStripPrice}>₹{totalPrice}</Text>
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
<View style={styles.viewCartButton}>
|
||||||
|
<Text style={styles.viewCartText}>View Cart</Text>
|
||||||
|
<Text style={styles.viewCartArrow}>→</Text>
|
||||||
|
</View>
|
||||||
|
</TouchableOpacity>
|
||||||
|
</View>
|
||||||
|
);
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Screen
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
export const ProviderDetailsScreen: React.FC = () => {
|
export const ProviderDetailsScreen: React.FC = () => {
|
||||||
const { colors } = useAppTheme();
|
const { colors } = useAppTheme();
|
||||||
@ -52,314 +243,65 @@ export const ProviderDetailsScreen: React.FC = () => {
|
|||||||
const dispatch = useAppDispatch();
|
const dispatch = useAppDispatch();
|
||||||
const navigation = useNavigation<ProviderDetailsNavProp>();
|
const navigation = useNavigation<ProviderDetailsNavProp>();
|
||||||
const route = useRoute<ProviderDetailsRouteProp>();
|
const route = useRoute<ProviderDetailsRouteProp>();
|
||||||
|
const { providerId, providerName } = route.params;
|
||||||
// Notice we still use providerId param name to avoid breaking routing everywhere
|
|
||||||
const { providerId } = route.params;
|
|
||||||
|
|
||||||
const cartItems = useAppSelector(state => state.cart.items);
|
const cartItems = useAppSelector(state => state.cart.items);
|
||||||
const { product, isLoading } = useAppSelector(state => state.providerDetails);
|
|
||||||
|
|
||||||
// Find this product in the cart (if already added)
|
const [provider, setProvider] = useState<Provider | null>(null);
|
||||||
const cartItem = product
|
const [catalog, setCatalog] = useState<CatalogItem[]>([]);
|
||||||
? cartItems.find(i => i.productId === product.id)
|
const [activeCategory, setActiveCategory] = useState(FALLBACK_CATEGORIES[0]);
|
||||||
: undefined;
|
|
||||||
const isInCart = !!cartItem;
|
|
||||||
|
|
||||||
const [activeImageIndex, setActiveImageIndex] = useState(0);
|
const resolvedProviderId = providerId || 'p1';
|
||||||
const [localQty, setLocalQty] = useState(0);
|
const resolvedProviderName = providerName || 'Pizza Planet';
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (providerId) {
|
getProviderCatalogApi(resolvedProviderId).then(setCatalog);
|
||||||
dispatch(getProductDetailsThunk(providerId));
|
}, [resolvedProviderId]);
|
||||||
}
|
|
||||||
}, [providerId, dispatch]);
|
|
||||||
|
|
||||||
// Sync localQty with cart quantity when cart loads or product changes
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (cartItem) {
|
getProvidersApi().then(providers => {
|
||||||
setLocalQty(cartItem.quantity);
|
const match = providers.find(p => p.id === resolvedProviderId);
|
||||||
} else {
|
if (match) setProvider(match);
|
||||||
setLocalQty(0);
|
});
|
||||||
}
|
}, [resolvedProviderId]);
|
||||||
}, [cartItem?.quantity, cartItem?.productId]);
|
|
||||||
|
|
||||||
const handleScroll = (e: NativeSyntheticEvent<NativeScrollEvent>) => {
|
const categories = useMemo(() => {
|
||||||
const slide = Math.round(e.nativeEvent.contentOffset.x / width);
|
const fromCatalog = Array.from(new Set(catalog.map(item => item.category)));
|
||||||
setActiveImageIndex(slide);
|
return fromCatalog.length > 0 ? fromCatalog : FALLBACK_CATEGORIES;
|
||||||
|
}, [catalog]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!categories.includes(activeCategory)) {
|
||||||
|
setActiveCategory(categories[0]);
|
||||||
|
}
|
||||||
|
}, [categories, activeCategory]);
|
||||||
|
|
||||||
|
const filteredItems = useMemo(
|
||||||
|
() => catalog.filter(item => item.category === activeCategory),
|
||||||
|
[catalog, activeCategory],
|
||||||
|
);
|
||||||
|
|
||||||
|
const getItemQuantity = (itemId: string) => {
|
||||||
|
const match = cartItems.find(i => i.item.id === itemId);
|
||||||
|
return match ? match.quantity : 0;
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleAddToCart = () => {
|
const totalItems = cartItems.reduce((sum, i) => sum + i.quantity, 0);
|
||||||
if (!product) return;
|
const totalPrice = cartItems.reduce(
|
||||||
|
(sum, i) => sum + i.item.price * i.quantity,
|
||||||
|
0,
|
||||||
|
);
|
||||||
|
|
||||||
|
const handleAddItem = (item: CatalogItem) => {
|
||||||
dispatch(
|
dispatch(
|
||||||
addToCartThunk({
|
addItem({
|
||||||
productId: product.id,
|
providerId: resolvedProviderId,
|
||||||
quantity: Math.max(1, localQty), // always add at least 1
|
providerName: resolvedProviderName,
|
||||||
|
item,
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleIncrement = () => {
|
|
||||||
if (!product) return;
|
|
||||||
const newQty = localQty + 1;
|
|
||||||
setLocalQty(newQty);
|
|
||||||
dispatch(updateCartItemThunk({ productId: product.id, quantity: newQty }));
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleDecrement = () => {
|
|
||||||
if (!product || localQty <= 1) return;
|
|
||||||
const newQty = localQty - 1;
|
|
||||||
setLocalQty(newQty);
|
|
||||||
dispatch(updateCartItemThunk({ productId: product.id, quantity: newQty }));
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleRateProduct = () => {
|
|
||||||
navigation.navigate('WriteReviewScreen', { productId: product?.id ?? '' });
|
|
||||||
};
|
|
||||||
|
|
||||||
const renderStars = (value: number, size = 14) => {
|
|
||||||
const rounded = Math.round(value);
|
|
||||||
const starColor = (colors as any).warning ?? '#F5A623';
|
|
||||||
return (
|
|
||||||
<View style={styles.starsRow}>
|
|
||||||
{[1, 2, 3, 4, 5].map(i => (
|
|
||||||
<Text
|
|
||||||
key={i}
|
|
||||||
style={[styles.starIcon, { fontSize: size, color: starColor }]}
|
|
||||||
>
|
|
||||||
{i <= rounded ? '★' : '☆'}
|
|
||||||
</Text>
|
|
||||||
))}
|
|
||||||
</View>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
const renderImages = () => {
|
|
||||||
if (isLoading) {
|
|
||||||
return (
|
|
||||||
<View style={styles.carouselWrap}>
|
|
||||||
<View
|
|
||||||
style={[
|
|
||||||
styles.fallbackIconWrap,
|
|
||||||
{ backgroundColor: (colors as any).surface ?? colors.cardBg },
|
|
||||||
]}
|
|
||||||
/>
|
|
||||||
</View>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
const images = product?.media?.length
|
|
||||||
? product.media
|
|
||||||
: [
|
|
||||||
{
|
|
||||||
id: 'fallback',
|
|
||||||
url: product?.imageUrl || '',
|
|
||||||
mediaType: 'IMAGE',
|
|
||||||
sortOrder: 0,
|
|
||||||
productId: '',
|
|
||||||
createdAt: '',
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
return (
|
|
||||||
<View style={styles.carouselWrap}>
|
|
||||||
<ScrollView
|
|
||||||
horizontal
|
|
||||||
pagingEnabled
|
|
||||||
showsHorizontalScrollIndicator={false}
|
|
||||||
onMomentumScrollEnd={handleScroll}
|
|
||||||
>
|
|
||||||
{images.map(img => (
|
|
||||||
<Image
|
|
||||||
key={img.id}
|
|
||||||
source={{ uri: getFullUrl(img.url) }}
|
|
||||||
style={styles.carouselImage}
|
|
||||||
/>
|
|
||||||
))}
|
|
||||||
</ScrollView>
|
|
||||||
{images.length > 1 && (
|
|
||||||
<View style={styles.paginationDots}>
|
|
||||||
{images.map((_, i) => (
|
|
||||||
<View
|
|
||||||
key={i}
|
|
||||||
style={[styles.dot, i === activeImageIndex && styles.dotActive]}
|
|
||||||
/>
|
|
||||||
))}
|
|
||||||
</View>
|
|
||||||
)}
|
|
||||||
</View>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
const renderProductInfo = () => {
|
|
||||||
if (isLoading || !product) {
|
|
||||||
return (
|
|
||||||
<View style={styles.infoBox}>
|
|
||||||
<View style={styles.skeletonTitle} />
|
|
||||||
<View style={styles.skeletonPrice} />
|
|
||||||
<View style={styles.skeletonText} />
|
|
||||||
<View style={styles.skeletonText} />
|
|
||||||
</View>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
const discount = getDiscountPercentage(
|
|
||||||
product.price,
|
|
||||||
product.compareAtPrice,
|
|
||||||
);
|
|
||||||
const currency = product.currency === 'INR' ? '₹' : product.currency;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<View style={styles.infoBox}>
|
|
||||||
<View style={styles.brandRow}>
|
|
||||||
<Text style={styles.brandText}>
|
|
||||||
{product.brand || product.merchant?.name || 'BRAND'}
|
|
||||||
</Text>
|
|
||||||
<View style={styles.ratingBadge}>
|
|
||||||
<Text style={{ fontSize: 11 }}>⭐</Text>
|
|
||||||
<Text style={styles.ratingBadgeText}>
|
|
||||||
{parseFloat(product.avgRating).toFixed(1)}
|
|
||||||
</Text>
|
|
||||||
</View>
|
|
||||||
</View>
|
|
||||||
|
|
||||||
<Text style={styles.titleText}>{product.name}</Text>
|
|
||||||
|
|
||||||
<View style={styles.priceRow}>
|
|
||||||
<Text style={styles.priceText}>
|
|
||||||
{formatPrice(product.price, currency)}
|
|
||||||
</Text>
|
|
||||||
{product.compareAtPrice &&
|
|
||||||
parseFloat(product.compareAtPrice) > parseFloat(product.price) && (
|
|
||||||
<Text style={styles.comparePriceText}>
|
|
||||||
{formatPrice(product.compareAtPrice, currency)}
|
|
||||||
</Text>
|
|
||||||
)}
|
|
||||||
{discount && (
|
|
||||||
<View style={styles.discountBadgeWrap}>
|
|
||||||
<Text style={styles.discountBadgeText}>{discount}</Text>
|
|
||||||
</View>
|
|
||||||
)}
|
|
||||||
</View>
|
|
||||||
<Text style={styles.taxText}>Inclusive of all taxes</Text>
|
|
||||||
</View>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
const renderRatingsSection = () => {
|
|
||||||
if (isLoading) {
|
|
||||||
return (
|
|
||||||
<View style={styles.ratingsSection}>
|
|
||||||
<View style={styles.skeletonBlock} />
|
|
||||||
<View style={[styles.skeletonBlock, { height: 80 }]} />
|
|
||||||
</View>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
const avgRating = product?.avgRating ? parseFloat(product.avgRating) : 0;
|
|
||||||
const reviews: ProductReview[] = (product as any)?.reviews ?? [];
|
|
||||||
const totalRatings: number =
|
|
||||||
(product as any)?.ratingsCount ?? reviews.length ?? 0;
|
|
||||||
const breakdown: RatingBreakdown | undefined = (product as any)
|
|
||||||
?.ratingBreakdown;
|
|
||||||
const hasRatings = totalRatings > 0;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<View style={styles.ratingsSection}>
|
|
||||||
<View style={styles.ratingsHeaderRow}>
|
|
||||||
<Text style={styles.sectionTitle}>Ratings & Reviews</Text>
|
|
||||||
{reviews.length > 3 && (
|
|
||||||
<TouchableOpacity activeOpacity={0.7}>
|
|
||||||
<Text style={styles.seeAllText}>See all</Text>
|
|
||||||
</TouchableOpacity>
|
|
||||||
)}
|
|
||||||
</View>
|
|
||||||
|
|
||||||
{hasRatings ? (
|
|
||||||
<View style={styles.ratingsSummaryCard}>
|
|
||||||
<View
|
|
||||||
style={[
|
|
||||||
styles.ratingsSummaryLeft,
|
|
||||||
breakdown && styles.ratingsSummaryLeftWithDivider,
|
|
||||||
]}
|
|
||||||
>
|
|
||||||
<Text style={styles.avgRatingNumber}>{avgRating.toFixed(1)}</Text>
|
|
||||||
{renderStars(avgRating, 16)}
|
|
||||||
<Text style={styles.totalRatingsText}>
|
|
||||||
{totalRatings} {totalRatings === 1 ? 'rating' : 'ratings'}
|
|
||||||
</Text>
|
|
||||||
</View>
|
|
||||||
|
|
||||||
{breakdown && (
|
|
||||||
<View style={styles.ratingsBarsWrap}>
|
|
||||||
{[5, 4, 3, 2, 1].map(star => {
|
|
||||||
const count = breakdown[star as 1 | 2 | 3 | 4 | 5] ?? 0;
|
|
||||||
const pct =
|
|
||||||
totalRatings > 0 ? (count / totalRatings) * 100 : 0;
|
|
||||||
return (
|
|
||||||
<View key={star} style={styles.barRow}>
|
|
||||||
<Text style={styles.barLabel}>{star}★</Text>
|
|
||||||
<View style={styles.barTrack}>
|
|
||||||
<View style={[styles.barFill, { width: `${pct}%` }]} />
|
|
||||||
</View>
|
|
||||||
</View>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
</View>
|
|
||||||
)}
|
|
||||||
</View>
|
|
||||||
) : (
|
|
||||||
<View style={styles.emptyRatingsCard}>
|
|
||||||
<Text style={styles.emptyRatingsIcon}>⭐</Text>
|
|
||||||
<Text style={styles.emptyRatingsTitle}>No ratings yet</Text>
|
|
||||||
<Text style={styles.emptyRatingsSubtitle}>
|
|
||||||
Be the first to share what you think of this product.
|
|
||||||
</Text>
|
|
||||||
<TouchableOpacity
|
|
||||||
style={styles.rateProductBtn}
|
|
||||||
activeOpacity={0.8}
|
|
||||||
onPress={handleRateProduct}
|
|
||||||
>
|
|
||||||
<Text style={styles.rateProductBtnText}>Rate this product</Text>
|
|
||||||
</TouchableOpacity>
|
|
||||||
</View>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{reviews.length > 0 && (
|
|
||||||
<View style={styles.reviewsList}>
|
|
||||||
{reviews.slice(0, 3).map(review => (
|
|
||||||
<View key={review.id} style={styles.reviewCard}>
|
|
||||||
<View style={styles.reviewCardHeader}>
|
|
||||||
<View style={styles.reviewerAvatar}>
|
|
||||||
<Text style={styles.reviewerAvatarText}>
|
|
||||||
{(review.userName || 'U').charAt(0).toUpperCase()}
|
|
||||||
</Text>
|
|
||||||
</View>
|
|
||||||
<View style={{ flex: 1 }}>
|
|
||||||
<Text style={styles.reviewerName}>
|
|
||||||
{review.userName || 'Anonymous'}
|
|
||||||
</Text>
|
|
||||||
{renderStars(review.rating, 12)}
|
|
||||||
</View>
|
|
||||||
<Text style={styles.reviewDate}>
|
|
||||||
{review.createdAt
|
|
||||||
? new Date(review.createdAt).toLocaleDateString('en-IN', {
|
|
||||||
day: 'numeric',
|
|
||||||
month: 'short',
|
|
||||||
})
|
|
||||||
: ''}
|
|
||||||
</Text>
|
|
||||||
</View>
|
|
||||||
{!!review.comment && (
|
|
||||||
<Text style={styles.reviewComment}>{review.comment}</Text>
|
|
||||||
)}
|
|
||||||
</View>
|
|
||||||
))}
|
|
||||||
</View>
|
|
||||||
)}
|
|
||||||
</View>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<View style={styles.container}>
|
<View style={styles.container}>
|
||||||
<ScrollView
|
<ScrollView
|
||||||
@ -367,147 +309,69 @@ export const ProviderDetailsScreen: React.FC = () => {
|
|||||||
contentContainerStyle={styles.contentBody}
|
contentContainerStyle={styles.contentBody}
|
||||||
showsVerticalScrollIndicator={false}
|
showsVerticalScrollIndicator={false}
|
||||||
>
|
>
|
||||||
<View style={{ position: 'relative' }}>
|
<HeroSection
|
||||||
{renderImages()}
|
styles={styles}
|
||||||
|
imageUrl={provider?.imageUrl}
|
||||||
|
onBack={() => navigation.goBack()}
|
||||||
|
/>
|
||||||
|
|
||||||
<View style={styles.topIconRow}>
|
<InfoCard
|
||||||
<TouchableOpacity
|
styles={styles}
|
||||||
style={styles.iconButton}
|
name={resolvedProviderName}
|
||||||
activeOpacity={0.8}
|
rating={provider?.rating ?? 4.5}
|
||||||
onPress={() => navigation.goBack()}
|
deliveryTime={provider?.deliveryTime ?? '25 min'}
|
||||||
>
|
tag={provider?.tag ?? 'Italian'}
|
||||||
<Text style={styles.iconButtonText}>←</Text>
|
/>
|
||||||
</TouchableOpacity>
|
|
||||||
<View style={styles.iconButtonGroup}>
|
|
||||||
<TouchableOpacity
|
|
||||||
style={[styles.iconButton, { marginRight: 12 }]}
|
|
||||||
activeOpacity={0.8}
|
|
||||||
>
|
|
||||||
<Text style={styles.iconButtonText}>🔗</Text>
|
|
||||||
</TouchableOpacity>
|
|
||||||
<TouchableOpacity style={styles.iconButton} activeOpacity={0.8}>
|
|
||||||
<Text style={{ fontSize: 17 }}>🤍</Text>
|
|
||||||
</TouchableOpacity>
|
|
||||||
</View>
|
|
||||||
</View>
|
|
||||||
</View>
|
|
||||||
|
|
||||||
{renderProductInfo()}
|
<OffersRow styles={styles} />
|
||||||
|
|
||||||
<View style={styles.sectionDivider} />
|
<View style={styles.sectionDivider} />
|
||||||
|
|
||||||
<View style={styles.detailsSection}>
|
<View style={styles.menuHeaderRow}>
|
||||||
<Text style={styles.sectionTitle}>Product Details</Text>
|
<Text style={styles.menuTitle}>Menu</Text>
|
||||||
<Text style={styles.descriptionText}>
|
<Text style={styles.menuCount}>{catalog.length} items</Text>
|
||||||
{product?.description ||
|
|
||||||
'No description available for this product.'}
|
|
||||||
</Text>
|
|
||||||
|
|
||||||
<View style={styles.metaGrid}>
|
|
||||||
<View style={styles.metaItem}>
|
|
||||||
<Text style={styles.metaLabel}>Category</Text>
|
|
||||||
<Text style={styles.metaValue}>
|
|
||||||
{product?.category?.name || 'N/A'}
|
|
||||||
</Text>
|
|
||||||
</View>
|
|
||||||
<View style={styles.metaItem}>
|
|
||||||
<Text style={styles.metaLabel}>SKU</Text>
|
|
||||||
<Text style={styles.metaValue}>{product?.sku || 'N/A'}</Text>
|
|
||||||
</View>
|
|
||||||
<View style={styles.metaItem}>
|
|
||||||
<Text style={styles.metaLabel}>Stock</Text>
|
|
||||||
<Text style={styles.metaValue}>
|
|
||||||
{product?.stockQuantity ? 'In Stock' : 'Out of Stock'}
|
|
||||||
</Text>
|
|
||||||
</View>
|
|
||||||
<View style={styles.metaItem}>
|
|
||||||
<Text style={styles.metaLabel}>Merchant</Text>
|
|
||||||
<Text style={styles.metaValue}>
|
|
||||||
{product?.merchant?.name || 'N/A'}
|
|
||||||
</Text>
|
|
||||||
</View>
|
|
||||||
</View>
|
|
||||||
</View>
|
</View>
|
||||||
|
|
||||||
<View style={styles.sectionDivider} />
|
<CategoryTabs
|
||||||
|
styles={styles}
|
||||||
|
categories={categories}
|
||||||
|
activeCategory={activeCategory}
|
||||||
|
onSelect={setActiveCategory}
|
||||||
|
/>
|
||||||
|
|
||||||
{renderRatingsSection()}
|
<View style={styles.menuList}>
|
||||||
|
{filteredItems.length === 0 && (
|
||||||
|
<View style={styles.emptyMenuWrap}>
|
||||||
|
<Text style={styles.emptyMenuEmoji}>🍽️</Text>
|
||||||
|
<Text style={styles.emptyMenuText}>
|
||||||
|
No items in this category yet
|
||||||
|
</Text>
|
||||||
|
</View>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{filteredItems.map(item => (
|
||||||
|
<CatalogItemRow
|
||||||
|
key={item.id}
|
||||||
|
imageUrl={item.imageUrl}
|
||||||
|
title={item.title}
|
||||||
|
description={item.description}
|
||||||
|
price={item.price}
|
||||||
|
quantity={getItemQuantity(item.id)}
|
||||||
|
onAdd={() => handleAddItem(item)}
|
||||||
|
onRemove={() => {}}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</View>
|
||||||
</ScrollView>
|
</ScrollView>
|
||||||
|
|
||||||
{/* Sticky Bottom Bar */}
|
{totalItems > 0 && (
|
||||||
<View style={styles.bottomBarWrap}>
|
<CartStrip
|
||||||
{isInCart ? (
|
styles={styles}
|
||||||
// ── Already in cart: show inline qty stepper + Go to Cart ──
|
totalItems={totalItems}
|
||||||
<>
|
totalPrice={totalPrice}
|
||||||
<View style={styles.qtySelector}>
|
onPress={() => navigation.navigate('CartScreen')}
|
||||||
<TouchableOpacity
|
/>
|
||||||
style={styles.qtyBtn}
|
)}
|
||||||
onPress={handleDecrement}
|
|
||||||
activeOpacity={0.7}
|
|
||||||
disabled={localQty <= 1}
|
|
||||||
>
|
|
||||||
<Text style={styles.qtyBtnText}>-</Text>
|
|
||||||
</TouchableOpacity>
|
|
||||||
<Text style={styles.qtyValue}>{localQty}</Text>
|
|
||||||
<TouchableOpacity
|
|
||||||
style={styles.qtyBtn}
|
|
||||||
onPress={handleIncrement}
|
|
||||||
activeOpacity={0.7}
|
|
||||||
>
|
|
||||||
<Text style={styles.qtyBtnText}>+</Text>
|
|
||||||
</TouchableOpacity>
|
|
||||||
</View>
|
|
||||||
|
|
||||||
<TouchableOpacity
|
|
||||||
style={styles.addBtn}
|
|
||||||
activeOpacity={0.8}
|
|
||||||
onPress={() => navigation.navigate('CartScreen')}
|
|
||||||
>
|
|
||||||
<Text style={styles.addBtnText}>Go to Cart</Text>
|
|
||||||
</TouchableOpacity>
|
|
||||||
</>
|
|
||||||
) : (
|
|
||||||
// ── Not in cart: qty picker + Add to Cart ──
|
|
||||||
<>
|
|
||||||
<View style={styles.qtySelector}>
|
|
||||||
<TouchableOpacity
|
|
||||||
style={styles.qtyBtn}
|
|
||||||
onPress={() => setLocalQty(q => Math.max(0, q - 1))}
|
|
||||||
activeOpacity={0.7}
|
|
||||||
disabled={localQty <= 0}
|
|
||||||
>
|
|
||||||
<Text style={styles.qtyBtnText}>-</Text>
|
|
||||||
</TouchableOpacity>
|
|
||||||
<Text style={styles.qtyValue}>{localQty}</Text>
|
|
||||||
<TouchableOpacity
|
|
||||||
style={styles.qtyBtn}
|
|
||||||
onPress={() => setLocalQty(q => q + 1)}
|
|
||||||
activeOpacity={0.7}
|
|
||||||
>
|
|
||||||
<Text style={styles.qtyBtnText}>+</Text>
|
|
||||||
</TouchableOpacity>
|
|
||||||
</View>
|
|
||||||
|
|
||||||
<TouchableOpacity
|
|
||||||
style={styles.addBtn}
|
|
||||||
activeOpacity={0.8}
|
|
||||||
onPress={handleAddToCart}
|
|
||||||
disabled={
|
|
||||||
!product ||
|
|
||||||
(product.isTrackStock && product.stockQuantity === 0)
|
|
||||||
}
|
|
||||||
>
|
|
||||||
<Text style={styles.addBtnText}>
|
|
||||||
{!product
|
|
||||||
? 'Loading...'
|
|
||||||
: product.isTrackStock && product.stockQuantity === 0
|
|
||||||
? 'Out of Stock'
|
|
||||||
: 'Add to Cart'}
|
|
||||||
</Text>
|
|
||||||
</TouchableOpacity>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</View>
|
|
||||||
</View>
|
</View>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@ -1,33 +0,0 @@
|
|||||||
import { createReducer } from '@reduxjs/toolkit';
|
|
||||||
import { Product } from '@interfaces';
|
|
||||||
import { getProductDetailsThunk } from './thunk';
|
|
||||||
|
|
||||||
export interface ProviderDetailsState {
|
|
||||||
product: Product | null;
|
|
||||||
isLoading: boolean;
|
|
||||||
error: string | null;
|
|
||||||
}
|
|
||||||
|
|
||||||
const initialState: ProviderDetailsState = {
|
|
||||||
product: null,
|
|
||||||
isLoading: false,
|
|
||||||
error: null,
|
|
||||||
};
|
|
||||||
|
|
||||||
const providerDetailsReducer = createReducer(initialState, builder => {
|
|
||||||
builder
|
|
||||||
.addCase(getProductDetailsThunk.pending, state => {
|
|
||||||
state.isLoading = true;
|
|
||||||
state.error = null;
|
|
||||||
})
|
|
||||||
.addCase(getProductDetailsThunk.fulfilled, (state, action) => {
|
|
||||||
state.product = action.payload;
|
|
||||||
state.isLoading = false;
|
|
||||||
})
|
|
||||||
.addCase(getProductDetailsThunk.rejected, (state, action) => {
|
|
||||||
state.isLoading = false;
|
|
||||||
state.error = action.payload || 'Failed to fetch product details';
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
export default providerDetailsReducer;
|
|
||||||
@ -1,18 +0,0 @@
|
|||||||
import { createAsyncThunk } from '@reduxjs/toolkit';
|
|
||||||
import { getProductDetailsApi } from '@api';
|
|
||||||
import { Product } from '@interfaces';
|
|
||||||
|
|
||||||
export const getProductDetailsThunk = createAsyncThunk<
|
|
||||||
Product,
|
|
||||||
string,
|
|
||||||
{ rejectValue: string }
|
|
||||||
>('providerDetails/getProductDetails', async (productId, { rejectWithValue }) => {
|
|
||||||
try {
|
|
||||||
const response = await getProductDetailsApi(productId);
|
|
||||||
return response;
|
|
||||||
} catch (error: any) {
|
|
||||||
return rejectWithValue(
|
|
||||||
error.response?.data?.message || 'Failed to fetch product details',
|
|
||||||
);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
@ -1,2 +1 @@
|
|||||||
export * from './setLocationScreen';
|
export * from './setLocationScreen';
|
||||||
export * from './reducer';
|
|
||||||
|
|||||||
@ -1,43 +0,0 @@
|
|||||||
import { createReducer, createAction } from '@reduxjs/toolkit';
|
|
||||||
|
|
||||||
// ─── Actions ──────────────────────────────────────────────────────────────────
|
|
||||||
|
|
||||||
export const setLocationData = createAction<{
|
|
||||||
latitude: number;
|
|
||||||
longitude: number;
|
|
||||||
mapAddress: string;
|
|
||||||
}>('setLocation/setLocationData');
|
|
||||||
|
|
||||||
export const clearLocationData = createAction('setLocation/clearLocationData');
|
|
||||||
|
|
||||||
// ─── State ────────────────────────────────────────────────────────────────────
|
|
||||||
|
|
||||||
export interface SetLocationState {
|
|
||||||
latitude: number | null;
|
|
||||||
longitude: number | null;
|
|
||||||
mapAddress: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
const initialState: SetLocationState = {
|
|
||||||
latitude: null,
|
|
||||||
longitude: null,
|
|
||||||
mapAddress: '',
|
|
||||||
};
|
|
||||||
|
|
||||||
// ─── Reducer ──────────────────────────────────────────────────────────────────
|
|
||||||
|
|
||||||
const setLocationReducer = createReducer(initialState, builder => {
|
|
||||||
builder
|
|
||||||
.addCase(setLocationData, (state, action) => {
|
|
||||||
state.latitude = action.payload.latitude;
|
|
||||||
state.longitude = action.payload.longitude;
|
|
||||||
state.mapAddress = action.payload.mapAddress;
|
|
||||||
})
|
|
||||||
.addCase(clearLocationData, state => {
|
|
||||||
state.latitude = null;
|
|
||||||
state.longitude = null;
|
|
||||||
state.mapAddress = '';
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
export default setLocationReducer;
|
|
||||||
@ -20,14 +20,8 @@ import {
|
|||||||
showPermissionDeniedAlert,
|
showPermissionDeniedAlert,
|
||||||
LatLng,
|
LatLng,
|
||||||
} from '../../../services/locationServices';
|
} from '../../../services/locationServices';
|
||||||
import { useAppDispatch } from '@store';
|
|
||||||
import { setLocationData } from './reducer';
|
|
||||||
import { OnboardingStackParamList } from '@navigation/onboardingStack';
|
|
||||||
|
|
||||||
type NavProp = StackNavigationProp<
|
type NavProp = StackNavigationProp<AuthStackParamList, 'SetLocationScreen'>;
|
||||||
OnboardingStackParamList,
|
|
||||||
'SetLocationScreen'
|
|
||||||
>;
|
|
||||||
|
|
||||||
const DELTA = { latitudeDelta: 0.006, longitudeDelta: 0.006 };
|
const DELTA = { latitudeDelta: 0.006, longitudeDelta: 0.006 };
|
||||||
|
|
||||||
@ -35,7 +29,6 @@ export const SetLocationScreen: React.FC = () => {
|
|||||||
const { colors } = useAppTheme();
|
const { colors } = useAppTheme();
|
||||||
const styles = getStyles(colors);
|
const styles = getStyles(colors);
|
||||||
const navigation = useNavigation<NavProp>();
|
const navigation = useNavigation<NavProp>();
|
||||||
const dispatch = useAppDispatch();
|
|
||||||
const mapRef = useRef<MapView>(null);
|
const mapRef = useRef<MapView>(null);
|
||||||
// Guard: never call setState after the component has unmounted
|
// Guard: never call setState after the component has unmounted
|
||||||
const isMounted = useRef(true);
|
const isMounted = useRef(true);
|
||||||
@ -156,16 +149,7 @@ export const SetLocationScreen: React.FC = () => {
|
|||||||
|
|
||||||
<PrimaryButton
|
<PrimaryButton
|
||||||
title="Confirm Location"
|
title="Confirm Location"
|
||||||
onPress={() => {
|
onPress={() => navigation.navigate('CompleteProfileScreen')}
|
||||||
dispatch(
|
|
||||||
setLocationData({
|
|
||||||
latitude: coords.latitude,
|
|
||||||
longitude: coords.longitude,
|
|
||||||
mapAddress: address,
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
navigation.navigate('CompleteProfileScreen');
|
|
||||||
}}
|
|
||||||
style={styles.confirmButton}
|
style={styles.confirmButton}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
|||||||
@ -1 +0,0 @@
|
|||||||
export * from './writeReviewScreen';
|
|
||||||
@ -1,31 +0,0 @@
|
|||||||
import { StyleSheet } from 'react-native';
|
|
||||||
import { typography } from '@theme';
|
|
||||||
|
|
||||||
export const getStyles = (colors: any) => StyleSheet.create({
|
|
||||||
container: {
|
|
||||||
flex: 1,
|
|
||||||
backgroundColor: colors.background,
|
|
||||||
},
|
|
||||||
content: {
|
|
||||||
flex: 1,
|
|
||||||
justifyContent: 'center',
|
|
||||||
alignItems: 'center',
|
|
||||||
paddingHorizontal: 24,
|
|
||||||
},
|
|
||||||
icon: {
|
|
||||||
fontSize: 64,
|
|
||||||
marginBottom: 16,
|
|
||||||
},
|
|
||||||
title: {
|
|
||||||
fontSize: typography.fontSize.xl,
|
|
||||||
fontWeight: typography.fontWeight.bold,
|
|
||||||
color: colors.text,
|
|
||||||
textAlign: 'center',
|
|
||||||
marginBottom: 8,
|
|
||||||
},
|
|
||||||
subtitle: {
|
|
||||||
fontSize: typography.fontSize.md,
|
|
||||||
color: colors.textSecondary,
|
|
||||||
marginBottom: 32,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
@ -1,43 +0,0 @@
|
|||||||
import React, { useState } from 'react';
|
|
||||||
import { View, Text } from 'react-native';
|
|
||||||
import { useNavigation } from '@react-navigation/native';
|
|
||||||
import { StackNavigationProp } from '@react-navigation/stack';
|
|
||||||
import { getStyles } from './writeReviewScreen.styles';
|
|
||||||
import { Header, RatingStars, PrimaryButton } from '@components';
|
|
||||||
import { useAppTheme } from '@theme';
|
|
||||||
import { AppStackParamList } from '../../../navigation/appStack';
|
|
||||||
|
|
||||||
type WriteReviewNavProp = StackNavigationProp<
|
|
||||||
AppStackParamList,
|
|
||||||
'WriteReviewScreen'
|
|
||||||
>;
|
|
||||||
|
|
||||||
export const WriteReviewScreen: React.FC = () => {
|
|
||||||
const { colors } = useAppTheme();
|
|
||||||
const styles = getStyles(colors);
|
|
||||||
const navigation = useNavigation<WriteReviewNavProp>();
|
|
||||||
const [rating, setRating] = useState(0);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<View style={styles.container}>
|
|
||||||
<Header
|
|
||||||
title="Order Delivered"
|
|
||||||
onBack={() => navigation.navigate('MainTabs')}
|
|
||||||
/>
|
|
||||||
<View style={styles.content}>
|
|
||||||
<Text style={styles.icon}>🎉</Text>
|
|
||||||
<Text style={styles.title}>Your order has been delivered!</Text>
|
|
||||||
<Text style={styles.subtitle}>How was your experience?</Text>
|
|
||||||
|
|
||||||
<RatingStars rating={rating} onRate={setRating} size={40} />
|
|
||||||
|
|
||||||
<PrimaryButton
|
|
||||||
title="Rate & Tip"
|
|
||||||
onPress={() => navigation.navigate('MainTabs')}
|
|
||||||
disabled={rating === 0}
|
|
||||||
style={{ marginTop: 32 }}
|
|
||||||
/>
|
|
||||||
</View>
|
|
||||||
</View>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
@ -0,0 +1 @@
|
|||||||
|
export * from './useLoginScreen';
|
||||||
@ -2,7 +2,7 @@ import { useState, useCallback } from 'react';
|
|||||||
import { useNavigation } from '@react-navigation/native';
|
import { useNavigation } from '@react-navigation/native';
|
||||||
import { StackNavigationProp } from '@react-navigation/stack';
|
import { StackNavigationProp } from '@react-navigation/stack';
|
||||||
import { AuthStackParamList } from 'app/navigation/authStack';
|
import { AuthStackParamList } from 'app/navigation/authStack';
|
||||||
import { loginWithPhone, useAppDispatch } from '@store';
|
import { loginWithPhone, useAppDispatch } from '../store';
|
||||||
|
|
||||||
type LoginNavProp = StackNavigationProp<AuthStackParamList, 'LoginScreen'>;
|
type LoginNavProp = StackNavigationProp<AuthStackParamList, 'LoginScreen'>;
|
||||||
|
|
||||||
@ -1,61 +0,0 @@
|
|||||||
export interface LoginResponse {
|
|
||||||
message: string;
|
|
||||||
phone: string;
|
|
||||||
code: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface VerifyOtpResponse {
|
|
||||||
message: string;
|
|
||||||
isNewUser: boolean;
|
|
||||||
accessToken: string;
|
|
||||||
refreshToken: string;
|
|
||||||
user: User;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface User {
|
|
||||||
id: string;
|
|
||||||
roleId: string;
|
|
||||||
roleEnum: RoleEnum;
|
|
||||||
name: string | null;
|
|
||||||
email: string | null;
|
|
||||||
phone: string;
|
|
||||||
profileImage: string | null;
|
|
||||||
status: UserStatus;
|
|
||||||
mfaEnabled: boolean;
|
|
||||||
createdAt: string;
|
|
||||||
updatedAt: string;
|
|
||||||
deletedAt: string | null;
|
|
||||||
isDeleted: boolean;
|
|
||||||
lastLogoutAt: string | null;
|
|
||||||
loginAt: string;
|
|
||||||
refreshTokenId: string;
|
|
||||||
refreshTokenHash: string;
|
|
||||||
refreshTokenExpiresAt: string;
|
|
||||||
categoryPreferences: CategoryPreference[];
|
|
||||||
gender: Gender | null;
|
|
||||||
dateOfBirth: string | null;
|
|
||||||
role: Role;
|
|
||||||
// merchants: Merchant[];
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface Role {
|
|
||||||
id: string;
|
|
||||||
code: string;
|
|
||||||
scope: string;
|
|
||||||
isSystem: boolean;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface CategoryPreference {
|
|
||||||
// Extend when backend starts returning data
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface Merchant {
|
|
||||||
name: string;
|
|
||||||
imageUrl: string | null;
|
|
||||||
}
|
|
||||||
|
|
||||||
export type RoleEnum = 'CUSTOMER';
|
|
||||||
|
|
||||||
export type UserStatus = 'ONBOARDING' | 'ACTIVE' | 'INACTIVE' | 'BLOCKED';
|
|
||||||
|
|
||||||
export type Gender = 'MALE' | 'FEMALE' | 'OTHER';
|
|
||||||
@ -1,51 +0,0 @@
|
|||||||
export interface AddToCartRequest {
|
|
||||||
productId: string;
|
|
||||||
quantity: number;
|
|
||||||
attributes?: CartItemAttributes;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface CartItemAttributes {
|
|
||||||
size?: string;
|
|
||||||
extraToppings?: string[];
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface CartProduct {
|
|
||||||
id: string;
|
|
||||||
name: string;
|
|
||||||
price: number;
|
|
||||||
compareAtPrice: number;
|
|
||||||
imageUrl: string;
|
|
||||||
stockQuantity: number;
|
|
||||||
isTrackStock: boolean;
|
|
||||||
merchantId: string;
|
|
||||||
merchantName: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface CartItem {
|
|
||||||
id: string;
|
|
||||||
productId: string;
|
|
||||||
quantity: number;
|
|
||||||
attributes: Record<string, any>;
|
|
||||||
createdAt: string;
|
|
||||||
updatedAt: string;
|
|
||||||
product: CartProduct;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface MerchantCartGroup {
|
|
||||||
merchantId: string;
|
|
||||||
merchantName: string;
|
|
||||||
items: CartItem[];
|
|
||||||
subtotal: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface CartResponse {
|
|
||||||
[x: string]: any;
|
|
||||||
id: string;
|
|
||||||
customerId: string;
|
|
||||||
items: CartItem[];
|
|
||||||
groupedByMerchant: MerchantCartGroup[];
|
|
||||||
subtotal: number;
|
|
||||||
totalItems: number;
|
|
||||||
createdAt: string;
|
|
||||||
updatedAt: string;
|
|
||||||
}
|
|
||||||
@ -1,49 +0,0 @@
|
|||||||
import { Gender, UserStatus } from './auth';
|
|
||||||
|
|
||||||
export interface CustomerResponse {
|
|
||||||
id: string;
|
|
||||||
userId: string;
|
|
||||||
customerCode: string;
|
|
||||||
createdAt: string;
|
|
||||||
deletedAt: string | null;
|
|
||||||
user: CustomerUser;
|
|
||||||
addresses: CustomerAddress[];
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface CustomerUser {
|
|
||||||
id: string;
|
|
||||||
name: string;
|
|
||||||
email: string;
|
|
||||||
phone: string;
|
|
||||||
profileImage: string | null;
|
|
||||||
status: UserStatus;
|
|
||||||
gender: Gender;
|
|
||||||
dateOfBirth: string | null;
|
|
||||||
categoryPreferences: string[];
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface CustomerAddress {
|
|
||||||
id: string;
|
|
||||||
customerId: string;
|
|
||||||
merchantId: string | null;
|
|
||||||
deliveryPartnerId: string | null;
|
|
||||||
latitude: string;
|
|
||||||
longitude: string;
|
|
||||||
mapAddress: string;
|
|
||||||
houseNumber: string;
|
|
||||||
landmark: string;
|
|
||||||
addressLine1: string;
|
|
||||||
city: string;
|
|
||||||
state: string;
|
|
||||||
postalCode: string;
|
|
||||||
phone: string;
|
|
||||||
label: AddressLabel;
|
|
||||||
isDefault: boolean;
|
|
||||||
createdAt: string;
|
|
||||||
updatedAt: string;
|
|
||||||
deletedAt: string | null;
|
|
||||||
}
|
|
||||||
|
|
||||||
// export type UserStatus = 'ACTIVE' | 'INACTIVE' | 'SUSPENDED';
|
|
||||||
|
|
||||||
export type AddressLabel = 'Home' | 'Work' | 'Other';
|
|
||||||
@ -43,37 +43,32 @@ export interface CartItem {
|
|||||||
quantity: number;
|
quantity: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
// export type OrderStatus =
|
export type OrderStatus = 'placed' | 'confirmed' | 'dispatched' | 'delivered' | 'cancelled';
|
||||||
// | 'placed'
|
|
||||||
// | 'confirmed'
|
|
||||||
// | 'dispatched'
|
|
||||||
// | 'delivered'
|
|
||||||
// | 'cancelled';
|
|
||||||
|
|
||||||
// export interface Order {
|
export interface Order {
|
||||||
// id: string;
|
id: string;
|
||||||
// providerId: string;
|
providerId: string;
|
||||||
// providerName: string;
|
providerName: string;
|
||||||
// providerImage: string;
|
providerImage: string;
|
||||||
// items: CartItem[];
|
items: CartItem[];
|
||||||
// status: OrderStatus;
|
status: OrderStatus;
|
||||||
// total: number;
|
total: number;
|
||||||
// deliveryFee: number;
|
deliveryFee: number;
|
||||||
// platformFee: number;
|
platformFee: number;
|
||||||
// discount: number;
|
discount: number;
|
||||||
// couponCode?: string;
|
couponCode?: string;
|
||||||
// createdAt: string;
|
createdAt: string;
|
||||||
// estimatedDelivery: string;
|
estimatedDelivery: string;
|
||||||
// deliveryAddress: Location;
|
deliveryAddress: Location;
|
||||||
// }
|
}
|
||||||
|
|
||||||
// export interface TrackingStep {
|
export interface TrackingStep {
|
||||||
// key: OrderStatus;
|
key: OrderStatus;
|
||||||
// label: string;
|
label: string;
|
||||||
// timestamp?: string;
|
timestamp?: string;
|
||||||
// isCompleted: boolean;
|
isCompleted: boolean;
|
||||||
// isActive: boolean;
|
isActive: boolean;
|
||||||
// }
|
}
|
||||||
|
|
||||||
export interface DeliveryAgent {
|
export interface DeliveryAgent {
|
||||||
name: string;
|
name: string;
|
||||||
@ -88,11 +83,3 @@ export interface PaymentMethod {
|
|||||||
label: string;
|
label: string;
|
||||||
icon?: string;
|
icon?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export * from './auth';
|
|
||||||
export * from './onboard';
|
|
||||||
export * from './product';
|
|
||||||
export * from './cart';
|
|
||||||
export * from './paymentMethods';
|
|
||||||
export * from './customerProfile';
|
|
||||||
export * from './order';
|
|
||||||
|
|||||||
@ -1,55 +0,0 @@
|
|||||||
import { Role } from './auth';
|
|
||||||
|
|
||||||
export interface Category {
|
|
||||||
id: string;
|
|
||||||
parentId: string | null;
|
|
||||||
name: string;
|
|
||||||
slug: string;
|
|
||||||
imageUrl: string | null;
|
|
||||||
isActive: boolean;
|
|
||||||
children: Category[];
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface onBoardPayload {
|
|
||||||
name: string;
|
|
||||||
email: string;
|
|
||||||
latitude: number;
|
|
||||||
longitude: number;
|
|
||||||
addressLabel: string;
|
|
||||||
addressLine1: string;
|
|
||||||
mapAddress: string;
|
|
||||||
houseNumber: string;
|
|
||||||
landmark: string;
|
|
||||||
city: string;
|
|
||||||
state: string;
|
|
||||||
postalCode: string;
|
|
||||||
addressPhone: string;
|
|
||||||
categoryPreferences: string[];
|
|
||||||
gender: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
export interface UserProfile {
|
|
||||||
id: string;
|
|
||||||
roleId: string;
|
|
||||||
roleEnum: 'CUSTOMER' | 'MERCHANT' | 'BUSINESS_ADMIN' | 'SUPER_ADMIN';
|
|
||||||
name: string;
|
|
||||||
email: string;
|
|
||||||
phone: string;
|
|
||||||
profileImage: string | null;
|
|
||||||
status: 'ACTIVE' | 'INACTIVE' | 'SUSPENDED';
|
|
||||||
mfaEnabled: boolean;
|
|
||||||
createdAt: string;
|
|
||||||
updatedAt: string;
|
|
||||||
deletedAt: string | null;
|
|
||||||
isDeleted: boolean;
|
|
||||||
lastLogoutAt: string | null;
|
|
||||||
loginAt: string;
|
|
||||||
refreshTokenId: string;
|
|
||||||
refreshTokenHash: string;
|
|
||||||
refreshTokenExpiresAt: string;
|
|
||||||
categoryPreferences: string[];
|
|
||||||
gender: 'MALE' | 'FEMALE' | 'OTHER';
|
|
||||||
dateOfBirth: string;
|
|
||||||
role: Role;
|
|
||||||
}
|
|
||||||
@ -1,187 +0,0 @@
|
|||||||
import { Merchant } from './auth';
|
|
||||||
|
|
||||||
export interface OrderRequest {
|
|
||||||
addressId: string;
|
|
||||||
paymentMethodId: string;
|
|
||||||
paymentMethod: string;
|
|
||||||
orderType: string;
|
|
||||||
idempotencyKey: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface PlaceOrderResponse {
|
|
||||||
success: boolean;
|
|
||||||
message: string;
|
|
||||||
orders: Order[];
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface Order {
|
|
||||||
id: string;
|
|
||||||
orderNumber: string;
|
|
||||||
customerId: string;
|
|
||||||
merchantId: string;
|
|
||||||
couponCode: string | null;
|
|
||||||
appliedPromotionId: string | null;
|
|
||||||
status: OrderStatus;
|
|
||||||
orderType: OrderType;
|
|
||||||
paymentMethodId: string;
|
|
||||||
paymentMethod: PaymentMethod;
|
|
||||||
currency: string;
|
|
||||||
subtotal: string;
|
|
||||||
deliveryFee: string;
|
|
||||||
platformFee: string;
|
|
||||||
serviceFee: string;
|
|
||||||
taxAmount: string;
|
|
||||||
discountAmount: string;
|
|
||||||
totalAmount: string;
|
|
||||||
pickupAddress: PickupAddress;
|
|
||||||
dropAddress: DropAddress;
|
|
||||||
metadata: OrderMetadata;
|
|
||||||
version: number;
|
|
||||||
createdAt: string;
|
|
||||||
updatedAt: string;
|
|
||||||
deletedAt: string | null;
|
|
||||||
|
|
||||||
orderItems?: OrderItem[];
|
|
||||||
payments?: Payment[];
|
|
||||||
delivery?: Delivery;
|
|
||||||
merchant?: Merchant;
|
|
||||||
|
|
||||||
customer?: OrderCustomer;
|
|
||||||
tracking?: TrackingEntry[];
|
|
||||||
}
|
|
||||||
|
|
||||||
// Add these new interfaces
|
|
||||||
|
|
||||||
export interface OrderCustomerUser {
|
|
||||||
name: string;
|
|
||||||
phone: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface OrderCustomer {
|
|
||||||
id: string;
|
|
||||||
userId: string;
|
|
||||||
customerCode: string;
|
|
||||||
createdAt: string;
|
|
||||||
deletedAt: string | null;
|
|
||||||
user: OrderCustomerUser;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface TrackingEntry {
|
|
||||||
status: OrderStatus;
|
|
||||||
time: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface PickupAddress {
|
|
||||||
city: string;
|
|
||||||
label: string | null;
|
|
||||||
phone: string;
|
|
||||||
state: string;
|
|
||||||
landmark: string;
|
|
||||||
postalCode: string;
|
|
||||||
houseNumber: string;
|
|
||||||
addressLine1: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface DropAddress {
|
|
||||||
city: string;
|
|
||||||
label: string;
|
|
||||||
phone: string;
|
|
||||||
state: string;
|
|
||||||
landmark: string;
|
|
||||||
latitude: number;
|
|
||||||
longitude: number;
|
|
||||||
postalCode: string;
|
|
||||||
houseNumber: string;
|
|
||||||
addressLine1: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface OrderMetadata {
|
|
||||||
idempotencyKey: string;
|
|
||||||
trackingHistory: TrackingHistory;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface TrackingHistory {
|
|
||||||
PLACED: string;
|
|
||||||
CONFIRMED: string;
|
|
||||||
// Future statuses can be added here:
|
|
||||||
// PREPARING?: string;
|
|
||||||
// OUT_FOR_DELIVERY?: string;
|
|
||||||
// DELIVERED?: string;
|
|
||||||
// CANCELLED?: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface OrderItem {
|
|
||||||
id: string;
|
|
||||||
orderId: string;
|
|
||||||
productId: string;
|
|
||||||
itemType: 'PRODUCT' | 'SERVICE';
|
|
||||||
name: string;
|
|
||||||
quantity: string;
|
|
||||||
unitPrice: string;
|
|
||||||
totalAmount: string;
|
|
||||||
attributes: Record<string, any>;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface Payment {
|
|
||||||
id: string;
|
|
||||||
orderId: string;
|
|
||||||
paymentReference: string;
|
|
||||||
paymentMethodId: string;
|
|
||||||
method: PaymentMethod;
|
|
||||||
provider: PaymentMethod;
|
|
||||||
providerPaymentId: string | null;
|
|
||||||
status: PaymentStatus;
|
|
||||||
amount: string;
|
|
||||||
currency: string;
|
|
||||||
idempotencyKey: string;
|
|
||||||
metadata: Record<string, any>;
|
|
||||||
createdAt: string;
|
|
||||||
updatedAt: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface Delivery {
|
|
||||||
id: string;
|
|
||||||
orderId: string;
|
|
||||||
deliveryPartnerId: string | null;
|
|
||||||
deliveryNumber: string;
|
|
||||||
status: DeliveryStatus;
|
|
||||||
estimatedDistanceKm: number | null;
|
|
||||||
actualDistanceKm: number | null;
|
|
||||||
proofOfPickupKey: string | null;
|
|
||||||
proofOfDeliveryKey: string | null;
|
|
||||||
pickedUpAt: string | null;
|
|
||||||
deliveredAt: string | null;
|
|
||||||
}
|
|
||||||
|
|
||||||
// export interface Merchant {
|
|
||||||
// name: string;
|
|
||||||
// imageUrl: string | null;
|
|
||||||
// }
|
|
||||||
|
|
||||||
export type PaymentStatus =
|
|
||||||
| 'PENDING'
|
|
||||||
| 'PROCESSING'
|
|
||||||
| 'COMPLETED'
|
|
||||||
| 'FAILED'
|
|
||||||
| 'REFUNDED';
|
|
||||||
|
|
||||||
export type DeliveryStatus =
|
|
||||||
| 'PENDING_ASSIGNMENT'
|
|
||||||
| 'ASSIGNED'
|
|
||||||
| 'PICKED_UP'
|
|
||||||
| 'IN_TRANSIT'
|
|
||||||
| 'DELIVERED'
|
|
||||||
| 'CANCELLED';
|
|
||||||
|
|
||||||
export type OrderStatus =
|
|
||||||
| 'PENDING'
|
|
||||||
| 'CONFIRMED'
|
|
||||||
| 'PREPARING'
|
|
||||||
| 'READY_FOR_PICKUP'
|
|
||||||
| 'OUT_FOR_DELIVERY'
|
|
||||||
| 'DELIVERED'
|
|
||||||
| 'CANCELLED';
|
|
||||||
|
|
||||||
export type OrderType = 'DELIVERY' | 'PICKUP';
|
|
||||||
|
|
||||||
export type PaymentMethod = 'CARD' | 'CASH' | 'UPI' | 'WALLET' | 'NET_BANKING';
|
|
||||||
@ -1,12 +0,0 @@
|
|||||||
export interface PaymentMethod {
|
|
||||||
id: string;
|
|
||||||
code: 'CARD' | 'COD' | 'UPI' | 'WALLET';
|
|
||||||
displayName: string;
|
|
||||||
iconUrl: string;
|
|
||||||
isActive: boolean;
|
|
||||||
createdAt: string;
|
|
||||||
updatedAt: string;
|
|
||||||
deletedAt: string | null;
|
|
||||||
}
|
|
||||||
|
|
||||||
export type PaymentMethodsResponse = PaymentMethod[];
|
|
||||||
@ -1,143 +0,0 @@
|
|||||||
export interface ProductMedia {
|
|
||||||
id: string;
|
|
||||||
productId: string;
|
|
||||||
url: string;
|
|
||||||
mediaType: 'IMAGE' | 'VIDEO';
|
|
||||||
sortOrder: number;
|
|
||||||
createdAt: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface ProductCategory {
|
|
||||||
name: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface ProductMerchant {
|
|
||||||
name: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface ProductAttributes {
|
|
||||||
featured?: boolean;
|
|
||||||
[key: string]: any;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface ProductDimensions {
|
|
||||||
[key: string]: any;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface Products {
|
|
||||||
id: string;
|
|
||||||
merchantId: string;
|
|
||||||
categoryId: string;
|
|
||||||
sku: string;
|
|
||||||
barcode: string | null;
|
|
||||||
name: string;
|
|
||||||
brand: string | null;
|
|
||||||
description: string;
|
|
||||||
imageUrl: string;
|
|
||||||
productType: string;
|
|
||||||
price: string;
|
|
||||||
compareAtPrice: string;
|
|
||||||
currency: string;
|
|
||||||
weightKg: string | null;
|
|
||||||
weightGm: string | null;
|
|
||||||
dimensions: ProductDimensions;
|
|
||||||
attributes: ProductAttributes;
|
|
||||||
stockQuantity: number;
|
|
||||||
isTrackStock: boolean;
|
|
||||||
isActive: boolean;
|
|
||||||
createdAt: string;
|
|
||||||
deletedAt: string | null;
|
|
||||||
category: ProductCategory;
|
|
||||||
merchant: ProductMerchant;
|
|
||||||
media: ProductMedia[];
|
|
||||||
avgRating: string;
|
|
||||||
totalRatings: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface PaginationMeta {
|
|
||||||
total: number;
|
|
||||||
page: number;
|
|
||||||
limit: number;
|
|
||||||
totalPages: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface Product {
|
|
||||||
id: string;
|
|
||||||
merchantId: string;
|
|
||||||
categoryId: string;
|
|
||||||
sku: string;
|
|
||||||
barcode: string | null;
|
|
||||||
name: string;
|
|
||||||
brand: string | null;
|
|
||||||
description: string;
|
|
||||||
imageUrl: string;
|
|
||||||
productType: ProductType;
|
|
||||||
price: string;
|
|
||||||
compareAtPrice: string;
|
|
||||||
currency: string;
|
|
||||||
weightKg: string | null;
|
|
||||||
weightGm: string | null;
|
|
||||||
dimensions: ProductDimensions;
|
|
||||||
attributes: ProductAttributes;
|
|
||||||
stockQuantity: number;
|
|
||||||
isTrackStock: boolean;
|
|
||||||
isActive: boolean;
|
|
||||||
createdAt: string;
|
|
||||||
deletedAt: string | null;
|
|
||||||
category: ProductCategory;
|
|
||||||
merchant: ProductMerchant;
|
|
||||||
media: ProductMedia[];
|
|
||||||
avgRating: string;
|
|
||||||
totalRatings: number;
|
|
||||||
recentRatings: ProductRating[];
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface ProductCategory {
|
|
||||||
id: string;
|
|
||||||
name: string;
|
|
||||||
slug: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface ProductMerchant {
|
|
||||||
id: string;
|
|
||||||
name: string;
|
|
||||||
merchantCode: string;
|
|
||||||
rating: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
// export interface ProductMedia {
|
|
||||||
// id: string;
|
|
||||||
// productId: string;
|
|
||||||
// url: string;
|
|
||||||
// mediaType: MediaType;
|
|
||||||
// sortOrder: number;
|
|
||||||
// createdAt: string;
|
|
||||||
// }
|
|
||||||
|
|
||||||
// export interface ProductAttributes {
|
|
||||||
// featured: boolean;
|
|
||||||
// }
|
|
||||||
|
|
||||||
// export interface ProductDimensions {
|
|
||||||
// [key: string]: unknown;
|
|
||||||
// }
|
|
||||||
|
|
||||||
export interface ProductRating {
|
|
||||||
id: string;
|
|
||||||
userId: string;
|
|
||||||
rating: number;
|
|
||||||
review: string;
|
|
||||||
createdAt: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export enum ProductType {
|
|
||||||
FOOD = 'FOOD',
|
|
||||||
GROCERIES = 'GROCERIES',
|
|
||||||
PHARMACY = 'PHARMACY',
|
|
||||||
ELECTRONICS = 'ELECTRONICS',
|
|
||||||
}
|
|
||||||
|
|
||||||
export enum MediaType {
|
|
||||||
IMAGE = 'IMAGE',
|
|
||||||
VIDEO = 'VIDEO',
|
|
||||||
}
|
|
||||||
@ -13,8 +13,6 @@ import {
|
|||||||
LiveTrackingScreen,
|
LiveTrackingScreen,
|
||||||
OrderDeliveredScreen,
|
OrderDeliveredScreen,
|
||||||
HelpSupportScreen,
|
HelpSupportScreen,
|
||||||
WriteReviewScreen,
|
|
||||||
OrderDetailsScreen,
|
|
||||||
} from '@features/screens';
|
} from '@features/screens';
|
||||||
|
|
||||||
export type AppStackParamList = {
|
export type AppStackParamList = {
|
||||||
@ -23,14 +21,12 @@ export type AppStackParamList = {
|
|||||||
ProviderDetailsScreen: { providerId: string; providerName?: string };
|
ProviderDetailsScreen: { providerId: string; providerName?: string };
|
||||||
CartScreen: undefined;
|
CartScreen: undefined;
|
||||||
CheckoutAddressScreen: undefined;
|
CheckoutAddressScreen: undefined;
|
||||||
CheckoutPaymentScreen: { selectedAddressId?: string } | undefined;
|
CheckoutPaymentScreen: undefined;
|
||||||
OrderConfirmedScreen: { orderId?: string } | undefined;
|
OrderConfirmedScreen: { orderId?: string } | undefined;
|
||||||
OrderTrackingScreen: { orderId?: string } | undefined;
|
OrderTrackingScreen: { orderId?: string } | undefined;
|
||||||
LiveTrackingScreen: { orderId?: string } | undefined;
|
LiveTrackingScreen: { orderId?: string } | undefined;
|
||||||
OrderDeliveredScreen: { orderId?: string } | undefined;
|
OrderDeliveredScreen: { orderId?: string } | undefined;
|
||||||
HelpSupportScreen: undefined;
|
HelpSupportScreen: undefined;
|
||||||
WriteReviewScreen: { productId: string } | undefined;
|
|
||||||
OrderDetailsScreen: { orderId: string } | undefined;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const Stack = createStackNavigator<AppStackParamList>();
|
const Stack = createStackNavigator<AppStackParamList>();
|
||||||
@ -40,38 +36,15 @@ export const AppStack: React.FC = () => {
|
|||||||
<Stack.Navigator screenOptions={{ headerShown: false }}>
|
<Stack.Navigator screenOptions={{ headerShown: false }}>
|
||||||
<Stack.Screen name="MainTabs" component={MainTabNavigator} />
|
<Stack.Screen name="MainTabs" component={MainTabNavigator} />
|
||||||
<Stack.Screen name="ProviderListScreen" component={ProviderListScreen} />
|
<Stack.Screen name="ProviderListScreen" component={ProviderListScreen} />
|
||||||
<Stack.Screen
|
<Stack.Screen name="ProviderDetailsScreen" component={ProviderDetailsScreen} />
|
||||||
name="ProviderDetailsScreen"
|
|
||||||
component={ProviderDetailsScreen}
|
|
||||||
/>
|
|
||||||
<Stack.Screen name="CartScreen" component={CartScreen} />
|
<Stack.Screen name="CartScreen" component={CartScreen} />
|
||||||
<Stack.Screen
|
<Stack.Screen name="CheckoutAddressScreen" component={CheckoutAddressScreen} />
|
||||||
name="CheckoutAddressScreen"
|
<Stack.Screen name="CheckoutPaymentScreen" component={CheckoutPaymentScreen} />
|
||||||
component={CheckoutAddressScreen}
|
<Stack.Screen name="OrderConfirmedScreen" component={OrderConfirmedScreen} />
|
||||||
/>
|
<Stack.Screen name="OrderTrackingScreen" component={OrderTrackingScreen} />
|
||||||
<Stack.Screen
|
|
||||||
name="CheckoutPaymentScreen"
|
|
||||||
component={CheckoutPaymentScreen}
|
|
||||||
/>
|
|
||||||
<Stack.Screen
|
|
||||||
name="OrderConfirmedScreen"
|
|
||||||
component={OrderConfirmedScreen}
|
|
||||||
/>
|
|
||||||
<Stack.Screen
|
|
||||||
name="OrderTrackingScreen"
|
|
||||||
component={OrderTrackingScreen}
|
|
||||||
/>
|
|
||||||
<Stack.Screen name="LiveTrackingScreen" component={LiveTrackingScreen} />
|
<Stack.Screen name="LiveTrackingScreen" component={LiveTrackingScreen} />
|
||||||
<Stack.Screen
|
<Stack.Screen name="OrderDeliveredScreen" component={OrderDeliveredScreen} />
|
||||||
name="OrderDeliveredScreen"
|
|
||||||
component={OrderDeliveredScreen}
|
|
||||||
/>
|
|
||||||
<Stack.Screen name="HelpSupportScreen" component={HelpSupportScreen} />
|
<Stack.Screen name="HelpSupportScreen" component={HelpSupportScreen} />
|
||||||
<Stack.Screen name="WriteReviewScreen" component={WriteReviewScreen} />
|
|
||||||
<Stack.Screen
|
|
||||||
name="OrderDetailsScreen"
|
|
||||||
component={OrderDetailsScreen}
|
|
||||||
/>
|
|
||||||
</Stack.Navigator>
|
</Stack.Navigator>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@ -12,10 +12,10 @@ import {
|
|||||||
export type AuthStackParamList = {
|
export type AuthStackParamList = {
|
||||||
LoginScreen: undefined;
|
LoginScreen: undefined;
|
||||||
OtpScreen: { mobileNumber: string };
|
OtpScreen: { mobileNumber: string };
|
||||||
// SetLocationScreen: undefined;
|
SetLocationScreen: undefined;
|
||||||
// CompleteProfileScreen: undefined;
|
CompleteProfileScreen: undefined;
|
||||||
// PreferencesScreen: undefined;
|
PreferencesScreen: undefined;
|
||||||
// OnboardingCompleteScreen: undefined;
|
OnboardingCompleteScreen: undefined;
|
||||||
};
|
};
|
||||||
|
|
||||||
const Stack = createStackNavigator<AuthStackParamList>();
|
const Stack = createStackNavigator<AuthStackParamList>();
|
||||||
@ -25,6 +25,10 @@ export const AuthStack: React.FC = () => {
|
|||||||
<Stack.Navigator screenOptions={{ headerShown: false }}>
|
<Stack.Navigator screenOptions={{ headerShown: false }}>
|
||||||
<Stack.Screen name="LoginScreen" component={LoginScreen} />
|
<Stack.Screen name="LoginScreen" component={LoginScreen} />
|
||||||
<Stack.Screen name="OtpScreen" component={OtpScreen} />
|
<Stack.Screen name="OtpScreen" component={OtpScreen} />
|
||||||
|
<Stack.Screen name="SetLocationScreen" component={SetLocationScreen} />
|
||||||
|
<Stack.Screen name="CompleteProfileScreen" component={CompleteProfileScreen} />
|
||||||
|
<Stack.Screen name="PreferencesScreen" component={PreferencesScreen} />
|
||||||
|
<Stack.Screen name="OnboardingCompleteScreen" component={OnboardingCompleteScreen} />
|
||||||
</Stack.Navigator>
|
</Stack.Navigator>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@ -1,33 +0,0 @@
|
|||||||
import {
|
|
||||||
CompleteProfileScreen,
|
|
||||||
OnboardingCompleteScreen,
|
|
||||||
PreferencesScreen,
|
|
||||||
SetLocationScreen,
|
|
||||||
} from '@features/screens';
|
|
||||||
import { createStackNavigator } from '@react-navigation/stack';
|
|
||||||
|
|
||||||
export type OnboardingStackParamList = {
|
|
||||||
SetLocationScreen: undefined;
|
|
||||||
CompleteProfileScreen: undefined;
|
|
||||||
PreferencesScreen: undefined;
|
|
||||||
OnboardingCompleteScreen: undefined;
|
|
||||||
};
|
|
||||||
|
|
||||||
const Stack = createStackNavigator<OnboardingStackParamList>();
|
|
||||||
|
|
||||||
export const OnboardingStack: React.FC = () => {
|
|
||||||
return (
|
|
||||||
<Stack.Navigator screenOptions={{ headerShown: false }}>
|
|
||||||
<Stack.Screen name="SetLocationScreen" component={SetLocationScreen} />
|
|
||||||
<Stack.Screen
|
|
||||||
name="CompleteProfileScreen"
|
|
||||||
component={CompleteProfileScreen}
|
|
||||||
/>
|
|
||||||
<Stack.Screen name="PreferencesScreen" component={PreferencesScreen} />
|
|
||||||
<Stack.Screen
|
|
||||||
name="OnboardingCompleteScreen"
|
|
||||||
component={OnboardingCompleteScreen}
|
|
||||||
/>
|
|
||||||
</Stack.Navigator>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
@ -1,23 +1,17 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { useSelector } from 'react-redux';
|
import { useSelector } from 'react-redux';
|
||||||
import { RootState, useAppSelector } from '../store';
|
import { RootState } from '../store';
|
||||||
import { AuthStack } from './authStack';
|
import { AuthStack } from './authStack';
|
||||||
import { AppStack } from './appStack';
|
import { AppStack } from './appStack';
|
||||||
import { OnboardingStack } from './onboardingStack';
|
|
||||||
|
|
||||||
export const RootNavigator: React.FC = () => {
|
export const RootNavigator: React.FC = () => {
|
||||||
const accessToken = useSelector((state: RootState) => state.auth.accessToken);
|
const { isAuthenticated, onboardingComplete } = useSelector(
|
||||||
const isOnboarding = useAppSelector(
|
(state: RootState) => state.auth,
|
||||||
state => state.auth.user?.status === 'ONBOARDING',
|
|
||||||
);
|
);
|
||||||
|
|
||||||
if (!accessToken) {
|
if (!isAuthenticated || !onboardingComplete) {
|
||||||
return <AuthStack />;
|
return <AuthStack />;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (isOnboarding) {
|
|
||||||
return <OnboardingStack />;
|
|
||||||
}
|
|
||||||
|
|
||||||
return <AppStack />;
|
return <AppStack />;
|
||||||
};
|
};
|
||||||
|
|||||||
@ -15,7 +15,7 @@ const STORAGE_KEYS = {
|
|||||||
} as const;
|
} as const;
|
||||||
|
|
||||||
// ─── Config ──────────────────────────────────────────────────────────────────
|
// ─── Config ──────────────────────────────────────────────────────────────────
|
||||||
const BASE_URL = 'https://db1d-202-8-116-13.ngrok-free.app'; // TODO: replace with your actual base URL
|
const BASE_URL = 'https://api.example.com/v1'; // TODO: replace with your actual base URL
|
||||||
|
|
||||||
// ─── Token Helpers ───────────────────────────────────────────────────────────
|
// ─── Token Helpers ───────────────────────────────────────────────────────────
|
||||||
export const tokenManager = {
|
export const tokenManager = {
|
||||||
@ -53,8 +53,8 @@ axiosInstance.interceptors.request.use(
|
|||||||
async (config: InternalAxiosRequestConfig) => {
|
async (config: InternalAxiosRequestConfig) => {
|
||||||
// Skip attaching token for auth endpoints (login, register, refresh)
|
// Skip attaching token for auth endpoints (login, register, refresh)
|
||||||
const publicPaths = [
|
const publicPaths = [
|
||||||
'/auth/otp/request',
|
'/auth/login',
|
||||||
'/auth/otp/verify',
|
'/auth/register',
|
||||||
'/auth/refresh-token',
|
'/auth/refresh-token',
|
||||||
];
|
];
|
||||||
const isPublic = publicPaths.some(path => config.url?.includes(path));
|
const isPublic = publicPaths.some(path => config.url?.includes(path));
|
||||||
@ -68,7 +68,7 @@ axiosInstance.interceptors.request.use(
|
|||||||
|
|
||||||
// Debug logging (remove in production)
|
// Debug logging (remove in production)
|
||||||
if (__DEV__) {
|
if (__DEV__) {
|
||||||
// console.log(`🚀 [API] ${config.method?.toUpperCase()} ${config.url}`);s
|
console.log(`🚀 [API] ${config.method?.toUpperCase()} ${config.url}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
return config;
|
return config;
|
||||||
|
|||||||
@ -1,2 +1,16 @@
|
|||||||
export * from './reducer';
|
export { default as authReducer } from './reducer';
|
||||||
export * from './thunk';
|
export {
|
||||||
|
setUser,
|
||||||
|
setLocation,
|
||||||
|
completeProfile,
|
||||||
|
completeOnboarding,
|
||||||
|
logout,
|
||||||
|
clearError,
|
||||||
|
} from './reducer';
|
||||||
|
export type { AuthState } from './reducer';
|
||||||
|
export {
|
||||||
|
loginWithPhone,
|
||||||
|
verifyOtp,
|
||||||
|
updateProfile,
|
||||||
|
logoutUser,
|
||||||
|
} from './thunk';
|
||||||
|
|||||||
@ -1,106 +1,129 @@
|
|||||||
import { createAction, createReducer } from '@reduxjs/toolkit';
|
import { createAction, createReducer } from '@reduxjs/toolkit';
|
||||||
import { VerifyOtpResponse } from '../../../interfaces';
|
import { User, Location } from '../../../interfaces';
|
||||||
import { loginWithPhone, verifyOtp, logoutUser, updateProfile } from './thunk';
|
import { loginWithPhone, verifyOtp, updateProfile, logoutUser } from './thunk';
|
||||||
import { LoginResponse, User } from '@interfaces/auth';
|
|
||||||
|
|
||||||
// ─── Sync Actions ─────────────────────────────────────────────────────────────
|
|
||||||
|
|
||||||
export const completeOnboarding = createAction('auth/completeOnboarding');
|
|
||||||
|
|
||||||
// ─── State ───────────────────────────────────────────────────────────────────
|
// ─── State ───────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
export interface AuthState {
|
export interface AuthState {
|
||||||
loginData: LoginResponse | null;
|
|
||||||
user: User | null;
|
user: User | null;
|
||||||
accessToken: string | null;
|
isAuthenticated: boolean;
|
||||||
refreshToken: string | null;
|
|
||||||
isNewUser: boolean | null;
|
|
||||||
error: string | null;
|
|
||||||
isLoading: boolean;
|
isLoading: boolean;
|
||||||
|
onboardingComplete: boolean;
|
||||||
|
error: string | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
const initialState: AuthState = {
|
const initialState: AuthState = {
|
||||||
loginData: null,
|
|
||||||
user: null,
|
user: null,
|
||||||
accessToken: null,
|
isAuthenticated: false,
|
||||||
refreshToken: null,
|
|
||||||
isNewUser: null,
|
|
||||||
isLoading: false,
|
isLoading: false,
|
||||||
|
onboardingComplete: false,
|
||||||
error: null,
|
error: null,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// ─── Sync Actions ────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
export const setUser = createAction<User>('auth/setUser');
|
||||||
|
export const setLocation = createAction<Location>('auth/setLocation');
|
||||||
|
export const completeProfile = createAction<{
|
||||||
|
name: string;
|
||||||
|
email: string;
|
||||||
|
locationLabels: string[];
|
||||||
|
}>('auth/completeProfile');
|
||||||
|
export const completeOnboarding = createAction('auth/completeOnboarding');
|
||||||
|
export const logout = createAction('auth/logout');
|
||||||
|
export const clearError = createAction('auth/clearError');
|
||||||
|
|
||||||
// ─── Reducer ─────────────────────────────────────────────────────────────────
|
// ─── Reducer ─────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
const authReducer = createReducer(initialState, builder => {
|
const authReducer = createReducer(initialState, builder => {
|
||||||
builder
|
builder
|
||||||
// Login
|
// ── Sync Actions ───────────────────────────────────────────────────────
|
||||||
|
.addCase(setUser, (state, action) => {
|
||||||
|
state.user = action.payload;
|
||||||
|
})
|
||||||
|
.addCase(setLocation, (state, _action) => {
|
||||||
|
if (state.user) {
|
||||||
|
state.user = { ...state.user };
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.addCase(completeProfile, (state, action) => {
|
||||||
|
if (state.user) {
|
||||||
|
state.user = {
|
||||||
|
...state.user,
|
||||||
|
name: action.payload.name,
|
||||||
|
email: action.payload.email,
|
||||||
|
locationLabels: action.payload.locationLabels,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.addCase(completeOnboarding, state => {
|
||||||
|
state.onboardingComplete = true;
|
||||||
|
})
|
||||||
|
.addCase(logout, state => {
|
||||||
|
state.user = null;
|
||||||
|
state.isAuthenticated = false;
|
||||||
|
state.onboardingComplete = false;
|
||||||
|
state.error = null;
|
||||||
|
})
|
||||||
|
.addCase(clearError, state => {
|
||||||
|
state.error = null;
|
||||||
|
})
|
||||||
|
|
||||||
|
// ── loginWithPhone ─────────────────────────────────────────────────────
|
||||||
.addCase(loginWithPhone.pending, state => {
|
.addCase(loginWithPhone.pending, state => {
|
||||||
state.isLoading = true;
|
state.isLoading = true;
|
||||||
state.error = null;
|
state.error = null;
|
||||||
})
|
})
|
||||||
.addCase(loginWithPhone.fulfilled, (state, action) => {
|
.addCase(loginWithPhone.fulfilled, state => {
|
||||||
state.loginData = action.payload;
|
|
||||||
state.isLoading = false;
|
state.isLoading = false;
|
||||||
state.error = null;
|
|
||||||
})
|
})
|
||||||
.addCase(loginWithPhone.rejected, (state, action) => {
|
.addCase(loginWithPhone.rejected, (state, action) => {
|
||||||
state.isLoading = false;
|
state.isLoading = false;
|
||||||
state.error = action.payload as string;
|
state.error = (action.payload as string) || 'Login failed';
|
||||||
})
|
})
|
||||||
|
|
||||||
// Verify OTP
|
// ── verifyOtp ──────────────────────────────────────────────────────────
|
||||||
.addCase(verifyOtp.pending, state => {
|
.addCase(verifyOtp.pending, state => {
|
||||||
state.isLoading = true;
|
state.isLoading = true;
|
||||||
state.error = null;
|
state.error = null;
|
||||||
})
|
})
|
||||||
.addCase(verifyOtp.fulfilled, (state, action) => {
|
.addCase(verifyOtp.fulfilled, (state, action) => {
|
||||||
const { user, accessToken, refreshToken, isNewUser } = action.payload;
|
|
||||||
state.user = user;
|
|
||||||
state.accessToken = accessToken;
|
|
||||||
state.refreshToken = refreshToken;
|
|
||||||
state.isNewUser = isNewUser;
|
|
||||||
state.isLoading = false;
|
state.isLoading = false;
|
||||||
state.error = null;
|
state.isAuthenticated = true;
|
||||||
|
state.user = action.payload.user;
|
||||||
})
|
})
|
||||||
.addCase(verifyOtp.rejected, (state, action) => {
|
.addCase(verifyOtp.rejected, (state, action) => {
|
||||||
state.isLoading = false;
|
state.isLoading = false;
|
||||||
state.error = action.payload as string;
|
state.error = (action.payload as string) || 'OTP verification failed';
|
||||||
})
|
})
|
||||||
|
|
||||||
// Complete Onboarding — flips user.status so RootNavigator swaps to AppStack
|
// ── updateProfile ──────────────────────────────────────────────────────
|
||||||
.addCase(completeOnboarding, state => {
|
.addCase(updateProfile.pending, state => {
|
||||||
if (state.user) {
|
state.isLoading = true;
|
||||||
state.user = { ...state.user, status: 'ACTIVE' };
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
// Update Profile
|
|
||||||
.addCase(updateProfile.fulfilled, (state, action) => {
|
|
||||||
if (state.user && action.payload?.status) {
|
|
||||||
state.user = { ...state.user, status: action.payload.status };
|
|
||||||
}
|
|
||||||
state.isLoading = false;
|
|
||||||
state.error = null;
|
state.error = null;
|
||||||
})
|
})
|
||||||
|
.addCase(updateProfile.fulfilled, (state, action) => {
|
||||||
|
state.isLoading = false;
|
||||||
|
state.user = action.payload.user;
|
||||||
|
})
|
||||||
|
.addCase(updateProfile.rejected, (state, action) => {
|
||||||
|
state.isLoading = false;
|
||||||
|
state.error = (action.payload as string) || 'Profile update failed';
|
||||||
|
})
|
||||||
|
|
||||||
// Logout — clear all auth state
|
// ── logoutUser ─────────────────────────────────────────────────────────
|
||||||
.addCase(logoutUser.fulfilled, state => {
|
.addCase(logoutUser.fulfilled, state => {
|
||||||
state.user = null;
|
state.user = null;
|
||||||
state.accessToken = null;
|
state.isAuthenticated = false;
|
||||||
state.refreshToken = null;
|
state.onboardingComplete = false;
|
||||||
state.loginData = null;
|
|
||||||
state.isNewUser = null;
|
|
||||||
state.isLoading = false;
|
|
||||||
state.error = null;
|
state.error = null;
|
||||||
})
|
})
|
||||||
.addCase(logoutUser.rejected, state => {
|
.addCase(logoutUser.rejected, state => {
|
||||||
// Even on failure, clear local auth state (tokens already cleared by thunk)
|
// Still clear state even if API fails (tokens already cleared in thunk)
|
||||||
state.user = null;
|
state.user = null;
|
||||||
state.accessToken = null;
|
state.isAuthenticated = false;
|
||||||
state.refreshToken = null;
|
state.onboardingComplete = false;
|
||||||
state.loginData = null;
|
state.error = null;
|
||||||
state.isNewUser = null;
|
|
||||||
state.isLoading = false;
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@ -1,23 +1,18 @@
|
|||||||
import { createAsyncThunk } from '@reduxjs/toolkit';
|
import { createAsyncThunk } from '@reduxjs/toolkit';
|
||||||
import {
|
import { loginApi, verifyOtpApi, updateProfileApi, logoutApi } from '../../../api/authApi';
|
||||||
loginApi,
|
|
||||||
verifyOtpApi,
|
|
||||||
updateProfileApi,
|
|
||||||
logoutApi,
|
|
||||||
} from '../../../api/authApi';
|
|
||||||
import { tokenManager } from '../../../services/apiClient';
|
import { tokenManager } from '../../../services/apiClient';
|
||||||
|
|
||||||
// ─── Login ───────────────────────────────────────────────────────────────────
|
// ─── Login ───────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
export const loginWithPhone = createAsyncThunk(
|
export const loginWithPhone = createAsyncThunk(
|
||||||
'auth/loginWithPhone',
|
'auth/loginWithPhone',
|
||||||
async (phone: string, { rejectWithValue }) => {
|
async (mobileNumber: string, { rejectWithValue }) => {
|
||||||
try {
|
try {
|
||||||
const response = await loginApi(phone);
|
const response = await loginApi(mobileNumber);
|
||||||
console.log(response);
|
|
||||||
return response;
|
return response;
|
||||||
} catch (error: unknown) {
|
} catch (error: unknown) {
|
||||||
const message = error instanceof Error ? error.message : 'Login failed';
|
const message =
|
||||||
|
error instanceof Error ? error.message : 'Login failed';
|
||||||
return rejectWithValue(message);
|
return rejectWithValue(message);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@ -28,11 +23,11 @@ export const loginWithPhone = createAsyncThunk(
|
|||||||
export const verifyOtp = createAsyncThunk(
|
export const verifyOtp = createAsyncThunk(
|
||||||
'auth/verifyOtp',
|
'auth/verifyOtp',
|
||||||
async (
|
async (
|
||||||
{ phone, code, role }: { phone: string; code: string; role: string },
|
{ mobileNumber, otp }: { mobileNumber: string; otp: string },
|
||||||
{ rejectWithValue },
|
{ rejectWithValue },
|
||||||
) => {
|
) => {
|
||||||
try {
|
try {
|
||||||
const response = await verifyOtpApi(phone, code, role);
|
const response = await verifyOtpApi(mobileNumber, otp);
|
||||||
|
|
||||||
// Persist tokens on successful verification
|
// Persist tokens on successful verification
|
||||||
if (response.accessToken && response.refreshToken) {
|
if (response.accessToken && response.refreshToken) {
|
||||||
@ -82,7 +77,8 @@ export const logoutUser = createAsyncThunk(
|
|||||||
} catch (error: unknown) {
|
} catch (error: unknown) {
|
||||||
// Clear tokens even if API call fails
|
// Clear tokens even if API call fails
|
||||||
await tokenManager.clearTokens();
|
await tokenManager.clearTokens();
|
||||||
const message = error instanceof Error ? error.message : 'Logout failed';
|
const message =
|
||||||
|
error instanceof Error ? error.message : 'Logout failed';
|
||||||
return rejectWithValue(message);
|
return rejectWithValue(message);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|||||||
@ -1,10 +1,10 @@
|
|||||||
export { default as cartReducer } from './reducer';
|
export { default as cartReducer } from './reducer';
|
||||||
export {
|
export {
|
||||||
|
addItem,
|
||||||
|
removeItem,
|
||||||
|
clearCart,
|
||||||
|
applyCoupon,
|
||||||
|
removeCoupon,
|
||||||
selectCartTotal,
|
selectCartTotal,
|
||||||
} from './reducer';
|
} from './reducer';
|
||||||
export {
|
|
||||||
addToCartThunk,
|
|
||||||
getCartThunk,
|
|
||||||
updateCartItemThunk,
|
|
||||||
} from './thunk';
|
|
||||||
export type { CartState } from './reducer';
|
export type { CartState } from './reducer';
|
||||||
|
|||||||
@ -1,45 +1,54 @@
|
|||||||
import { createReducer } from '@reduxjs/toolkit';
|
import { createAction, createReducer } from '@reduxjs/toolkit';
|
||||||
import { CartItem, MerchantCartGroup } from '@interfaces/cart';
|
import { CartItem, CatalogItem } from '../../../interfaces';
|
||||||
import { addToCartThunk, getCartThunk, updateCartItemThunk } from './thunk';
|
|
||||||
|
|
||||||
// ─── State ───────────────────────────────────────────────────────────────────
|
// ─── State ───────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
export interface CartState {
|
export interface CartState {
|
||||||
id: string | null;
|
|
||||||
customerId: string | null;
|
|
||||||
items: CartItem[];
|
items: CartItem[];
|
||||||
groupedByMerchant: MerchantCartGroup[];
|
couponCode: string | null;
|
||||||
subtotal: number;
|
discount: number;
|
||||||
totalItems: number;
|
providerId: string | null;
|
||||||
isLoading: boolean;
|
providerName: string | null;
|
||||||
error: string | null;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const initialState: CartState = {
|
const initialState: CartState = {
|
||||||
id: null,
|
|
||||||
customerId: null,
|
|
||||||
items: [],
|
items: [],
|
||||||
groupedByMerchant: [],
|
couponCode: null,
|
||||||
subtotal: 0,
|
discount: 0,
|
||||||
totalItems: 0,
|
providerId: null,
|
||||||
isLoading: false,
|
providerName: null,
|
||||||
error: null,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// ─── Actions ─────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
export const addItem = createAction<{
|
||||||
|
providerId: string;
|
||||||
|
providerName: string;
|
||||||
|
item: CatalogItem;
|
||||||
|
}>('cart/addItem');
|
||||||
|
|
||||||
|
export const removeItem = createAction<string>('cart/removeItem');
|
||||||
|
export const clearCart = createAction('cart/clearCart');
|
||||||
|
export const applyCoupon = createAction<string>('cart/applyCoupon');
|
||||||
|
export const removeCoupon = createAction('cart/removeCoupon');
|
||||||
|
|
||||||
// ─── Selector ────────────────────────────────────────────────────────────────
|
// ─── Selector ────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
export const selectCartTotal = (state: { cart: CartState }) => {
|
export const selectCartTotal = (state: { cart: CartState }) => {
|
||||||
const subtotal = state.cart.subtotal || 0;
|
const subtotal = state.cart.items.reduce(
|
||||||
const deliveryFee = 40; // Flat fee for now
|
(sum, item) => sum + item.item.price * item.quantity,
|
||||||
const platformFee = 10; // Flat fee for now
|
0,
|
||||||
const discount = 0; // Removing discount logic for now as requested
|
);
|
||||||
|
const deliveryFee = 40;
|
||||||
|
const platformFee = 10;
|
||||||
|
const discount = state.cart.discount;
|
||||||
return {
|
return {
|
||||||
subtotal,
|
subtotal,
|
||||||
deliveryFee,
|
deliveryFee,
|
||||||
platformFee,
|
platformFee,
|
||||||
discount,
|
discount,
|
||||||
total: Math.max(0, subtotal + deliveryFee + platformFee - discount),
|
total: Math.max(0, subtotal + deliveryFee + platformFee - discount),
|
||||||
itemCount: state.cart.totalItems || 0,
|
itemCount: state.cart.items.reduce((sum, item) => sum + item.quantity, 0),
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -47,64 +56,58 @@ export const selectCartTotal = (state: { cart: CartState }) => {
|
|||||||
|
|
||||||
const cartReducer = createReducer(initialState, builder => {
|
const cartReducer = createReducer(initialState, builder => {
|
||||||
builder
|
builder
|
||||||
// getCartThunk
|
.addCase(addItem, (state, action) => {
|
||||||
.addCase(getCartThunk.pending, state => {
|
const { providerId, providerName, item } = action.payload;
|
||||||
state.isLoading = true;
|
|
||||||
state.error = null;
|
|
||||||
})
|
|
||||||
.addCase(getCartThunk.fulfilled, (state, action) => {
|
|
||||||
state.isLoading = false;
|
|
||||||
const cart = action.payload;
|
|
||||||
state.id = cart.id;
|
|
||||||
state.customerId = cart.customerId;
|
|
||||||
state.items = cart.items || [];
|
|
||||||
state.groupedByMerchant = cart.groupedByMerchant || [];
|
|
||||||
state.subtotal = cart.subtotal || 0;
|
|
||||||
state.totalItems = cart.totalItems || 0;
|
|
||||||
})
|
|
||||||
.addCase(getCartThunk.rejected, (state, action) => {
|
|
||||||
state.isLoading = false;
|
|
||||||
state.error = (action.payload as string) || 'Failed to get cart';
|
|
||||||
})
|
|
||||||
|
|
||||||
// addToCartThunk
|
// Clear cart if switching providers
|
||||||
.addCase(addToCartThunk.pending, state => {
|
if (state.providerId && state.providerId !== providerId) {
|
||||||
state.isLoading = true;
|
state.items = [];
|
||||||
state.error = null;
|
}
|
||||||
})
|
|
||||||
.addCase(addToCartThunk.fulfilled, (state, action) => {
|
|
||||||
state.isLoading = false;
|
|
||||||
const cart = action.payload;
|
|
||||||
state.id = cart.id;
|
|
||||||
state.customerId = cart.customerId;
|
|
||||||
state.items = cart.items || [];
|
|
||||||
state.groupedByMerchant = cart.groupedByMerchant || [];
|
|
||||||
state.subtotal = cart.subtotal || 0;
|
|
||||||
state.totalItems = cart.totalItems || 0;
|
|
||||||
})
|
|
||||||
.addCase(addToCartThunk.rejected, (state, action) => {
|
|
||||||
state.isLoading = false;
|
|
||||||
state.error = (action.payload as string) || 'Failed to add to cart';
|
|
||||||
})
|
|
||||||
|
|
||||||
// updateCartItemThunk
|
state.providerId = providerId;
|
||||||
.addCase(updateCartItemThunk.pending, state => {
|
state.providerName = providerName;
|
||||||
state.isLoading = true;
|
|
||||||
state.error = null;
|
const existing = state.items.find(i => i.item.id === item.id);
|
||||||
|
if (existing) {
|
||||||
|
existing.quantity += 1;
|
||||||
|
} else {
|
||||||
|
state.items.push({
|
||||||
|
id: `${providerId}-${item.id}`,
|
||||||
|
providerId,
|
||||||
|
providerName,
|
||||||
|
item,
|
||||||
|
quantity: 1,
|
||||||
|
});
|
||||||
|
}
|
||||||
})
|
})
|
||||||
.addCase(updateCartItemThunk.fulfilled, (state, action) => {
|
.addCase(removeItem, (state, action) => {
|
||||||
state.isLoading = false;
|
const existing = state.items.find(i => i.item.id === action.payload);
|
||||||
const cart = action.payload;
|
if (existing) {
|
||||||
state.id = cart.id;
|
if (existing.quantity > 1) {
|
||||||
state.customerId = cart.customerId;
|
existing.quantity -= 1;
|
||||||
state.items = cart.items || [];
|
} else {
|
||||||
state.groupedByMerchant = cart.groupedByMerchant || [];
|
state.items = state.items.filter(i => i.item.id !== action.payload);
|
||||||
state.subtotal = cart.subtotal || 0;
|
}
|
||||||
state.totalItems = cart.totalItems || 0;
|
}
|
||||||
|
if (state.items.length === 0) {
|
||||||
|
state.providerId = null;
|
||||||
|
state.providerName = null;
|
||||||
|
}
|
||||||
})
|
})
|
||||||
.addCase(updateCartItemThunk.rejected, (state, action) => {
|
.addCase(clearCart, state => {
|
||||||
state.isLoading = false;
|
state.items = [];
|
||||||
state.error = (action.payload as string) || 'Failed to update cart item';
|
state.couponCode = null;
|
||||||
|
state.discount = 0;
|
||||||
|
state.providerId = null;
|
||||||
|
state.providerName = null;
|
||||||
|
})
|
||||||
|
.addCase(applyCoupon, (state, action) => {
|
||||||
|
state.couponCode = action.payload;
|
||||||
|
state.discount = 50;
|
||||||
|
})
|
||||||
|
.addCase(removeCoupon, state => {
|
||||||
|
state.couponCode = null;
|
||||||
|
state.discount = 0;
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@ -1,48 +1,2 @@
|
|||||||
import { addToCartApi, getCartApi, updateCartItem } from '@api';
|
// Cart thunks — placeholder for future async cart operations
|
||||||
import { AddToCartRequest, CartResponse } from '@interfaces/cart';
|
// (e.g., syncing cart with backend, applying server-side coupons)
|
||||||
import { createAsyncThunk } from '@reduxjs/toolkit';
|
|
||||||
|
|
||||||
export const addToCartThunk = createAsyncThunk<CartResponse, AddToCartRequest>(
|
|
||||||
'cart/addToCart',
|
|
||||||
async (addToCartRequest, { rejectWithValue }) => {
|
|
||||||
try {
|
|
||||||
const response = await addToCartApi(addToCartRequest);
|
|
||||||
return response;
|
|
||||||
} catch (error: any) {
|
|
||||||
const message =
|
|
||||||
error instanceof Error ? error.message : 'Failed to add to cart';
|
|
||||||
return rejectWithValue(message);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
export const getCartThunk = createAsyncThunk<CartResponse>(
|
|
||||||
'cart/getCart',
|
|
||||||
async (_, { rejectWithValue }) => {
|
|
||||||
try {
|
|
||||||
const response = await getCartApi();
|
|
||||||
return response;
|
|
||||||
} catch (error: any) {
|
|
||||||
const message =
|
|
||||||
error instanceof Error ? error.message : 'Failed to get cart';
|
|
||||||
return rejectWithValue(message);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
export const updateCartItemThunk = createAsyncThunk<
|
|
||||||
CartResponse,
|
|
||||||
{ productId: string; quantity: number }
|
|
||||||
>(
|
|
||||||
'cart/updateCartItem',
|
|
||||||
async ({ productId, quantity }, { rejectWithValue }) => {
|
|
||||||
try {
|
|
||||||
const response = await updateCartItem(productId, quantity);
|
|
||||||
return response;
|
|
||||||
} catch (error: any) {
|
|
||||||
const message =
|
|
||||||
error instanceof Error ? error.message : 'Failed to update cart item';
|
|
||||||
return rejectWithValue(message);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|||||||
@ -1,2 +0,0 @@
|
|||||||
export * from './thunk';
|
|
||||||
export * from './reducer';
|
|
||||||
@ -1,32 +0,0 @@
|
|||||||
import { CustomerResponse } from '@interfaces';
|
|
||||||
import { createReducer } from '@reduxjs/toolkit';
|
|
||||||
import { fetchCustomerDetails } from './thunk';
|
|
||||||
|
|
||||||
export interface CustomerProfileState {
|
|
||||||
customerDetails: CustomerResponse | null;
|
|
||||||
error: string | null;
|
|
||||||
isLoading: boolean;
|
|
||||||
}
|
|
||||||
|
|
||||||
const initialState: CustomerProfileState = {
|
|
||||||
customerDetails: null,
|
|
||||||
error: null,
|
|
||||||
isLoading: false,
|
|
||||||
};
|
|
||||||
|
|
||||||
const customerProfileReducer = createReducer(initialState, builder => {
|
|
||||||
builder
|
|
||||||
.addCase(fetchCustomerDetails.pending, state => {
|
|
||||||
state.isLoading = true;
|
|
||||||
state.error = null;
|
|
||||||
})
|
|
||||||
.addCase(fetchCustomerDetails.fulfilled, (state, action) => {
|
|
||||||
state.isLoading = false;
|
|
||||||
state.customerDetails = action.payload;
|
|
||||||
})
|
|
||||||
.addCase(fetchCustomerDetails.rejected, (state, action) => {
|
|
||||||
state.isLoading = false;
|
|
||||||
state.error = action.payload as string;
|
|
||||||
});
|
|
||||||
});
|
|
||||||
export default customerProfileReducer;
|
|
||||||
@ -1,17 +0,0 @@
|
|||||||
import { getCustomerDetails } from '@api';
|
|
||||||
import { CustomerResponse } from '@interfaces';
|
|
||||||
import { createAsyncThunk } from '@reduxjs/toolkit';
|
|
||||||
|
|
||||||
export const fetchCustomerDetails = createAsyncThunk<CustomerResponse>(
|
|
||||||
'customer/fetchCustomerDetails',
|
|
||||||
async (_, { rejectWithValue }) => {
|
|
||||||
try {
|
|
||||||
const response = await getCustomerDetails();
|
|
||||||
console.log(response);
|
|
||||||
return response;
|
|
||||||
} catch (error: any) {
|
|
||||||
console.log(error);
|
|
||||||
return rejectWithValue(error?.message);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
);
|
|
||||||
@ -1,4 +1,3 @@
|
|||||||
export * from './auth';
|
export * from './auth';
|
||||||
export * from './cart';
|
export * from './cart';
|
||||||
export * from './order';
|
export * from './order';
|
||||||
export * from './customerProfile';
|
|
||||||
|
|||||||
@ -1,27 +1,12 @@
|
|||||||
import { combineReducers } from '@reduxjs/toolkit';
|
import { combineReducers } from '@reduxjs/toolkit';
|
||||||
|
import { authReducer } from './commonreducers/auth';
|
||||||
import { cartReducer } from './commonreducers/cart';
|
import { cartReducer } from './commonreducers/cart';
|
||||||
import { orderReducer } from './commonreducers/order';
|
import { orderReducer } from './commonreducers/order';
|
||||||
import preferencesReducer from '@features/screens/preferencesScreen/reducer';
|
|
||||||
import setLocationReducer from '@features/screens/setLocationScreen/reducer';
|
|
||||||
import completeProfileReducer from '@features/screens/completeProfileScreen/reducer';
|
|
||||||
import authReducer from './commonreducers/auth/reducer';
|
|
||||||
import homeReducer from '@features/screens/homeScreen/reducer';
|
|
||||||
import providerDetailsReducer from '@features/screens/providerDetailsScreen/reducer';
|
|
||||||
import paymentMethodsReducer from '@features/screens/checkoutPaymentScreen/reducer';
|
|
||||||
import customerProfileReducer from './commonreducers/customerProfile/reducer';
|
|
||||||
|
|
||||||
const rootReducer = combineReducers({
|
const rootReducer = combineReducers({
|
||||||
auth: authReducer,
|
auth: authReducer,
|
||||||
cart: cartReducer,
|
cart: cartReducer,
|
||||||
order: orderReducer,
|
order: orderReducer,
|
||||||
preferences: preferencesReducer,
|
|
||||||
setLocation: setLocationReducer,
|
|
||||||
completeProfile: completeProfileReducer,
|
|
||||||
home: homeReducer,
|
|
||||||
providerDetails: providerDetailsReducer,
|
|
||||||
paymentMethods: paymentMethodsReducer,
|
|
||||||
customerProfile: customerProfileReducer,
|
|
||||||
});
|
});
|
||||||
|
|
||||||
export type RootState = ReturnType<typeof rootReducer>;
|
export type RootState = ReturnType<typeof rootReducer>;
|
||||||
|
|||||||
@ -1,31 +0,0 @@
|
|||||||
export const getFullUrl = (url?: string) => {
|
|
||||||
const BASE_URL = 'https://db1d-202-8-116-13.ngrok-free.app';
|
|
||||||
if (!url) return '';
|
|
||||||
return url.startsWith('/') ? `${BASE_URL}${url}` : url;
|
|
||||||
};
|
|
||||||
|
|
||||||
export const formatTime = (dateString?: string) => {
|
|
||||||
if (!dateString) return '';
|
|
||||||
|
|
||||||
const dateObj = new Date(dateString);
|
|
||||||
|
|
||||||
// Example output: 11:57 AM
|
|
||||||
return dateObj.toLocaleTimeString('en-US', {
|
|
||||||
hour: 'numeric',
|
|
||||||
minute: '2-digit',
|
|
||||||
hour12: true,
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
export const formatDate = (dateString?: string) => {
|
|
||||||
if (!dateString) return '';
|
|
||||||
|
|
||||||
const dateObj = new Date(dateString);
|
|
||||||
|
|
||||||
// Example output: July 8, 2026
|
|
||||||
return dateObj.toLocaleDateString('en-US', {
|
|
||||||
month: 'long',
|
|
||||||
day: 'numeric',
|
|
||||||
year: 'numeric',
|
|
||||||
});
|
|
||||||
};
|
|
||||||
@ -1 +0,0 @@
|
|||||||
export * from './helper';
|
|
||||||
@ -9,12 +9,6 @@ module.exports = {
|
|||||||
'@components': './app/components',
|
'@components': './app/components',
|
||||||
'@features/screens': './app/features/screens',
|
'@features/screens': './app/features/screens',
|
||||||
'@theme': './app/theme',
|
'@theme': './app/theme',
|
||||||
'@hooks': './app/hooks',
|
|
||||||
'@navigation': './app/navigation',
|
|
||||||
'@services': './app/services',
|
|
||||||
'@store': './app/store',
|
|
||||||
'@api': './app/api',
|
|
||||||
'@utils': './app/utils',
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
|
|||||||
4
index.js
4
index.js
@ -2,8 +2,4 @@ import { AppRegistry } from 'react-native';
|
|||||||
import App from './app/App';
|
import App from './app/App';
|
||||||
import { name as appName } from './app.json';
|
import { name as appName } from './app.json';
|
||||||
|
|
||||||
if (__DEV__) {
|
|
||||||
require('./ReactotronConfig');
|
|
||||||
}
|
|
||||||
|
|
||||||
AppRegistry.registerComponent(appName, () => App);
|
AppRegistry.registerComponent(appName, () => App);
|
||||||
|
|||||||
@ -27,10 +27,8 @@
|
|||||||
"react-native-safe-area-context": "^5.8.0",
|
"react-native-safe-area-context": "^5.8.0",
|
||||||
"react-native-screens": "^4.25.2",
|
"react-native-screens": "^4.25.2",
|
||||||
"react-native-svg": "^15.15.5",
|
"react-native-svg": "^15.15.5",
|
||||||
"react-native-uuid": "^2.0.4",
|
|
||||||
"react-native-worklets": "^0.10.0",
|
"react-native-worklets": "^0.10.0",
|
||||||
"react-redux": "^9.3.0",
|
"react-redux": "^9.3.0",
|
||||||
"reactotron-react-native": "^5.2.0",
|
|
||||||
"redux-persist": "^6.0.0",
|
"redux-persist": "^6.0.0",
|
||||||
"redux-thunk": "^3.1.0"
|
"redux-thunk": "^3.1.0"
|
||||||
},
|
},
|
||||||
|
|||||||
@ -9,21 +9,7 @@
|
|||||||
"@features/screens": ["./app/features/screens"],
|
"@features/screens": ["./app/features/screens"],
|
||||||
"@features/screens/*": ["./app/features/screens/*"],
|
"@features/screens/*": ["./app/features/screens/*"],
|
||||||
"@theme": ["./app/theme"],
|
"@theme": ["./app/theme"],
|
||||||
"@theme/*": ["./app/theme/*"],
|
"@theme/*": ["./app/theme/*"]
|
||||||
"@api": ["./app/api"],
|
|
||||||
"@api/*": ["./app/api/*"],
|
|
||||||
"@store": ["./app/store"],
|
|
||||||
"@store/*": ["./app/store/*"],
|
|
||||||
"@interfaces": ["./app/interfaces"],
|
|
||||||
"@interfaces/*": ["./app/interfaces/*"],
|
|
||||||
"@navigation": ["./app/navigation"],
|
|
||||||
"@navigation/*": ["./app/navigation/*"],
|
|
||||||
"@hooks": ["./app/hooks"],
|
|
||||||
"@hooks/*": ["./app/hooks/*"],
|
|
||||||
"@utils": ["./app/utils"],
|
|
||||||
"@utils/*": ["./app/utils/*"],
|
|
||||||
"@services": ["./app/services"],
|
|
||||||
"@services/*": ["./app/services/*"]
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"include": ["**/*.ts", "**/*.tsx"],
|
"include": ["**/*.ts", "**/*.tsx"],
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Loading…
x
Reference in New Issue
Block a user