273 lines
No EOL
9.4 KiB
TypeScript
273 lines
No EOL
9.4 KiB
TypeScript
import React, { useState, useMemo } from 'react';
|
|
import { View, ScrollView } from 'react-native';
|
|
import { useLocalSearchParams, useRouter } from 'expo-router';
|
|
import { tw, useMarkDataFetchers , BottomDialog, MyText, MyTouchableOpacity } from 'common-ui';
|
|
import MaterialIcons from '@expo/vector-icons/MaterialIcons';
|
|
import { useQueryClient } from '@tanstack/react-query';
|
|
import AddressForm from '@/src/components/AddressForm';
|
|
import { useAuthenticatedRoute } from '@/hooks/useAuthenticatedRoute';
|
|
|
|
import { trpc } from '@/src/trpc-client';
|
|
import { useGetCart } from '@/hooks/cart-query-hooks';
|
|
import { useGetEssentialConsts } from '@/src/api-hooks/essential-consts.api';
|
|
import PaymentAndOrderComponent from '@/components/PaymentAndOrderComponent';
|
|
import CheckoutAddressSelector from '@/components/CheckoutAddressSelector';
|
|
import { useAddressStore } from '@/src/store/addressStore';
|
|
|
|
interface CheckoutPageProps {
|
|
isFlashDelivery?: boolean;
|
|
}
|
|
|
|
const CheckoutPage: React.FC<CheckoutPageProps> = ({ isFlashDelivery = false }) => {
|
|
const params = useLocalSearchParams();
|
|
const queryClient = useQueryClient();
|
|
const router = useRouter();
|
|
|
|
// Protect checkout route and preserve query params
|
|
useAuthenticatedRoute({
|
|
targetUrl: isFlashDelivery ? '/(drawer)/(tabs)/flash-delivery/checkout' : '/(drawer)/(tabs)/home/checkout',
|
|
queryParams: params
|
|
});
|
|
|
|
const cartType: "regular" | "flash" = isFlashDelivery ? "flash" : "regular";
|
|
const { data: cartData, refetch: refetchCart } = useGetCart({}, cartType);
|
|
console.log({cartType})
|
|
|
|
const { data: addresses, refetch: refetchAddresses } = trpc.user.address.getUserAddresses.useQuery();
|
|
const { data: slotsData, refetch: refetchSlots } = trpc.user.slots.getSlots.useQuery();
|
|
const { data: constsData } = useGetEssentialConsts();
|
|
const { data: productsData } = trpc.user.product.getAllProductsSummary.useQuery();
|
|
|
|
useMarkDataFetchers(() => {
|
|
refetchCart();
|
|
refetchAddresses();
|
|
refetchSlots();
|
|
});
|
|
|
|
const { selectedAddressId, setSelectedAddressId } = useAddressStore();
|
|
const [showAddAddress, setShowAddAddress] = useState(false);
|
|
const [selectedCouponId, setSelectedCouponId] = useState<number | null>(null);
|
|
|
|
|
|
|
|
const cartItems = cartData?.items || [];
|
|
|
|
// Memoized flash-eligible product IDs
|
|
const flashEligibleProductIds = useMemo(() => {
|
|
if (!productsData) return new Set<number>();
|
|
return new Set(
|
|
productsData
|
|
.filter((product: any) => product.isFlashAvailable)
|
|
.map((product: any) => product.id)
|
|
);
|
|
}, [productsData]);
|
|
|
|
// Parse slots parameter from URL (format: "1:1,2,3;2:4,5")
|
|
const selectedSlots = useMemo(() => {
|
|
const slots: Record<number, number> = {};
|
|
if (params.slots) {
|
|
const slotGroups = (params.slots as string).split(';');
|
|
slotGroups.forEach(group => {
|
|
const [slotIdStr, itemIdsStr] = group.split(':');
|
|
const slotId = Number(slotIdStr);
|
|
const itemIds = itemIdsStr.split(',').map(Number);
|
|
itemIds.forEach(itemId => {
|
|
slots[itemId] = slotId;
|
|
});
|
|
});
|
|
}
|
|
return slots;
|
|
}, [params.slots]);
|
|
|
|
const selectedItems = cartItems.filter(item => {
|
|
// For flash delivery, check if product supports flash delivery
|
|
if (isFlashDelivery) {
|
|
return flashEligibleProductIds.has(item.productId);
|
|
}
|
|
// For regular delivery, only include items with assigned slots
|
|
return selectedSlots[item.id];
|
|
});
|
|
|
|
React.useEffect(() => {
|
|
if (params.coupons) {
|
|
const couponId = Number(params.coupons as string);
|
|
setSelectedCouponId(couponId);
|
|
}
|
|
}, [params.coupons]);
|
|
|
|
// Handle empty cart case
|
|
if (selectedItems.length === 0) {
|
|
return (
|
|
<View style={tw`flex-1 bg-gray-50 items-center justify-center p-6`}>
|
|
<MaterialIcons name="shopping-cart" size={64} color="#9CA3AF" />
|
|
<MyText style={tw`text-xl font-semibold text-gray-900 mt-4 text-center`}>
|
|
{cartItems.length === 0 ? "Your cart is empty" : "No items to checkout"}
|
|
</MyText>
|
|
<MyText style={tw`text-gray-500 text-center mt-2 mb-6`}>
|
|
{cartItems.length === 0
|
|
? "Add some delicious items to your cart before checking out"
|
|
: isFlashDelivery
|
|
? "None of your cart items are available for flash delivery"
|
|
: "Please select delivery slots for your items"
|
|
}
|
|
</MyText>
|
|
<MyTouchableOpacity
|
|
style={tw`bg-brand500 py-3 px-6 rounded-lg`}
|
|
onPress={() => router.back()}
|
|
>
|
|
<MyText style={tw`text-white font-semibold`}>Back to Shopping</MyText>
|
|
</MyTouchableOpacity>
|
|
</View>
|
|
);
|
|
}
|
|
|
|
|
|
|
|
const totalPrice = selectedItems
|
|
.filter((item) => !item.product?.isOutOfStock)
|
|
.reduce(
|
|
(sum, item) => {
|
|
const price = isFlashDelivery ? (item.product?.flashPrice ?? item.product?.price ?? 0) : (item.product?.price || 0);
|
|
return sum + price * item.quantity;
|
|
},
|
|
0
|
|
);
|
|
|
|
const { data: couponsRaw } = trpc.user.coupon.getEligible.useQuery();
|
|
|
|
const eligibleCoupons = useMemo(() => {
|
|
if (!couponsRaw?.data) return [];
|
|
return couponsRaw.data.map(coupon => {
|
|
let isEligible = true;
|
|
let ineligibilityReason = '';
|
|
if (coupon.maxLimitForUser && coupon.usages.length >= coupon.maxLimitForUser) {
|
|
isEligible = false;
|
|
ineligibilityReason = 'Usage limit exceeded';
|
|
}
|
|
if (coupon.minOrder && parseFloat(coupon.minOrder) > totalPrice) {
|
|
isEligible = false;
|
|
ineligibilityReason = `Min order ₹${coupon.minOrder}`;
|
|
}
|
|
return {
|
|
id: coupon.id,
|
|
code: coupon.couponCode,
|
|
discountType: coupon.discountPercent ? 'percentage' : 'flat',
|
|
discountValue: parseFloat(coupon.discountPercent || coupon.flatDiscount || '0'),
|
|
maxValue: coupon.maxValue ? parseFloat(coupon.maxValue) : undefined,
|
|
minOrder: coupon.minOrder ? parseFloat(coupon.minOrder) : undefined,
|
|
description: '',
|
|
exclusiveApply: coupon.exclusiveApply,
|
|
isEligible,
|
|
ineligibilityReason: isEligible ? undefined : ineligibilityReason,
|
|
};
|
|
}).filter(coupon => coupon.ineligibilityReason !== 'Usage limit exceeded');
|
|
}, [couponsRaw, totalPrice]);
|
|
|
|
const selectedCoupons = useMemo(
|
|
() =>
|
|
selectedCouponId ? eligibleCoupons?.filter((coupon) => coupon.id === selectedCouponId) : [],
|
|
[eligibleCoupons, selectedCouponId]
|
|
);
|
|
|
|
const discountAmount = useMemo(
|
|
() =>
|
|
selectedCoupons?.reduce(
|
|
(sum, coupon) =>
|
|
sum +
|
|
(coupon.discountType === "percentage"
|
|
? Math.min(
|
|
(totalPrice * coupon.discountValue) / 100,
|
|
coupon.maxValue || Infinity
|
|
)
|
|
: Math.min(coupon.discountValue, coupon.maxValue || totalPrice)),
|
|
0
|
|
) || 0,
|
|
[selectedCoupons, totalPrice]
|
|
);
|
|
|
|
const finalTotal = totalPrice - discountAmount;
|
|
|
|
const deliveryCharge = useMemo(() => {
|
|
const threshold = isFlashDelivery
|
|
? constsData?.flashFreeDeliveryThreshold
|
|
: constsData?.freeDeliveryThreshold;
|
|
const charge = isFlashDelivery
|
|
? constsData?.flashDeliveryCharge
|
|
: constsData?.deliveryCharge;
|
|
return finalTotal < threshold ? charge : 0;
|
|
}, [finalTotal, constsData, isFlashDelivery]);
|
|
|
|
const finalTotalWithDelivery = finalTotal + deliveryCharge;
|
|
|
|
|
|
|
|
|
|
return (
|
|
<View style={tw`flex-1 bg-gray-50`}>
|
|
|
|
{/* Checkout Type Header */}
|
|
<View style={tw`bg-white px-4 py-3 mb-4 border-b border-gray-100 mt-6`}>
|
|
<View style={tw`flex-row items-center`}>
|
|
<MyTouchableOpacity
|
|
onPress={() => router.back()}
|
|
style={tw`p-2 -ml-2 mr-1`}
|
|
activeOpacity={0.7}
|
|
>
|
|
<MaterialIcons name="chevron-left" size={28} color="#374151" />
|
|
</MyTouchableOpacity>
|
|
<MaterialIcons
|
|
name={isFlashDelivery ? "flash-on" : "schedule"}
|
|
size={24}
|
|
color={isFlashDelivery ? "#f81260" : "#374151"}
|
|
/>
|
|
<MyText style={[
|
|
tw`text-lg font-bold ml-2`,
|
|
{ color: isFlashDelivery ? '#f81260' : '#374151' }
|
|
]}>
|
|
{isFlashDelivery ? "Flash Delivery Checkout" : "Scheduled Delivery Checkout"}
|
|
</MyText>
|
|
</View>
|
|
</View>
|
|
|
|
<ScrollView
|
|
style={tw`flex-1`}
|
|
contentContainerStyle={tw`p-4 pb-32`}
|
|
showsVerticalScrollIndicator={false}
|
|
>
|
|
<CheckoutAddressSelector
|
|
selectedAddress={selectedAddressId}
|
|
onAddressSelect={setSelectedAddressId}
|
|
/>
|
|
|
|
<PaymentAndOrderComponent
|
|
selectedAddress={selectedAddressId}
|
|
selectedSlots={selectedSlots}
|
|
selectedCouponId={selectedCouponId}
|
|
cartItems={selectedItems}
|
|
totalPrice={totalPrice}
|
|
discountAmount={discountAmount}
|
|
finalTotal={finalTotal}
|
|
finalTotalWithDelivery={finalTotalWithDelivery}
|
|
deliveryCharge={deliveryCharge}
|
|
constsData={constsData}
|
|
selectedCoupons={selectedCoupons}
|
|
isFlashDelivery={isFlashDelivery}
|
|
/>
|
|
</ScrollView>
|
|
|
|
|
|
|
|
<BottomDialog open={showAddAddress} onClose={() => setShowAddAddress(false)}>
|
|
<AddressForm
|
|
onSuccess={() => {
|
|
setShowAddAddress(false);
|
|
queryClient.invalidateQueries();
|
|
}}
|
|
/>
|
|
</BottomDialog>
|
|
|
|
</View>
|
|
);
|
|
};
|
|
|
|
export default CheckoutPage; |