'use client'; import { Suspense, useEffect, useState } from 'react'; import { useSearchParams } from 'next/navigation'; import { CdnImage as Image } from '@/ui/shared/cdn-image'; import { Link } from '@/core/lib/navigation'; import type { Order, WaitForOrderResult, OrderDownloadLink } from 'brainerce'; import { formatPrice } from 'brainerce'; import { getClient } from '@/core/lib/brainerce'; import { useCart } from '@/core/providers/store-provider'; import { useCurrency } from '@/core/lib/use-currency'; import { OrderCustomizations } from '@/components/account/order-customizations'; import { LoadingSpinner } from '@/ui/shared/loading-spinner'; import { useTranslations } from '@/core/lib/translations'; import { trackPurchase } from '@/core/lib/tracking'; function OrderConfirmationContent() { const searchParams = useSearchParams(); const checkoutId = searchParams.get('checkout_id'); const { refreshCart } = useCart(); const t = useTranslations('orderConfirmation'); const tc = useTranslations('common'); const [result, setResult] = useState(null); // The real Order behind the checkout. `waitForOrder` resolves to a status // envelope that carries no line items at all, so this second fetch is the // only way this page can show what was actually bought. const [order, setOrder] = useState(null); const [loading, setLoading] = useState(true); const [error, setError] = useState(null); useEffect(() => { if (!checkoutId) { setError(t('missingCheckoutInfo')); setLoading(false); return; } async function waitForOrder() { try { const client = getClient(); // Clear cart state after successful payment. handlePaymentSuccess is // SYNCHRONOUS: it returns the outcome rather than a promise, so there // is nothing to await. Read the result, then refresh the cart AFTER // it, or the header badge can be repopulated from the pre-clear cart. // // mode 'full' - whole cart cleared // mode 'partial' - only the purchased items were removed (partial // checkout); whatever is left is still for sale // mode 'none' - nothing to clear. Usually the SDK's idempotency // guard: React Strict Mode runs this effect twice // in development, and a refresh of this URL hits it // too. On a good order this is expected, so it must // never become a warning to the shopper. const clearResult = client.handlePaymentSuccess(checkoutId!); await refreshCart(); if (!clearResult.cleared && clearResult.mode !== 'none') { // A clear that neither succeeded nor was skipped leaves purchased // items in the badge. Nothing the shopper can act on, but a silent // gap here looks exactly like a working cart. console.warn('Cart not cleared after payment:', clearResult); } // For redirect-based payment providers, the customer returns with // provider params in the URL — CardCom: lowprofilecode; PayPal: // token + PayerID. Send these to the backend for server-side // verification via the provider's API (e.g. GetLpResult / PayPal // capture) — never trust URL params alone. For PayPal this call is // what actually captures the approved payment. const lowProfileCode = searchParams.get('lowprofilecode') || searchParams.get('LowProfileCode'); const paypalOrderToken = searchParams.get('token') && searchParams.get('PayerID') ? searchParams.get('token') : null; const redirectReturnToken = lowProfileCode || paypalOrderToken; if (redirectReturnToken) { try { await client.confirmSdkPayment(checkoutId!, { paymentIntentId: redirectReturnToken, }); } catch (err) { console.warn('Redirect payment confirmation failed:', err); // Don't block — payment-status polling re-verifies server-side } } const orderResult = await client.waitForOrder(checkoutId!, { maxWaitMs: 30000, }); setResult(orderResult); // `purchase` — the one event ad platforms optimize campaigns on. // Read the real order rather than the pre-payment cart so value, tax, // shipping and coupon match what was actually charged; `trackPurchase` // de-duplicates on the order id, so a refresh cannot double-count it. // // The same Order is what this page renders its line items from, so // keep it rather than discarding it after the conversion call. // `getOrderByCheckout` is typed `Promise` and REJECTS on 404 — // it never resolves to null — so the catch below is what degrades this // page to the order-number-only view instead of breaking it. if (orderResult.success) { try { const fetchedOrder = await client.getOrderByCheckout(checkoutId!); setOrder(fetchedOrder); trackPurchase(fetchedOrder); } catch (err) { // A conversion we failed to report must never break the page the // shopper is looking at — but it should be visible, because a // silent gap here looks identical to "no sales" in ad reporting. console.warn('Purchase conversion not reported:', err); } } } catch (err) { const message = err instanceof Error ? err.message : 'Failed to confirm order'; setError(message); } finally { setLoading(false); } } waitForOrder(); }, [checkoutId, refreshCart]); if (loading) { return (

{t('confirming')}

{t('confirmingHint')}

); } if (error) { return (

{t('errorTitle')}

{error}

{t('errorChargedHint')}

{t('returnHome')}
); } // Order was created successfully if (result?.success) { const orderNumber = result.status.orderNumber; return (

