359 lines
14 KiB
TypeScript
359 lines
14 KiB
TypeScript
import React, { useState, useMemo } from 'react';
|
|
import { View, Alert, Platform } from 'react-native';
|
|
import { useRouter } from 'expo-router';
|
|
import { tw, MyTextInput, LoadingDialog, MyText, MyTouchableOpacity } from 'common-ui';
|
|
import MaterialIcons from '@expo/vector-icons/MaterialIcons';
|
|
import { trpc } from '@/src/trpc-client';
|
|
import { useCentralProductStore } from '@/src/store/centralProductStore';
|
|
import { useCentralSlotStore } from '@/src/store/centralSlotStore';
|
|
import { clearLocalCart } from '@/hooks/cart-query-hooks';
|
|
import { useQueryClient } from '@tanstack/react-query';
|
|
import { FontAwesome5, FontAwesome6 } from '@expo/vector-icons';
|
|
|
|
interface PaymentAndOrderProps {
|
|
selectedAddress: number | null;
|
|
selectedSlots: Record<number, number>;
|
|
selectedCouponId: number | null;
|
|
cartItems: any[];
|
|
totalPrice: number;
|
|
totalSavings?: number;
|
|
discountAmount: number;
|
|
finalTotal: number;
|
|
finalTotalWithDelivery: number;
|
|
deliveryCharge: number;
|
|
constsData?: any;
|
|
selectedCoupons: any[];
|
|
isFlashDelivery?: boolean;
|
|
onCancel?: () => void;
|
|
}
|
|
|
|
const PaymentAndOrderComponent: React.FC<PaymentAndOrderProps> = ({
|
|
selectedAddress,
|
|
selectedSlots,
|
|
selectedCouponId,
|
|
cartItems,
|
|
totalPrice,
|
|
totalSavings = 0,
|
|
discountAmount,
|
|
finalTotal,
|
|
finalTotalWithDelivery,
|
|
deliveryCharge,
|
|
constsData,
|
|
selectedCoupons,
|
|
isFlashDelivery = false,
|
|
onCancel,
|
|
}) => {
|
|
|
|
const router = useRouter();
|
|
const queryClient = useQueryClient();
|
|
const [userNotes, setUserNotes] = useState('');
|
|
const [isLoadingDialogOpen, setIsLoadingDialogOpen] = useState(false);
|
|
const [paymentMethod, setPaymentMethod] = useState<'online' | 'cod'>('cod');
|
|
|
|
// Helper function to clear cart and invalidate queries
|
|
const clearCartAndInvalidateQueries = async (cartType: 'regular' | 'flash' = 'regular') => {
|
|
await clearLocalCart(cartType);
|
|
queryClient.invalidateQueries({ queryKey: [`local-cart-${cartType}`] });
|
|
};
|
|
|
|
const products = useCentralProductStore((state) => state.products);
|
|
const productsById = useCentralProductStore((state) => state.productsById);
|
|
const productSlotsMap = useCentralSlotStore((state) => state.productSlotsMap);
|
|
|
|
// Memoized flash-eligible product IDs
|
|
const flashEligibleProductIds = useMemo(() => {
|
|
if (!products.length) return new Set<number>();
|
|
return new Set(
|
|
products
|
|
.filter((product) => productSlotsMap[product.id]?.isFlashAvailable)
|
|
.map((product) => product.id)
|
|
);
|
|
}, [products, productSlotsMap]);
|
|
|
|
const placeOrderMutation = trpc.user.order.placeOrder.useMutation({
|
|
onSuccess: (data) => {
|
|
const orders = data.data; // Now an array of orders
|
|
const firstOrder = orders[0]; // Use first order for payment flow
|
|
|
|
if (!firstOrder.isCod) {
|
|
// createRazorpayOrderMutation.mutate({ orderId: firstOrder.id.toString() });
|
|
} else {
|
|
// Clear cart and navigate to success page
|
|
clearCartAndInvalidateQueries(isFlashDelivery ? 'flash' : 'regular');
|
|
const successPath = isFlashDelivery
|
|
? '/(drawer)/(tabs)/flash-delivery/order-success'
|
|
: './order-success';
|
|
router.replace(`${successPath}?orderId=${firstOrder.id}&totalAmount=${finalTotalWithDelivery}`);
|
|
}
|
|
},
|
|
onError: (error: any) => {
|
|
Alert.alert('Error', error.message || 'Failed to place order');
|
|
},
|
|
onSettled: () => {
|
|
setIsLoadingDialogOpen(false);
|
|
},
|
|
});
|
|
|
|
const verifyPaymentMutation = trpc.user.payment.verifyPayment.useMutation({
|
|
onSuccess: () => {
|
|
const orders = placeOrderMutation.data?.data || [];
|
|
const firstOrder = orders[0];
|
|
// Clear cart and navigate to success page
|
|
clearCartAndInvalidateQueries(isFlashDelivery ? 'flash' : 'regular');
|
|
const successPath = isFlashDelivery
|
|
? '/(drawer)/(tabs)/flash-delivery/order-success'
|
|
: './order-success';
|
|
router.replace(`${successPath}?orderId=${firstOrder?.id}&totalAmount=${finalTotalWithDelivery}`);
|
|
},
|
|
onError: (error: any) => {
|
|
Alert.alert('Error', error.message || 'Payment verification failed');
|
|
},
|
|
});
|
|
|
|
const markPaymentFailedMutation = trpc.user.payment.markPaymentFailed.useMutation();
|
|
|
|
|
|
const handlePlaceOrder = () => {
|
|
if (!selectedAddress) {
|
|
Alert.alert('Error', 'Please select an address');
|
|
return;
|
|
}
|
|
|
|
const availableItems = cartItems
|
|
.filter(item => {
|
|
if (productSlotsMap[item.skuId]?.isOutOfStock) return false;
|
|
// For flash delivery, check if product supports flash delivery
|
|
if (isFlashDelivery) {
|
|
return flashEligibleProductIds.has(item.skuId);
|
|
}
|
|
// For regular delivery, only include items with assigned slots
|
|
return selectedSlots[item.id];
|
|
})
|
|
.map(item => item.id);
|
|
|
|
if (availableItems.length === 0) {
|
|
Alert.alert('Error', 'No valid items to order');
|
|
return;
|
|
}
|
|
|
|
setIsLoadingDialogOpen(true);
|
|
|
|
|
|
const orderData = {
|
|
selectedItems: availableItems.map(itemId => {
|
|
const item = cartItems.find(cartItem => cartItem.id === itemId);
|
|
return {
|
|
skuId: item.skuId,
|
|
quantity: item.quantity,
|
|
slotId: isFlashDelivery ? null : selectedSlots[itemId]
|
|
};
|
|
}),
|
|
addressId: selectedAddress,
|
|
paymentMethod: paymentMethod,
|
|
couponId: selectedCouponId || undefined,
|
|
userNotes: userNotes,
|
|
isFlashDelivery: isFlashDelivery,
|
|
};
|
|
|
|
placeOrderMutation.mutate(orderData);
|
|
};
|
|
|
|
return (
|
|
<>
|
|
{/* Back Button */}
|
|
{onCancel && (
|
|
<View style={tw`bg-white p-4 rounded-2xl shadow-sm mb-4 border border-gray-100`}>
|
|
<MyTouchableOpacity
|
|
onPress={onCancel}
|
|
style={tw`flex-row items-center`}
|
|
>
|
|
<MaterialIcons name="arrow-back" size={20} color="#6B7280" />
|
|
<MyText style={tw`text-gray-600 ml-2 font-medium`}>Back to Cart</MyText>
|
|
</MyTouchableOpacity>
|
|
</View>
|
|
)}
|
|
|
|
{/* Special Instructions */}
|
|
<View style={tw`bg-white p-5 rounded-2xl shadow-sm mb-4 border border-gray-100`}>
|
|
<MyText style={tw`text-base font-bold text-gray-900 mb-3`}>Delivery Instructions</MyText>
|
|
<MyTextInput
|
|
style={tw`border border-gray-200 rounded-xl p-3 min-h-24 text-base bg-gray-50`}
|
|
value={userNotes}
|
|
onChangeText={setUserNotes}
|
|
placeholder="Any special instructions for the delivery partner..."
|
|
multiline
|
|
numberOfLines={3}
|
|
textAlignVertical="top"
|
|
/>
|
|
</View>
|
|
|
|
{/* Payment Method */}
|
|
<View style={tw`bg-white p-5 rounded-2xl shadow-sm mb-4 border border-gray-100`}>
|
|
<MyText style={tw`text-lg font-bold text-gray-900 mb-4`}>Payment Method</MyText>
|
|
|
|
<MyTouchableOpacity
|
|
disabled={true}
|
|
style={tw`flex-row items-center p-4 rounded-xl border mb-3 opacity-50 bg-gray-100`}
|
|
>
|
|
<View style={tw`w-5 h-5 rounded-full border-2 border-gray-300 items-center justify-center mr-3`}>
|
|
{/* No selection dot for disabled state */}
|
|
</View>
|
|
<View style={tw`w-10 h-10 bg-gray-200 rounded-full items-center justify-center mr-3`}>
|
|
<MaterialIcons name="payment" size={20} color="#9CA3AF" />
|
|
</View>
|
|
<View>
|
|
<MyText style={tw`font-bold text-gray-500`}>Pay Online (Coming Soon)</MyText>
|
|
<MyText style={tw`text-xs text-gray-400`}>UPI, Cards, Netbanking</MyText>
|
|
</View>
|
|
</MyTouchableOpacity>
|
|
|
|
<MyTouchableOpacity
|
|
onPress={() => setPaymentMethod('cod')}
|
|
style={tw`flex-row items-center p-4 rounded-xl border ${paymentMethod === 'cod' ? 'border-brand500 bg-blue-50' : 'border-gray-200'}`}
|
|
>
|
|
<View style={tw`w-5 h-5 rounded-full border-2 ${paymentMethod === 'cod' ? 'border-brand500' : 'border-gray-400'} items-center justify-center mr-3`}>
|
|
{paymentMethod === 'cod' && <View style={tw`w-2.5 h-2.5 rounded-full bg-brand500`} />}
|
|
</View>
|
|
<View style={tw`w-10 h-10 bg-green-50 rounded-full items-center justify-center mr-3`}>
|
|
<FontAwesome5 name="rupee-sign" size={20} color="#10B981" />
|
|
</View>
|
|
<View>
|
|
<MyText style={tw`font-bold text-gray-900`}>Cash on Delivery</MyText>
|
|
<MyText style={tw`text-xs text-gray-500`}>Pay when you receive</MyText>
|
|
</View>
|
|
</MyTouchableOpacity>
|
|
|
|
<View style={tw`mt-3 flex-row items-center gap-4`}>
|
|
<View style={tw`flex-row items-center`}>
|
|
<FontAwesome6 name="wallet" size={12} color="#663399" style={tw`mr-1.5`} />
|
|
<MyText style={tw`text-xs text-gray-500`}>UPI accepted during COD</MyText>
|
|
</View>
|
|
<View style={tw`flex-row items-center`}>
|
|
<FontAwesome5 name="money-bill-wave" size={12} color="#10B981" style={tw`mr-1.5`} />
|
|
<MyText style={tw`text-xs text-gray-500`}>Cash payment</MyText>
|
|
</View>
|
|
</View>
|
|
</View>
|
|
|
|
{/* Bill Details */}
|
|
<View style={tw`bg-white p-5 rounded-2xl shadow-sm mb-4 border border-gray-100`}>
|
|
<MyText style={tw`text-lg font-bold text-gray-900 mb-4`}>Bill Details</MyText>
|
|
|
|
{/* Item Total */}
|
|
<View style={tw`flex-row justify-between items-center mb-2`}>
|
|
<MyText style={tw`text-gray-500`}>Item Total</MyText>
|
|
<MyText style={tw`text-gray-900 font-medium`}>₹{Math.trunc(totalPrice)}</MyText>
|
|
</View>
|
|
|
|
{/* Total Savings */}
|
|
{totalSavings > 1 && (
|
|
<View style={tw`flex-row justify-between items-center mb-2`}>
|
|
<MyText style={tw`text-gray-500`}>Total Savings</MyText>
|
|
<MyText style={tw`text-green-600 font-medium`}>₹{Math.trunc(totalSavings)}</MyText>
|
|
</View>
|
|
)}
|
|
|
|
{/* Discount */}
|
|
{discountAmount > 0 && (
|
|
<View style={tw`flex-row justify-between items-center mb-2`}>
|
|
<MyText style={tw`text-gray-500`}>Product Discount</MyText>
|
|
<MyText style={tw`text-green-600 font-medium`}>-₹{Math.trunc(discountAmount)}</MyText>
|
|
</View>
|
|
)}
|
|
|
|
{/* Delivery Fee */}
|
|
<View style={tw`flex-row justify-between items-center mb-2`}>
|
|
<View style={tw`flex-row items-center`}>
|
|
<MyText style={tw`text-gray-500`}>Delivery Fee</MyText>
|
|
<MaterialIcons name="info-outline" size={14} color="#9CA3AF" style={tw`ml-1`} />
|
|
</View>
|
|
<View style={tw`flex-row items-center`}>
|
|
{deliveryCharge === 0 && (
|
|
isFlashDelivery
|
|
? (constsData?.flashDeliveryCharge > 0 && (
|
|
<MyText style={tw`text-gray-400 line-through mr-2 text-xs`}>
|
|
₹{constsData?.flashDeliveryCharge}
|
|
</MyText>
|
|
))
|
|
: (constsData?.deliveryCharge > 0 && (
|
|
<MyText style={tw`text-gray-400 line-through mr-2 text-xs`}>
|
|
₹{constsData?.deliveryCharge}
|
|
</MyText>
|
|
))
|
|
)}
|
|
<MyText style={tw`${deliveryCharge === 0 ? 'text-green-600' : 'text-gray-900'} font-medium`}>
|
|
{deliveryCharge === 0 ? 'Free' : `₹${deliveryCharge}`}
|
|
</MyText>
|
|
</View>
|
|
</View>
|
|
|
|
{/* Free Delivery Nudge */}
|
|
{deliveryCharge > 0 && (() => {
|
|
const threshold = isFlashDelivery
|
|
? constsData?.flashFreeDeliveryThreshold
|
|
: constsData?.freeDeliveryThreshold;
|
|
return threshold > 0 && finalTotal < threshold && (
|
|
<View style={tw`bg-blue-50 p-2.5 rounded-lg mb-2 flex-row items-center`}>
|
|
<MaterialIcons name="shopping-bag" size={16} color="#2563EB" style={tw`mr-2`} />
|
|
<MyText style={tw`text-blue-700 text-xs font-medium flex-1`}>
|
|
Add products worth ₹{(threshold - finalTotal).toFixed(0)} for free delivery
|
|
</MyText>
|
|
</View>
|
|
);
|
|
})()}
|
|
|
|
{/* Divider */}
|
|
<View style={tw`h-px bg-gray-100 my-2`} />
|
|
|
|
{/* Grand Total */}
|
|
<View style={tw`flex-row justify-between items-center`}>
|
|
<MyText style={tw`text-lg font-bold text-gray-900`}>To Pay</MyText>
|
|
<MyText style={tw`text-xl font-bold text-gray-900`}>₹{finalTotalWithDelivery}</MyText>
|
|
</View>
|
|
|
|
{/* Savings Banner */}
|
|
{(discountAmount > 0 || deliveryCharge === 0 || totalSavings > 1) && (
|
|
<View style={tw`bg-green-50 rounded-lg p-2 mt-4 flex-row justify-center items-center`}>
|
|
<MaterialIcons name="stars" size={16} color="#059669" style={tw`mr-1.5`} />
|
|
<MyText style={tw`text-green-700 text-xs font-bold`}>
|
|
You saved ₹{Math.trunc(discountAmount + totalSavings + (deliveryCharge === 0 ? (
|
|
isFlashDelivery
|
|
? constsData?.flashDeliveryCharge
|
|
: constsData?.deliveryCharge
|
|
) : 0))} on this order
|
|
</MyText>
|
|
</View>
|
|
)}
|
|
</View>
|
|
|
|
{/* Bottom Action Bar */}
|
|
<View style={tw`bg-white p-5 rounded-2xl shadow-sm mb-4 border border-gray-100`}>
|
|
{!selectedAddress && (
|
|
<View testID="select-address-error" style={tw`bg-red-50 p-3 rounded-lg mb-3 flex-row items-center`}>
|
|
<MaterialIcons name="error-outline" size={16} color="#EF4444" style={tw`mr-2`} />
|
|
<MyText style={tw`text-red-600 text-sm font-medium`}>
|
|
Please select a delivery address to place order
|
|
</MyText>
|
|
</View>
|
|
)}
|
|
<MyTouchableOpacity
|
|
testID="place-order-button"
|
|
style={tw`bg-brand500 py-4 rounded-xl items-center shadow-md ${!selectedAddress ? 'opacity-50' : ''}`}
|
|
disabled={!selectedAddress}
|
|
onPress={handlePlaceOrder}
|
|
>
|
|
<MyText style={tw`text-white font-bold text-lg`}>
|
|
Place Order
|
|
</MyText>
|
|
</MyTouchableOpacity>
|
|
</View>
|
|
|
|
<LoadingDialog
|
|
open={isLoadingDialogOpen}
|
|
message="Placing your order..."
|
|
/>
|
|
</>
|
|
);
|
|
};
|
|
|
|
export default PaymentAndOrderComponent;
|