## Live Checkout (S—486)

A SaaS upgrade checkout whose summary recomputes as you edit, then glides to the centre and unfolds into a receipt.

- URL: https://sundaeui.dev/components/live-checkout
- Category: Checkout & payments · Art direction: Refined
- Access: Sundae Pro (authenticated install)
- Stack: React 18+, TypeScript, plain CSS; dependencies: none
- Install: `npx shadcn@latest add https://sundaeui.dev/r/live-checkout.json`
- Tags: seats, coupon, subscription, saas, order-summary, tax, payment-form, checkout, receipt, billing, upgrade

The paid signup or upgrade step of a SaaS app, on one page: plan, seats, monthly or yearly billing, add-ons, a coupon, the billing country and the card. Every change recomputes the summary (seat lines, add-ons, the yearly discount, the coupon, tax for the chosen country) and each amount rolls digit by digit to its new value.
Details carry the motion: the selected-plan ring and the billing pill travel between options on a spring, add-on and discount lines open and close their height, the coupon field turns into a removable chip once your server accepts it, and declined payments shake in with the reason. On success the form steps aside and the summary glides to the centre and unfolds into a receipt with a downloadable copy.
Plans, add-ons and countries (tax rate, tax label, postal-code format) are props, coupons go through validateCoupon and charging through onPay, so it plugs into Stripe, Paddle, HandyPay or your own backend. computeTotals is exported so your server can use the same maths.

### Usage

```tsx
import { LiveCheckout } from '@/components/sundae/live-checkout'

export function UpgradePage() {
  return (
    <LiveCheckout
      defaultPlan="team"
      defaultSeats={12}
      validateCoupon={(code) => fetch(`/api/coupons/${code}`).then((r) => r.json())}
      onPay={async (order) => {
        const res = await fetch('/api/subscribe', { method: 'POST', body: JSON.stringify(order) })
        if (!res.ok) throw new Error('Your card was declined. Try another card.')
      }}
    />
  )
}
```

### Props

- `plans` (`Plan[]`, default `sample plans`): id, name, seatCents (per seat per month) and a blurb.
- `addons` (`Addon[]`, default `sample add-ons`): id, name, monthlyCents and an optional blurb.
- `countries` (`Country[]`, default `sample countries`): code, name, taxRate (0.2 = 20%), taxLabel, optional postal RegExp and postalHint.
- `defaultPlan / defaultSeats / defaultBilling / defaultAddons / defaultCountry` (`string / number / Billing / string[] / string`): Starting selections. Seats are 1–500.
- `yearlyDiscount` (`number`, default `0.2`): Yearly discount, 0–1, shown on the billing toggle.
- `currency` (`string`, default `'USD'`): ISO code used to format every amount.
- `validateCoupon` (`(code) => CouponResult | Promise<CouponResult>`, default `sample: LAUNCH20, TEAM10`): Return { ok: true, code, percent } or { ok: false, message }.
- `onPay` (`(order: CheckoutOrder) => void | Promise<void>`): Charge the order; throw an Error to show its message. Without it the demo simulates payment and declines 4000 0000 0000 0002.
- `onDone` (`() => void`): Called from the receipt’s Done button. Default resets the form.
- `theme` (`'auto' | 'light' | 'dark'`, default `'auto'`): auto follows a .dark ancestor.

### When to use

- Upgrading from a free plan to a paid team plan.
- Paid signup where seats and add-ons change the price.
- Self-serve checkout that needs coupons and tax by country.

### When not to use

- A physical goods cart with shipping: use Tender Checkout.
- Only showing totals: use Seat Pricing.

### Accessibility

- Plans and the billing period are radio groups (arrow keys move between plans); add-ons are real checkboxes.
- Errors are tied to their inputs with aria-describedby and aria-invalid; a failed submit focuses the first invalid field; payment errors use role="alert".
- Rolling amounts keep the full value in visually hidden text, so screen readers never hear single digits.
- On success focus moves to the receipt heading. Reduced motion keeps every state and drops the travel, rolls and unfold.

### Keyboard

- Arrow keys: Move between plans
- Enter (coupon field): Apply the coupon
- Escape (coupon field): Close the coupon field
- Enter: Submit the payment

### FAQ

**Does it collect real card details?**
It validates the card fields for layout and feedback. In production, swap them for your payment provider’s hosted fields and pass the token in onPay; the rest of the layout stays.