{t('thankYou')}

{orderNumber && (

{t('orderNumber')} {orderNumber}

)}

{t('confirmationEmail')}

{/* What was actually bought. Renders nothing at all when the order fetch failed or came back with no lines, leaving the number-only view above intact. */} {result.status.orderId && ( )}
{tc('continueShopping')} {t('viewOrders')}
); } // Order not yet confirmed (polling timed out) - still show success return (

{t('paymentReceived')}

{t('orderProcessing')}

{tc('continueShopping')} {t('viewOrders')}
); } /** * Line items on the confirmation page. * * `OrderItem` is FLAT — `item.name`, `item.price`, `item.image` — unlike the * nested `CartItem` / `CheckoutLineItem` shapes used elsewhere in this * storefront. Do not copy a cart row renderer here; `item.product.name` does * not exist on an order. * * Auto-hides: returns null when the order fetch failed (`order === null`) or * the order carries no lines, so the number-only confirmation still renders. */ function ConfirmationLineItems({ order }: { order: Order | null }) { // Named `tch` rather than `t` on purpose: the other components in this file // bind `t` to the `orderConfirmation` namespace, and a same-named alias on a // different namespace is how a wrong-namespace key slips through review. const tch = useTranslations('checkout'); const ta = useTranslations('account'); const tc = useTranslations('common'); const currency = useCurrency(order?.currency); if (!order || !order.items || order.items.length === 0) return null; const totalAmount = order.totalAmount || order.total || '0'; return (

{tch('orderSummary')}

{order.items.map((item, index) => (
{item.image ? ( {item.name ) : null}

{item.name || ta('productFallback')}

{tc('qty')} {item.quantity}

{formatPrice(parseFloat(item.price), { currency }) as string}
{/* What the buyer typed / uploaded / picked, with the merchant's own labels. Renders nothing when the line has none. */} {item.customizations && }
))}
{tc('total')} {formatPrice(parseFloat(totalAmount), { currency }) as string}
{/* What actually paid. This is the first screen a customer sees after handing over money, and a total with no gift-card line tells them they paid an amount they did not. The total above is what the order was WORTH and stays unreduced — tax was calculated on it — so the cards sit below it, followed by what the card or wallet was really charged. */} {order.tenders && order.tenders.length > 0 && ( <> {order.tenders.map((tender) => (
{tender.giftCard?.codeLast4 ? `${tc('giftCard')} ····${tender.giftCard.codeLast4}` : tc('giftCard')} -{formatPrice(parseFloat(tender.amountBase), { currency }) as string}
))}
{tc('amountCharged')} { formatPrice( order.tenders.reduce( (due, tn) => due - parseFloat(tn.amountBase), parseFloat(totalAmount) ), { currency } ) as string }
)}
); } function ConfirmationDownloads({ orderId, checkoutId }: { orderId: string; checkoutId: string }) { const t = useTranslations('orderConfirmation'); const [downloads, setDownloads] = useState(null); useEffect(() => { let cancelled = false; async function fetchDownloads() { const client = getClient(); // Retry a few times — the worker may still be writing downloadMeta for (let attempt = 0; attempt < 3; attempt++) { try { const links = await client.getOrderDownloads(orderId, { checkoutId }); if (!cancelled && links.length > 0) { setDownloads(links); return; } } catch { // Not all orders have downloads } if (attempt < 2 && !cancelled) { await new Promise((r) => setTimeout(r, 1500)); } } } fetchDownloads(); return () => { cancelled = true; }; }, [orderId, checkoutId]); if (!downloads || downloads.length === 0) return null; return (

{t('yourDownloads')}

{downloads.map((link, idx) => (

{link.fileName}

{link.productName}

{t('download')}
))}
); } export default function OrderConfirmationPage() { return ( } > ); }