"use client"; import { useState, useEffect, useRef, useCallback } from "react"; import { useRouter } from "next/navigation"; import Link from "next/link"; import { useCart } from "@/context/CartContext"; import type { StopInfo } from "@/context/CartContext"; import { createRetailStripeCheckoutSession } from "@/actions/billing/retail-checkout"; import { getPublicStopsForBrand } from "@/actions/checkout"; import StorefrontHeader from "@/components/storefront/StorefrontHeader"; import StorefrontFooter from "@/components/storefront/StorefrontFooter"; import StripeExpressCheckout, { type CheckoutItem as ExpressItem, } from "@/components/storefront/StripeExpressCheckout"; export default function CheckoutClient() { const cartContext = useCart(); const cart = cartContext.cart; const subtotal = cartContext.subtotal; const selectedStop: StopInfo | null = cartContext.selectedStop; const setSelectedStop = cartContext.setSelectedStop; const cartBrandId = cartContext.cartBrandId; const router = useRouter(); const [stops, setStops] = useState([]); // Controlled form state — passed down to StripeExpressCheckout so the // Express + Card paths know who the buyer is and where to ship / pick up. const [customerName, setCustomerName] = useState(""); const [customerEmail, setCustomerEmail] = useState(""); const [customerPhone, setCustomerPhone] = useState(""); const [shippingState, setShippingState] = useState(""); const [shippingPostal, setShippingPostal] = useState(""); const [shippingCity, setShippingCity] = useState(""); // Hosted-checkout (legacy Stripe Checkout) fallback state const [hostedLoading, setHostedLoading] = useState(false); const [hostedError, setHostedError] = useState(null); // Stable idempotency key per checkout session — survives retries const [idempotencyKey, setIdempotencyKey] = useState(""); useEffect(() => { // Initialize idempotency key in useEffect, not during render const init = async () => { if (typeof window !== "undefined" && !idempotencyKey) { setIdempotencyKey(crypto.randomUUID()); } }; init(); }, [idempotencyKey]); useEffect(() => { if (!cartBrandId) return; getPublicStopsForBrand(cartBrandId) .then((data) => setStops(data ?? [])) .catch(() => setStops([])); }, [cartBrandId]); const hasPickupItems = cart.some((i) => i.fulfillment === "pickup"); const hasShedPickupItems = cart.some((i) => i.fulfillment === "pickup" && i.pickup_type === "shed"); const hasStopPickupItems = cart.some( (i) => i.fulfillment === "pickup" && i.pickup_type !== "shed" ); const hasShipItems = cart.some((i) => i.fulfillment === "ship"); /** * Legacy "Use secure hosted checkout" fallback. Creates a Stripe * Checkout Session and redirects — this is the path used when the * embedded Stripe Elements can't load (no publishable key, browser * incompatibility, etc.) or when the buyer prefers Stripe's hosted page. */ const handleHostedCheckout = useCallback(async () => { if (cart.length === 0) return; if (selectedStop?.brand_id && cartBrandId && selectedStop.brand_id !== cartBrandId) { setSelectedStop(null); router.replace("/cart"); return; } if (!customerEmail.trim()) { setHostedError("Please enter your email before continuing to checkout."); return; } if (hasStopPickupItems && !selectedStop) { setHostedError("Please select a pickup location."); return; } setHostedLoading(true); setHostedError(null); const items = cart.map((item) => ({ id: item.id, name: item.name, price: Number(item.price.replace("$", "")), quantity: item.quantity, fulfillment: item.fulfillment ?? "pickup", })); const siteUrl = typeof window !== "undefined" ? window.location.origin : "https://route-commerce-platform.vercel.app"; const successUrl = `${siteUrl}/checkout/success?session_id={CHECKOUT_SESSION_ID}`; const cancelUrl = `${siteUrl}/checkout?status=cancelled`; if (typeof sessionStorage !== "undefined") { sessionStorage.setItem( "pending_checkout", JSON.stringify({ customerName, customerEmail, customerPhone, stopId: selectedStop?.id ?? null, items: items.map((item) => ({ ...item, fulfillment: item.fulfillment as "pickup" | "ship", })), cartBrandId, shippingAddress: hasShipItems ? { state: shippingState, postal_code: shippingPostal, city: shippingCity } : undefined, idempotencyKey: idempotencyKey, }) ); } const stripeResult = await createRetailStripeCheckoutSession( items, "", cartBrandId ?? "unknown", successUrl, cancelUrl ); if (!stripeResult.success || !stripeResult.url) { setHostedError(stripeResult.error ?? "Failed to initiate payment. Please try again."); setHostedLoading(false); return; } window.location.href = stripeResult.url; }, [ cart, customerName, customerEmail, customerPhone, selectedStop, shippingState, shippingPostal, shippingCity, cartBrandId, hasShipItems, hasStopPickupItems, router, setSelectedStop, ]); if (cart.length === 0) { return (

Your cart is empty

← Back to storefront
); } // Map cart → StripeExpressCheckout item shape const expressItems: ExpressItem[] = cart.map((item) => ({ id: item.id, name: item.name, price: item.price, quantity: item.quantity, fulfillment: (item.fulfillment ?? "pickup") as "pickup" | "ship", })); return (
{/* LEFT — Form + Embedded Stripe Elements */}

