import React, { useState, useEffect } from 'react'; import { useNavigate } from 'react-router-dom'; import { useCartStore } from '@/lib/cart/store'; import { useCheckoutSettings } from '@/hooks/useAppearanceSettings'; import { Button } from '@/components/ui/button'; import { SearchableSelect } from '@/components/ui/searchable-select'; import { DynamicCheckoutField, type CheckoutField } from '@/components/DynamicCheckoutField'; import Container from '@/components/Layout/Container'; import SEOHead from '@/components/SEOHead'; import { formatPrice } from '@/lib/currency'; import { ArrowLeft, ShoppingBag, MapPin, Check, Edit2, Loader2, X, Tag } from 'lucide-react'; import { toast } from 'sonner'; import { apiClient } from '@/lib/api/client'; import { api } from '@/lib/api/client'; import { AddressSelector } from '@/components/AddressSelector'; import { applyCoupon, removeCoupon, fetchCart } from '@/lib/cart/api'; interface SavedAddress { id: number; label: string; type: 'billing' | 'shipping' | 'both'; first_name: string; last_name: string; company?: string; address_1: string; address_2?: string; city: string; state: string; postcode: string; country: string; email?: string; phone?: string; is_default: boolean; } export default function Checkout() { const navigate = useNavigate(); const { cart } = useCartStore(); const { layout, elements } = useCheckoutSettings(); const [isProcessing, setIsProcessing] = useState(false); const [couponCode, setCouponCode] = useState(''); const [isApplyingCoupon, setIsApplyingCoupon] = useState(false); const [appliedCoupons, setAppliedCoupons] = useState<{ code: string; discount: number }[]>([]); const [discountTotal, setDiscountTotal] = useState(0); const user = (window as any).woonoowCustomer?.user; // Check if cart needs shipping (virtual-only carts don't need shipping) // Use cart.needs_shipping from WooCommerce API, fallback to item-level check const isVirtualOnly = React.useMemo(() => { // Prefer the needs_shipping flag from the cart API ( WooCommerce calculates this properly) if (typeof cart.needs_shipping === 'boolean') { return !cart.needs_shipping; } // Fallback: check individual items if needs_shipping not available if (cart.items.length === 0) return false; return cart.items.every(item => item.virtual || item.downloadable); }, [cart.items, cart.needs_shipping]); // Calculate totals const subtotal = cart.items.reduce((sum, item) => sum + (item.price * item.quantity), 0); const tax = 0; // TODO: Calculate tax // Note: shipping is calculated from dynamic shippingCost state (defined below) // Form state const [billingData, setBillingData] = useState({ firstName: '', lastName: '', email: '', phone: '', address: '', city: '', state: '', postcode: '', country: '', }); const [shippingData, setShippingData] = useState({ firstName: '', lastName: '', address: '', city: '', state: '', postcode: '', country: '', }); const [shipToDifferentAddress, setShipToDifferentAddress] = useState(false); const [orderNotes, setOrderNotes] = useState(''); const [paymentMethod, setPaymentMethod] = useState(isVirtualOnly ? 'bacs' : 'cod'); // Saved addresses const [savedAddresses, setSavedAddresses] = useState([]); const [selectedBillingAddressId, setSelectedBillingAddressId] = useState(null); const [selectedShippingAddressId, setSelectedShippingAddressId] = useState(null); const [loadingAddresses, setLoadingAddresses] = useState(true); const [showBillingModal, setShowBillingModal] = useState(false); const [showShippingModal, setShowShippingModal] = useState(false); const [showBillingForm, setShowBillingForm] = useState(true); const [showShippingForm, setShowShippingForm] = useState(true); // Countries and states data const [countries, setCountries] = useState<{ code: string; name: string }[]>([]); const [states, setStates] = useState>>({}); const [defaultCountry, setDefaultCountry] = useState(''); // Load countries and states useEffect(() => { const loadCountries = async () => { try { const data = await api.get<{ countries: { code: string; name: string }[]; states: Record>; default_country: string; }>('/countries'); setCountries(data.countries || []); setStates(data.states || {}); setDefaultCountry(data.default_country || ''); // Set default country if not already set if (!billingData.country && data.default_country) { setBillingData(prev => ({ ...prev, country: data.default_country })); } if (!shippingData.country && data.default_country) { setShippingData(prev => ({ ...prev, country: data.default_country })); } } catch (error) { console.error('Failed to load countries:', error); } }; loadCountries(); }, []); // Country/state options for SearchableSelect const countryOptions = countries.map(c => ({ value: c.code, label: `${c.name} (${c.code})` })); const billingStateOptions = Object.entries(states[billingData.country] || {}).map(([code, name]) => ({ value: code, label: name })); const shippingStateOptions = Object.entries(states[shippingData.country] || {}).map(([code, name]) => ({ value: code, label: name })); // Clear state when country changes useEffect(() => { if (billingData.country && billingData.state) { const countryStates = states[billingData.country] || {}; if (!countryStates[billingData.state]) { setBillingData(prev => ({ ...prev, state: '' })); } } }, [billingData.country, states]); useEffect(() => { if (shippingData.country && shippingData.state) { const countryStates = states[shippingData.country] || {}; if (!countryStates[shippingData.state]) { setShippingData(prev => ({ ...prev, state: '' })); } } }, [shippingData.country, states]); // Dynamic checkout fields from API const [checkoutFields, setCheckoutFields] = useState([]); const [customFieldData, setCustomFieldData] = useState>({}); // Dynamic shipping rates interface ShippingRate { id: string; label: string; cost: number; method_id: string; instance_id: number; } const [shippingRates, setShippingRates] = useState([]); const [selectedShippingRate, setSelectedShippingRate] = useState(''); const [isLoadingRates, setIsLoadingRates] = useState(false); const [shippingCost, setShippingCost] = useState(0); // Fetch checkout fields from API useEffect(() => { const loadCheckoutFields = async () => { if (cart.items.length === 0) return; try { const items = cart.items.map(item => ({ product_id: item.product_id, qty: item.quantity, })); const data = await api.post<{ ok: boolean; fields: CheckoutField[]; is_digital_only: boolean; }>('/checkout/fields', { items, is_digital_only: isVirtualOnly }); if (data.ok && data.fields) { setCheckoutFields(data.fields); // Initialize custom field values with defaults const customDefaults: Record = {}; data.fields.forEach(field => { if (field.default) { customDefaults[field.key] = field.default; } }); if (Object.keys(customDefaults).length > 0) { setCustomFieldData(prev => ({ ...customDefaults, ...prev })); } // Set billing default values for hidden fields (e.g., Indonesia-only stores) const billingCountryField = data.fields.find(f => f.key === 'billing_country'); if (billingCountryField?.type === 'hidden' && billingCountryField.default) { setBillingData(prev => ({ ...prev, country: billingCountryField.default || prev.country })); } // Set shipping default values for hidden fields const shippingCountryField = data.fields.find(f => f.key === 'shipping_country'); if (shippingCountryField?.type === 'hidden' && shippingCountryField.default) { setShippingData(prev => ({ ...prev, country: shippingCountryField.default || prev.country })); } } } catch (error) { console.error('Failed to load checkout fields:', error); } }; loadCheckoutFields(); }, [cart.items, isVirtualOnly]); // Helper to check if a standard field should be hidden (set to type: 'hidden' in PHP) const isFieldHidden = (fieldKey: string): boolean => { const field = checkoutFields.find(f => f.key === fieldKey); return field?.type === 'hidden'; }; // Filter custom fields by fieldset const billingCustomFields = checkoutFields.filter(f => f.fieldset === 'billing' && f.custom && !f.hidden); const shippingCustomFields = checkoutFields.filter(f => f.fieldset === 'shipping' && f.custom && !f.hidden); // Handler for custom field changes const handleCustomFieldChange = (key: string, value: string) => { setCustomFieldData(prev => ({ ...prev, [key]: value })); }; // Listen for label events from searchable_select useEffect(() => { const handleLabelEvent = (e: Event) => { const { key, value } = (e as CustomEvent).detail; setCustomFieldData(prev => ({ ...prev, [key]: value })); }; document.addEventListener('woonoow:field_label', handleLabelEvent); return () => document.removeEventListener('woonoow:field_label', handleLabelEvent); }, []); // Fetch shipping rates when address changes const fetchShippingRates = async () => { if (isVirtualOnly || !cart.items.length) return; // Get address data (use shipping if different, otherwise billing) const addressData = shipToDifferentAddress ? shippingData : billingData; // Need at least country to calculate shipping if (!addressData.country) { setShippingRates([]); return; } setIsLoadingRates(true); try { const items = cart.items.map(item => ({ product_id: item.product_id, quantity: item.quantity, })); const destinationId = shipToDifferentAddress ? customFieldData['shipping_destination_id'] : customFieldData['billing_destination_id']; const response = await api.post<{ ok: boolean; rates: ShippingRate[]; zone_name?: string }>('/checkout/shipping-rates', { shipping: { country: addressData.country, state: addressData.state, city: addressData.city, postcode: addressData.postcode, destination_id: destinationId || undefined, }, items, }); if (response.ok && response.rates) { setShippingRates(response.rates); // Auto-select first rate if none selected if (response.rates.length > 0 && !selectedShippingRate) { setSelectedShippingRate(response.rates[0].id); setShippingCost(response.rates[0].cost); } } } catch (error) { console.error('Failed to fetch shipping rates:', error); setShippingRates([]); } finally { setIsLoadingRates(false); } }; // Trigger shipping rate fetch when address or destination changes useEffect(() => { const addressData = shipToDifferentAddress ? shippingData : billingData; const destinationId = shipToDifferentAddress ? customFieldData['shipping_destination_id'] : customFieldData['billing_destination_id']; // Debounce the fetch const timeoutId = setTimeout(() => { if (addressData.country) { fetchShippingRates(); } }, 500); return () => clearTimeout(timeoutId); }, [ billingData.country, billingData.state, billingData.city, billingData.postcode, shippingData.country, shippingData.state, shippingData.city, shippingData.postcode, shipToDifferentAddress, customFieldData['billing_destination_id'], customFieldData['shipping_destination_id'], cart.items.length, ]); // Update shipping cost when rate selected const handleShippingRateChange = (rateId: string) => { setSelectedShippingRate(rateId); const rate = shippingRates.find(r => r.id === rateId); setShippingCost(rate?.cost || 0); }; useEffect(() => { const loadAddresses = async () => { if (!user?.isLoggedIn) { setLoadingAddresses(false); return; } try { const addresses = await api.get('/account/addresses'); setSavedAddresses(addresses); // Auto-select default addresses const defaultBilling = addresses.find(a => a.is_default && (a.type === 'billing' || a.type === 'both')); const defaultShipping = addresses.find(a => a.is_default && (a.type === 'shipping' || a.type === 'both')); if (defaultBilling) { setSelectedBillingAddressId(defaultBilling.id); fillBillingFromAddress(defaultBilling); setShowBillingForm(false); // Hide form when default address is auto-selected } if (defaultShipping && !isVirtualOnly) { setSelectedShippingAddressId(defaultShipping.id); fillShippingFromAddress(defaultShipping); setShowShippingForm(false); // Hide form when default address is auto-selected } } catch (error) { console.error('Failed to load addresses:', error); } finally { setLoadingAddresses(false); } }; loadAddresses(); }, [user, isVirtualOnly]); // Helper functions to fill forms from saved addresses const fillBillingFromAddress = (address: SavedAddress) => { setBillingData({ firstName: address.first_name, lastName: address.last_name, email: address.email || billingData.email, phone: address.phone || billingData.phone, address: address.address_1, city: address.city, state: address.state, postcode: address.postcode, country: address.country, }); }; const fillShippingFromAddress = (address: SavedAddress) => { setShippingData({ firstName: address.first_name, lastName: address.last_name, address: address.address_1, city: address.city, state: address.state, postcode: address.postcode, country: address.country, }); }; const handleSelectBillingAddress = (address: SavedAddress) => { setSelectedBillingAddressId(address.id); fillBillingFromAddress(address); setShowBillingForm(false); // Hide form when address is selected }; const handleSelectShippingAddress = (address: SavedAddress) => { setSelectedShippingAddressId(address.id); fillShippingFromAddress(address); setShowShippingForm(false); // Hide form when address is selected }; // Auto-fill form with user data if logged in useEffect(() => { if (user?.isLoggedIn && user?.billing && savedAddresses.length === 0) { setBillingData({ firstName: user.billing.first_name || '', lastName: user.billing.last_name || '', email: user.billing.email || user.email || '', phone: user.billing.phone || '', address: user.billing.address_1 || '', city: user.billing.city || '', state: user.billing.state || '', postcode: user.billing.postcode || '', country: user.billing.country || '', }); } if (user?.isLoggedIn && user?.shipping) { setShippingData({ firstName: user.shipping.first_name || '', lastName: user.shipping.last_name || '', address: user.shipping.address_1 || '', city: user.shipping.city || '', state: user.shipping.state || '', postcode: user.shipping.postcode || '', country: user.shipping.country || '', }); } }, [user]); const handleApplyCoupon = async () => { if (!couponCode.trim()) return; setIsApplyingCoupon(true); try { const updatedCart = await applyCoupon(couponCode.trim()); if (updatedCart.coupons) { setAppliedCoupons(updatedCart.coupons); setDiscountTotal(updatedCart.discount_total || 0); } setCouponCode(''); toast.success('Coupon applied successfully'); } catch (error: any) { toast.error(error.message || 'Failed to apply coupon'); } finally { setIsApplyingCoupon(false); } }; const handleRemoveCoupon = async (code: string) => { setIsApplyingCoupon(true); try { const updatedCart = await removeCoupon(code); if (updatedCart.coupons) { setAppliedCoupons(updatedCart.coupons); setDiscountTotal(updatedCart.discount_total || 0); } toast.success('Coupon removed'); } catch (error: any) { toast.error(error.message || 'Failed to remove coupon'); } finally { setIsApplyingCoupon(false); } }; // Load cart data including coupons on mount useEffect(() => { const loadCartData = async () => { try { const cartData = await fetchCart(); if (cartData.coupons) { setAppliedCoupons(cartData.coupons); setDiscountTotal(cartData.discount_total || 0); } } catch (error) { console.error('Failed to load cart data:', error); } }; loadCartData(); }, []); const handlePlaceOrder = async (e: React.FormEvent) => { e.preventDefault(); setIsProcessing(true); try { // Prepare order data const orderData = { items: cart.items.map(item => ({ product_id: item.product_id, variation_id: item.variation_id, qty: item.quantity, meta: item.attributes ? Object.entries(item.attributes).map(([key, value]) => ({ key, value })) : [] })), billing: { first_name: billingData.firstName, last_name: billingData.lastName, email: billingData.email, phone: billingData.phone, address_1: billingData.address, city: billingData.city, state: billingData.state, postcode: billingData.postcode, country: billingData.country, // Include custom billing fields ...Object.fromEntries( billingCustomFields.map(f => [f.key.replace('billing_', ''), customFieldData[f.key] || '']) ), }, shipping: shipToDifferentAddress ? { first_name: shippingData.firstName, last_name: shippingData.lastName, address_1: shippingData.address, city: shippingData.city, state: shippingData.state, postcode: shippingData.postcode, country: shippingData.country, ship_to_different: true, // Include custom shipping fields ...Object.fromEntries( shippingCustomFields.map(f => [f.key.replace('shipping_', ''), customFieldData[f.key] || '']) ), } : { ship_to_different: false, }, payment_method: paymentMethod, shipping_method: selectedShippingRate || undefined, // Selected shipping rate ID customer_note: orderNotes, // Include all custom field data for backend processing custom_fields: customFieldData, }; // Submit order const response = await apiClient.post('/checkout/submit', orderData); const data = (response as any).data || response; if (data.ok && data.order_id) { // Clear cart - use store method directly useCartStore.getState().clearCart(); toast.success('Order placed successfully!'); // Navigate to thank you page via SPA routing // Using window.location.replace to prevent back button issues const thankYouUrl = `/order-received/${data.order_id}?key=${data.order_key}`; navigate(thankYouUrl, { replace: true }); return; // Stop execution here } else { throw new Error(data.error || 'Failed to create order'); } } catch (error: any) { toast.error(error.message || 'Failed to place order'); console.error('Order creation error:', error); } finally { setIsProcessing(false); } }; // Empty cart redirect (but only if not processing) if (cart.items.length === 0 && !isProcessing) { return (

Your cart is empty

Add some products before checking out!

); } return (
{/* Header */}

Checkout

{/* Billing & Shipping Forms */}
{/* Selected Billing Address Summary */} {!loadingAddresses && savedAddresses.length > 0 && savedAddresses.some(a => a.type === 'billing' || a.type === 'both') && (

Billing Address

{selectedBillingAddressId ? ( (() => { const selected = savedAddresses.find(a => a.id === selectedBillingAddressId); return selected ? (

{selected.label}

{selected.is_default && ( Default )}

{selected.first_name} {selected.last_name}

{selected.phone &&

{selected.phone}

}

{selected.address_1}

{selected.address_2 &&

{selected.address_2}

}

{selected.city}, {selected.state} {selected.postcode}

{selected.country}

) : null; })() ) : (

No address selected

)}
)} {/* Billing Address Modal */} setShowBillingModal(false)} addresses={savedAddresses} selectedAddressId={selectedBillingAddressId} onSelectAddress={handleSelectBillingAddress} type="billing" /> {/* Billing Details Form - Only show if no saved address selected or user wants to enter manually */} {(savedAddresses.length === 0 || !selectedBillingAddressId || showBillingForm) && (

Billing Details

setBillingData({ ...billingData, firstName: e.target.value })} className="w-full border rounded-lg px-4 py-2" />
setBillingData({ ...billingData, lastName: e.target.value })} className="w-full border rounded-lg px-4 py-2" />
setBillingData({ ...billingData, email: e.target.value })} className="w-full border rounded-lg px-4 py-2" />
setBillingData({ ...billingData, phone: e.target.value })} className="w-full border rounded-lg px-4 py-2" />
{/* Address fields - only for physical products */} {!isVirtualOnly && ( <>
setBillingData({ ...billingData, address: e.target.value })} className="w-full border rounded-lg px-4 py-2" />
{/* City field - hidden if PHP sets type to 'hidden' */} {!isFieldHidden('billing_city') && (
setBillingData({ ...billingData, city: e.target.value })} className="w-full border rounded-lg px-4 py-2" />
)} {/* Country field - hidden if PHP sets type to 'hidden' */} {!isFieldHidden('billing_country') && (
setBillingData({ ...billingData, country: v })} placeholder="Select country" disabled={countries.length === 1} />
)} {/* State field - hidden if PHP sets type to 'hidden' */} {!isFieldHidden('billing_state') && (
{billingStateOptions.length > 0 ? ( setBillingData({ ...billingData, state: v })} placeholder="Select state" /> ) : ( setBillingData({ ...billingData, state: e.target.value })} placeholder="Enter state/province" className="w-full border rounded-lg px-4 py-2" /> )}
)}
setBillingData({ ...billingData, postcode: e.target.value })} className="w-full border rounded-lg px-4 py-2" />
{/* Custom billing fields from plugins */} {billingCustomFields.map(field => ( handleCustomFieldChange(field.key, v)} countryOptions={countryOptions} stateOptions={billingStateOptions} /> ))} )}
)} {/* Ship to Different Address - only for physical products */} {!isVirtualOnly && (
{shipToDifferentAddress && ( <> {/* Selected Shipping Address Summary */} {!loadingAddresses && savedAddresses.length > 0 && savedAddresses.some(a => a.type === 'shipping' || a.type === 'both') && (

Shipping Address

{selectedShippingAddressId ? ( (() => { const selected = savedAddresses.find(a => a.id === selectedShippingAddressId); return selected ? (

{selected.label}

{selected.is_default && ( Default )}

{selected.first_name} {selected.last_name}

{selected.phone &&

{selected.phone}

}

{selected.address_1}

{selected.address_2 &&

{selected.address_2}

}

{selected.city}, {selected.state} {selected.postcode}

{selected.country}

) : null; })() ) : (

No address selected

)}
)} {/* Shipping Address Modal */} setShowShippingModal(false)} addresses={savedAddresses} selectedAddressId={selectedShippingAddressId} onSelectAddress={handleSelectShippingAddress} type="shipping" /> {/* Shipping Form - Only show if no saved address selected or user wants to enter manually */} {(!selectedShippingAddressId || showShippingForm) && (
setShippingData({ ...shippingData, firstName: e.target.value })} className="w-full border rounded-lg px-4 py-2" />
setShippingData({ ...shippingData, lastName: e.target.value })} className="w-full border rounded-lg px-4 py-2" />
setShippingData({ ...shippingData, address: e.target.value })} className="w-full border rounded-lg px-4 py-2" />
{/* City field - hidden if PHP sets type to 'hidden' */} {!isFieldHidden('shipping_city') && (
setShippingData({ ...shippingData, city: e.target.value })} className="w-full border rounded-lg px-4 py-2" />
)} {/* Country field - hidden if PHP sets type to 'hidden' */} {!isFieldHidden('shipping_country') && (
setShippingData({ ...shippingData, country: v })} placeholder="Select country" disabled={countries.length === 1} />
)} {/* State field - hidden if PHP sets type to 'hidden' */} {!isFieldHidden('shipping_state') && (
{shippingStateOptions.length > 0 ? ( setShippingData({ ...shippingData, state: v })} placeholder="Select state" /> ) : ( setShippingData({ ...shippingData, state: e.target.value })} placeholder="Enter state/province" className="w-full border rounded-lg px-4 py-2" /> )}
)}
setShippingData({ ...shippingData, postcode: e.target.value })} className="w-full border rounded-lg px-4 py-2" />
{/* Custom shipping fields from plugins */} {shippingCustomFields.map(field => ( handleCustomFieldChange(field.key, v)} countryOptions={countryOptions} stateOptions={shippingStateOptions} /> ))}
)} )}
)} {/* Order Notes */} {elements.order_notes && (

Order Notes (Optional)