freshyo/apps/user-ui/components/checkout-page.tsx
2026-01-24 00:13:15 +05:30

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;