## Seat Checkout (S—019)

A complete SaaS checkout with plans, billing interval, seats, add-ons, tax, promo codes and a live order summary.

- URL: https://sundaeui.dev/components/seat-checkout
- Category: Checkout & payments · Art direction: Understated
- Access: Sundae Pro (authenticated install)
- Stack: React 18+, TypeScript, Tailwind CSS; dependencies: none
- Install: `npx shadcn@latest add https://sundaeui.dev/r/seat-checkout.json`
- Tags: tailwind, form, subscription, saas, pricing, payment, checkout, react, validation

A real subscription checkout, not a card form on its own. Choose a plan, switch between monthly and yearly, and adjust seats with a stepper that respects each plan’s minimum. Add optional add-ons, enter billing details and pick card, bank debit or invoice. The summary recalculates every line as you go: seats × interval, add-ons, promo discount, and tax by country, which is waived when a tax ID makes it reverse-charge.
Validation runs on submit. It checks the card number with Luhn, rejects past expiry dates, and checks CVC length, bank account format and email. Errors are linked to their fields. The pay button reflects the method and the exact total, shows a processing state, and ends in a confirmation screen or an inline error.
Use it for B2B SaaS upgrades and team plans. When you have one price and nothing to configure, use a simple pricing table instead. It is the interface only: tokenize card data with your payment provider and re-validate prices and promo codes on the server.

### Usage

```tsx
import { SeatCheckout } from '@/components/sundae/seat-checkout'

<SeatCheckout
  plans={[
    { id: 'team', name: 'Team', blurb: 'Shared workspaces', perSeat: { monthly: 1800, yearly: 1440 }, minSeats: 3, features: [] },
  ]}
  addons={[{ id: 'support', name: 'Priority support', description: '4-hour response', monthly: 4900 }]}
  taxRates={{ GB: 0.2 }}
  countries={[{ code: 'US', name: 'United States' }, { code: 'GB', name: 'United Kingdom' }]}
  onSubmit={async (order) => {
    const res = await fetch('/api/subscribe', { method: 'POST', body: JSON.stringify(order) })
    if (!res.ok) throw new Error('Payment failed. Try another method.')
  }}
/>
```

### Props

- `plans` (`CheckoutPlan[]`, required): { id, name, blurb, perSeat: { monthly, yearly } (cents), minSeats?, features }.
- `addons` (`CheckoutAddon[]`): { id, name, description, monthly (cents) }. Yearly is charged 12×.
- `taxRates` (`Record<string, number>`): Rate by country code. A tax ID switches to reverse charge (0%).
- `countries` (`{ code, name }[]`): Country options for billing.
- `promoCodes` (`Record<string, number>`): Client-side preview of discounts. Always re-validate on the server.
- `currency` (`string`, default `'USD'`): ISO currency code.
- `onSubmit` (`(order: CheckoutResult) => Promise<void>`, required): Receives the full order, including the billing address. Throw to show an error.
- `expressMethods` (`{ id, label }[]`): Wallet buttons shown above the card form, e.g. Apple Pay or Google Pay. Omit to hide the row.
- `onExpressPay` (`(methodId, order) => Promise<void>`): Called when an express button is pressed. Throw to show an error.
- `testMode` (`boolean`): Shows a test-mode badge, a simulated-payment note and a “Use test card” helper.

### Accessibility

- Plans, interval and payment method are radio groups with aria-checked. The seat stepper has labelled buttons and an editable input.
- Every field has a label. Errors are tied to their field with aria-describedby and aria-invalid.
- The summary and total are polite live regions, so changes are announced.
