"use client"; /** * ShopClient — pick a plan, see what it costs, pay. * * Selecting a card used to navigate straight to /checkout, so the shopper was * handed to Stripe without ever seeing the total they were about to be charged. * The order summary is that missing beat: name, price, what is included, total. * * On a wide screen the summary is a sticky column and is always in view. On a * phone it is stacked underneath the plans, which is where the real defect was: * tapping a card changed something the customer could not see, so the tap read * as dead. `scrollSummaryIntoView` answers the tap by bringing the summary — * and the pay button in it — on screen. */ import { useCallback, useRef, useState } from "react"; import { useRouter } from "next/navigation"; import { PlanCard } from "@/components/landing/PlanCard"; import { conversionEvents } from "@/lib/conversion-events"; import { formatDataAllowance, formatPrice, formatValidity } from "@/vendor/geo"; import type { SkuTemplate } from "@/vendor/carrier/types"; /** Above this width the summary is a sticky column and never leaves the fold. */ const WIDE_VIEWPORT = "(min-width: 1024px)"; function prefersReducedMotion(): boolean { if (typeof window === "undefined" || typeof window.matchMedia !== "function") return false; return window.matchMedia("(prefers-reduced-motion: reduce)").matches; } function isWideViewport(): boolean { if (typeof window === "undefined" || typeof window.matchMedia !== "function") return false; return window.matchMedia(WIDE_VIEWPORT).matches; } export function ShopClient({ plans }: { plans: SkuTemplate[] }) { const router = useRouter(); const [selected, setSelected] = useState(null); const summaryRef = useRef(null); /** * Bring the summary on screen after a selection, on narrow viewports only. * * `scrollIntoView` is missing in some engines and in jsdom, so the call is * optional-chained: a browser that cannot scroll must still select the plan. */ const scrollSummaryIntoView = useCallback(() => { if (isWideViewport()) return; requestAnimationFrame(() => { summaryRef.current?.scrollIntoView?.({ behavior: prefersReducedMotion() ? "auto" : "smooth", block: "start", }); }); }, []); function handleSelect(plan: SkuTemplate) { conversionEvents.selectPlan(plan.name); setSelected(plan); scrollSummaryIntoView(); } function handleContinue() { if (!selected) return; router.push(`/checkout/${selected.id}`); } const total = selected ? formatPrice(selected.price_cents, selected.currency) : null; return (

Choose your plan

Instant activation. No contracts. Price next to the limit.

{plans.map((plan, i) => ( ))}

Your order

{selected?.name ?? "Pick a plan"}

{selected ? `${formatDataAllowance(selected.dataGb)} · ${formatValidity(selected.validityDays)}` : "Choose a plan on the left and the total appears here."}

Plan
{total ?? "—"}
eSIM activation
Free
Tax
Included
Total today {total ?? "—"}

Your QR code is ready the moment the payment clears. Install it before you fly.

); }