Checkout

Tap to pay with Apple Pay, Google Pay, or card. Your details stay secure — we never see them.

{/* Error alert (shared across both payment paths) */} {(hostedError) && (
{hostedError}
)}
{/* Customer Information — controlled inputs feed the embedded Stripe Elements above. The wallet (Apple Pay) will overwrite name/email at confirm time, but we still collect them here so the order row has them in case the wallet omits them. */}
Your details

Used for the order confirmation. Apple Pay / Google Pay will fill these in for you.

setCustomerName(e.target.value)} className="mt-1 w-full rounded-xl border border-slate-300 px-4 py-3 outline-none focus:border-emerald-500 focus:ring-2 focus:ring-emerald-500/20 transition-all" placeholder="Jane Smith" />
setCustomerEmail(e.target.value)} className="mt-1 w-full rounded-xl border border-slate-300 px-4 py-3 outline-none focus:border-emerald-500 focus:ring-2 focus:ring-emerald-500/20 transition-all" placeholder="jane@example.com" aria-required="true" />
setCustomerPhone(e.target.value)} className="mt-1 w-full rounded-xl border border-slate-300 px-4 py-3 outline-none focus:border-emerald-500 focus:ring-2 focus:ring-emerald-500/20 transition-all" placeholder="(555) 555-5555" />
{/* Shed Pickup notice */} {hasShedPickupItems && (
Shed Pickup

{cart.filter((i) => i.fulfillment === "pickup" && i.pickup_type === "shed").map((i) => i.description || i.name).join(", ")}

Pickup location details are included in your order confirmation.

)} {/* Stop Pickup */} {hasStopPickupItems && (
Pickup Location {selectedStop ? (

{selectedStop.city}, {selectedStop.state}

{selectedStop.date} · {selectedStop.location}

) : (
)}
)} {/* Shipping Address */} {hasShipItems && (
Shipping Address

Enter your shipping details for delivery.

setShippingCity(e.target.value)} className="mt-1 w-full rounded-xl border border-slate-300 px-4 py-3 outline-none focus:border-emerald-500 focus:ring-2 focus:ring-emerald-500/20 transition-all" />
setShippingState(e.target.value.toUpperCase())} className="mt-1 w-full rounded-xl border border-slate-300 px-4 py-3 outline-none focus:border-emerald-500 focus:ring-2 focus:ring-emerald-500/20 transition-all uppercase" placeholder="NC" />
setShippingPostal(e.target.value)} className="mt-1 w-full rounded-xl border border-slate-300 px-4 py-3 outline-none focus:border-emerald-500 focus:ring-2 focus:ring-emerald-500/20 transition-all" placeholder="28147" />
)} {/* Embedded Stripe Elements (Express + Card) — real checkout */}
void handleHostedCheckout()} />
{/* Hosted-checkout fallback (legacy Stripe Checkout) */}

Prefer Stripe’s hosted page? Tap below.

{/* Order Summary */}
); }