/** * @page Checkout Page (`app/checkout/page.tsx`) * @purpose Authenticated COD & Razorpay checkout featuring collapsible accordion steps, * visual payment selection cards, order summary sidebar, trust badges. * Performs auto-remediation of missing variants, grays out out-of-stock items, and disables checkout. */ 'use client'; import { useEffect, useState } from 'react'; import Link from 'next/link'; import { useCartStore } from '@/store/cartStore'; import { useAuthStore } from '@/store/authStore'; import { formatCurrency, getImageUrl, parseMoney } from '@/lib/utils'; import { setAccessToken } from '@/services/api/client'; import { createCheckoutOrder, createCustomerAddress, fetchCustomerAddresses, syncCartItems, initiatePayment, cancelPayment, verifyPayment, type CustomerAddress, type CheckoutCreateResponse, } from '@/services/api/checkoutService'; import GoogleAuthModal from '@/components/auth/GoogleAuthModal'; import { Header } from '@/layout/Header'; import { Footer } from '@/layout/Footer'; import { StickyHeaderSpacer } from '@/components/StickyHeaderSpacer'; import { PhoneInput, PincodeInput } from '@/components/PhoneInput'; import { validatePhone, validatePincode, validateAddress } from '@/lib/validation'; import { toast } from 'sonner'; import { ShieldCheck, CreditCard, CheckCircle, Lock, Loader2, MapPin, Plus, Wallet, ShoppingBag, ArrowRight, ArrowLeft, Info, Check, RefreshCw, Trash2, KeyRound, } from 'lucide-react'; import { motion, AnimatePresence } from 'framer-motion'; type CheckoutStep = 'shipping' | 'payment' | 'review'; const STEP_META: { id: CheckoutStep; label: string; num: number }[] = [ { id: 'shipping', label: 'Shipping', num: 1 }, { id: 'payment', label: 'Payment', num: 2 }, { id: 'review', label: 'Review', num: 3 }, ]; export default function CheckoutPage() { const cart = useCartStore((state) => state.cart); const getCartTotal = useCartStore((state) => state.getCartTotal); const clearCart = useCartStore((state) => state.clearCart); const hasHydrated = useCartStore((state) => state._hasHydrated); const { isAuthenticated, accessToken, authReady, bootstrapAuth } = useAuthStore(); const [activeStep, setActiveStep] = useState('shipping'); const [authModalOpen, setAuthModalOpen] = useState(false); const [addresses, setAddresses] = useState([]); const [selectedAddressId, setSelectedAddressId] = useState(null); const [paymentMethod, setPaymentMethod] = useState<'COD' | 'RAZORPAY'>('COD'); const [loadingAddresses, setLoadingAddresses] = useState(false); const [submitting, setSubmitting] = useState(false); const [errorMsg, setErrorMsg] = useState(null); const [orderResult, setOrderResult] = useState(null); const [showAddressForm, setShowAddressForm] = useState(false); const [outOfStockVariantIds, setOutOfStockVariantIds] = useState>( new Set() ); const [addressForm, setAddressForm] = useState({ address_type: 'SHIPPING', full_name: '', phone: '', street_address: '', city: '', state: '', pincode: '', is_default: true, }); const subtotal = getCartTotal(); // Free shipping on INR orders over ₹999; ₹99 flat shipping for orders below ₹999 const shipping = subtotal > 0 && subtotal < 999 ? 99.0 : 0; const tax = Math.round(subtotal * 0.18 * 100) / 100; const total = subtotal + shipping + tax; const hasOutOfStockItems = cart.some((item) => outOfStockVariantIds.has(item.selectedVariant.variant_id) ); useEffect(() => { void bootstrapAuth(); }, [bootstrapAuth]); useEffect(() => { if (accessToken) { setAccessToken(accessToken); } }, [accessToken]); useEffect(() => { if (!authReady) return; if (!isAuthenticated) { toast.message('Please sign in to continue', { description: 'You need to log in before checkout.', }); setAuthModalOpen(true); return; } setAuthModalOpen(false); loadAddresses(); void syncAndValidateCart(); }, [isAuthenticated, authReady]); const syncAndValidateCart = async () => { if (!isAuthenticated) return; setErrorMsg(null); setSubmitting(true); let attempts = 0; const maxAttempts = 15; let localCart = [...cart]; while (attempts < maxAttempts) { if (localCart.length === 0) { setSubmitting(false); return; } const itemsToSync = localCart .filter( (item) => !outOfStockVariantIds.has(item.selectedVariant.variant_id) ) .map((item) => ({ variant_id: item.selectedVariant.variant_id, qty: item.quantity, })); try { if (itemsToSync.length > 0) { await syncCartItems(itemsToSync); } const response = await fetch('/api/v1/cart', { headers: { Authorization: `Bearer ${accessToken || ''}` }, }); if (response.ok) { const serverCartData = await response.json(); const newOutOfStock = new Set(); if (serverCartData && serverCartData.items) { serverCartData.items.forEach((sItem: any) => { if ( typeof sItem.available_stock === 'number' && sItem.available_stock <= 0 ) { newOutOfStock.add(sItem.variant_id); } }); setOutOfStockVariantIds(newOutOfStock); } } break; } catch (err: any) { const detail = err.message || ''; if ( detail.includes('not found') || detail.includes('no longer exists') || detail.includes('Variant') ) { const match = detail.match(/Variant (\S+) not found/i) || detail.match(/Product variant (\S+) no longer exists/i); const missingId = match ? match[1] : detail.includes('Variant') ? detail.split(' ')[1] : null; if (missingId) { const itemToRemove = localCart.find( (item) => item.selectedVariant?.variant_id === missingId || item.id?.includes(missingId) ); if (itemToRemove) { await useCartStore.getState().removeFromCart(itemToRemove.id); localCart = localCart.filter( (item) => item.id !== itemToRemove.id ); attempts++; continue; } } } if (detail.includes('Insufficient stock')) { const match = detail.match(/Available:\s*(\d+)/i); const availableQty = match ? parseInt(match[1], 10) : 0; const skuMatch = detail.match(/Insufficient stock for (\S+)\./i); const sku = skuMatch ? skuMatch[1] : null; if (sku) { const itemToAdjust = localCart.find( (item) => item.selectedVariant?.sku === sku || item.selectedVariant?.variant_id === sku ); if (itemToAdjust) { const variantId = itemToAdjust.selectedVariant.variant_id; if (availableQty <= 0) { setOutOfStockVariantIds((prev) => { const updated = new Set(prev); updated.add(variantId); return updated; }); attempts++; continue; } else { await useCartStore .getState() .updateQuantity(itemToAdjust.id, availableQty); localCart = localCart.map((item) => item.id === itemToAdjust.id ? { ...item, quantity: availableQty } : item ); attempts++; continue; } } } } setErrorMsg(detail); break; } } setSubmitting(false); }; const loadAddresses = async () => { setLoadingAddresses(true); setErrorMsg(null); try { const addrs = await fetchCustomerAddresses(); setAddresses(addrs); const defaultAddr = addrs.find((a) => a.is_default) || addrs[0]; setSelectedAddressId(defaultAddr?.address_id || null); } catch (err: any) { setErrorMsg(err.message || 'Failed to load addresses. Please sign in again.'); } finally { setLoadingAddresses(false); } }; const handleSaveAddress = async (e: React.FormEvent) => { e.preventDefault(); setErrorMsg(null); const phoneErr = validatePhone(addressForm.phone, true); if (phoneErr) { setErrorMsg(phoneErr); return; } const pincodeErr = validatePincode(addressForm.pincode, true); if (pincodeErr) { setErrorMsg(pincodeErr); return; } const addressErr = validateAddress(addressForm.street_address, true); if (addressErr) { setErrorMsg(addressErr); return; } setSubmitting(true); try { const created = await createCustomerAddress(addressForm); setAddresses((prev) => [...prev, created]); setSelectedAddressId(created.address_id); setShowAddressForm(false); setAddressForm({ address_type: 'SHIPPING', full_name: '', phone: '', street_address: '', city: '', state: '', pincode: '', is_default: true, }); } catch (err: any) { setErrorMsg(err.message || 'Failed to save address'); } finally { setSubmitting(false); } }; const loadRazorpayScript = (): Promise => { return new Promise((resolve) => { if ((window as any).Razorpay) { resolve(true); return; } const script = document.createElement('script'); script.src = 'https://checkout.razorpay.com/v1/checkout.js'; script.onload = () => resolve(true); script.onerror = () => resolve(false); document.body.appendChild(script); }); }; const handlePlaceOrder = async (e: React.FormEvent) => { e.preventDefault(); setErrorMsg(null); if (hasOutOfStockItems) { setErrorMsg( 'Please remove out-of-stock items from your cart before placing your order.' ); return; } if (!isAuthenticated) { setAuthModalOpen(true); return; } if (cart.length === 0) { setErrorMsg('Your cart is empty.'); return; } if (!selectedAddressId) { setErrorMsg('Please select or add a shipping address.'); return; } setSubmitting(true); try { await syncCartItems( cart.map((item) => ({ variant_id: item.selectedVariant.variant_id, qty: item.quantity, })) ); const orderData = await createCheckoutOrder({ address_id: selectedAddressId, payment_method: paymentMethod, }); if (paymentMethod === 'COD') { clearCart(); setOrderResult(orderData); setSubmitting(false); } else { const scriptLoaded = await loadRazorpayScript(); if (!scriptLoaded) { throw new Error( 'Razorpay SDK failed to load. Please check your internet connection.' ); } const initRes = await initiatePayment(orderData.order_id); if (initRes.zero_amount) { clearCart(); setOrderResult({ ...orderData, payment_status: 'PAYMENT_CAPTURED', status: 'ORDER_CONFIRMED', invoice_id: initRes.invoice_id || orderData.invoice_id, invoice_no: initRes.invoice_no || orderData.invoice_no, }); setSubmitting(false); return; } const selAddr = addresses.find((a) => a.address_id === selectedAddressId); const options = { key: initRes.rzp_key_id, amount: initRes.amount, currency: initRes.currency, name: 'iFixKart', description: `Order #${orderData.order_no}`, order_id: initRes.rzp_order_id, redirect: false, prefill: { name: selAddr?.full_name || '', contact: selAddr?.phone || '', }, theme: { color: '#1976F3', }, handler: async function (response: any) { try { setSubmitting(true); const verifyRes = await verifyPayment({ order_id: orderData.order_id, razorpay_order_id: response.razorpay_order_id, razorpay_payment_id: response.razorpay_payment_id, razorpay_signature: response.razorpay_signature, }); clearCart(); setOrderResult({ ...orderData, payment_status: 'PAYMENT_CAPTURED', status: 'ORDER_CONFIRMED', invoice_no: verifyRes.invoice_no || orderData.invoice_no, }); } catch (vErr: any) { setErrorMsg(vErr.message || 'Payment verification failed.'); } finally { setSubmitting(false); } }, modal: { escape: true, handleback: true, confirm_close: true, ondismiss: async function () { setSubmitting(false); setErrorMsg('Payment popup was closed before completion. Order cancelled.'); try { await cancelPayment(orderData.order_id, 'Payment popup closed by customer'); } catch (cErr) { console.error('Failed to cancel payment session:', cErr); } }, }, }; const rzp = new (window as any).Razorpay(options); rzp.open(); } } catch (err: any) { const detail = err.message || 'Failed to place order'; if ( typeof detail === 'string' && (detail.includes('no longer exists') || detail.includes('not found') || detail.includes('Variant')) ) { const match = detail.match(/Product variant (\S+) no longer exists/i) || detail.match(/Variant (\S+) not found/i); const missingId = match ? match[1] : detail.includes('Variant') ? detail.split(' ')[1] : null; if (missingId) { const itemToRemove = cart.find( (item) => item.selectedVariant?.variant_id === missingId || item.id?.includes(missingId) ); if (itemToRemove) { void useCartStore.getState().removeFromCart(itemToRemove.id); setErrorMsg( `Stale item "${itemToRemove.product.name}" was automatically removed from your cart because it no longer exists. Please review your order and try again.` ); setSubmitting(false); void syncAndValidateCart(); return; } } } setErrorMsg(typeof detail === 'string' ? detail : 'Failed to place order'); setSubmitting(false); } }; const currentSelectedAddress = addresses.find( (a) => a.address_id === selectedAddressId ); const stepIndex = STEP_META.findIndex((s) => s.id === activeStep); const itemImage = (item: (typeof cart)[number]) => { const raw = (item.product as any)?.images?.[0]?.image_url || (item.product as any)?.thumbnail_url || ''; return getImageUrl(raw) || ''; }; if (!hasHydrated || !authReady) { return (
); } if (orderResult) { return (

Order placed successfully

Thank you for shopping at iFixKart. Your order{' '} {orderResult.order_no}{' '} is confirmed.

{orderResult.invoice_no && (
Invoice: {orderResult.invoice_no}
)}
Track order status Continue shopping
); } if (!isAuthenticated) { return (
iFix Kart

Sign in to checkout

Authenticate securely to choose a shipping address and complete your order.

Secure OAuth 2.0 · SSL encrypted checkout

Back to cart
setAuthModalOpen(false)} onSuccess={() => { setAuthModalOpen(false); loadAddresses(); }} />
); } return (
{/* Header + stepper */}

Secure Checkout

{STEP_META.map((step, i) => { const done = i < stepIndex; const active = step.id === activeStep; const clickable = step.id === 'shipping' || (step.id === 'payment' && selectedAddressId) || (step.id === 'review' && activeStep === 'review'); return (
{i > 0 && (
)}
); })}
{errorMsg && (
{errorMsg}
)} {hasOutOfStockItems && (
Please remove all out-of-stock items from your cart before placing the order.
)}
{/* Left: steps */}
{activeStep === 'shipping' && (

Shipping address

{loadingAddresses ? (
Loading addresses...
) : addresses.length > 0 ? (
{addresses.map((addr) => { const isSelected = selectedAddressId === addr.address_id; return ( ); })}
) : (

No saved shipping addresses found.

)}
)} {activeStep === 'payment' && (

Payment method

)} {activeStep === 'review' && (

Review & place order

Delivery

{currentSelectedAddress ? (

{currentSelectedAddress.full_name}

{currentSelectedAddress.street_address},{' '} {currentSelectedAddress.city}
{currentSelectedAddress.state} -{' '} {currentSelectedAddress.pincode}

{currentSelectedAddress.phone}

) : (

No address selected.

)}

Payment

{paymentMethod === 'RAZORPAY' ? 'Online payment' : 'Cash on delivery'} {paymentMethod}

{paymentMethod === 'RAZORPAY' ? 'Secured via Razorpay.' : 'Pay in cash on delivery.'}

)}
{/* Right: summary */}

Order summary

{cart.length === 0 ? (

Cart is empty.{' '} Continue shopping

) : (
{cart.map((item) => { const isOutOfStock = outOfStockVariantIds.has( item.selectedVariant.variant_id ); const img = itemImage(item); return (
{img ? ( ) : ( )} {isOutOfStock && (
OOS
)}
{item.product.name} Qty {item.quantity} ×{' '} {formatCurrency( parseMoney(item.selectedVariant.price) )}
{formatCurrency( parseMoney(item.selectedVariant.price) * item.quantity )}
); })}
)}
Subtotal {formatCurrency(subtotal)}
GST (18%) {formatCurrency(tax)}
Shipping {shipping === 0 ? ( Free ) : ( {formatCurrency(shipping)} )}
Total {formatCurrency(total)}

Why shop with us

SSL encrypted checkout Your payment details stay protected.
Genuine products Authentic spares and accessories.
Easy returns Hassle-free return window on eligible orders.
{showAddressForm && (

Add shipping address

setAddressForm({ ...addressForm, full_name: e.target.value, }) } className="w-full bg-gray-50 border border-gray-200 rounded-lg p-2.5 outline-none focus:border-primary focus:ring-2 focus:ring-primary/15 text-[#1a1a1a]" />
setAddressForm({ ...addressForm, phone: val })} />
setAddressForm({ ...addressForm, street_address: e.target.value, }) } className="w-full bg-gray-50 border border-gray-200 rounded-lg p-2.5 outline-none focus:border-primary focus:ring-2 focus:ring-primary/15 text-[#1a1a1a]" />
setAddressForm({ ...addressForm, city: e.target.value }) } className="w-full bg-gray-50 border border-gray-200 rounded-lg p-2.5 outline-none focus:border-primary focus:ring-2 focus:ring-primary/15 text-[#1a1a1a]" />
setAddressForm({ ...addressForm, state: e.target.value }) } className="w-full bg-gray-50 border border-gray-200 rounded-lg p-2.5 outline-none focus:border-primary focus:ring-2 focus:ring-primary/15 text-[#1a1a1a]" />
setAddressForm({ ...addressForm, pincode: val })} />
)} setAuthModalOpen(false)} onSuccess={() => { setAuthModalOpen(false); loadAddresses(); }} />
); }