Skip to content
Payment Emulator LabDocumentationOpen console

Frontends

The checkout

apps/checkout is what a buyer sees. It is a separate Nuxt application from theconsole on purpose: a different person, a different surface, a different sessionsealed with a differen

5 min read

Índice / Index · Español

apps/checkout is what a buyer sees. It is a separate Nuxt application from the console on purpose: a different person, a different surface, a different session sealed with a different secret. One secret doing both jobs would let one cookie be the other.

It is public. There is no operator token anywhere in it, and no control-plane route is reachable from it.

#Why it wears each provider's face

A developer integrating a gateway has to recognise what their buyer will see. A single generic form with the provider's name in the corner teaches nothing about the redirect, the method list or the flow — and this project exists so that an integration built against the emulator survives contact with the real thing.

So a provider has two halves, and they never import each other:

Half Lives in Holds
Behaviour apps/api/src/providers/<id>/ Routes, validation, state vocabulary, signing
Identity apps/checkout/providers/<id>/ The visual kit: the component and its theme

What the checkout needs to know about a provider — display name, payment methods, canonical states — it reads from the API at runtime. Never from a shared type, because a shared type would make the split a formality.

#Its shape

text
app/
  pages/
    mercadopago/checkout/[resourceId].vue   the paths the real providers use
    paypal/checkoutnow.vue
    stripe/c/pay/[resourceId].vue
  components/          the shell: summary, methods, payer, outcome
  core/gateway.ts      what the API says about this checkout
  core/kits.ts         provider id → visual kit
providers/
  mercadopago/         Checkout.vue + theme.css
  stripe/              Checkout.vue + theme.css + logo.svg
  paypal/              Checkout.vue + theme.css + logo.svg

core/kits.ts is the registry, and it is the reason no shared component ever branches on a provider id. A provider with no kit falls back to the shell's own plain form — a usable checkout, not a blank page.

These are local emulator routes inspired by provider paths. A Stripe PaymentIntent is not a Checkout Session, and a Mercado Pago Order is not a checkout preference. Neither page proves compatibility with the provider's hosted SDK or redirect flow. PayPal has a review and approval kit at /paypal/checkoutnow?token=...; approval moves no money. Return and cancel links come from the API. See its fidelity report for the supported flow and limits.

Stripe uses a two-column merchant summary and card form, stacked on mobile. Mercado Pago uses a blue/white header and stacked payment choices. Test-card data is read-only; technical IDs and rejection controls live in expandable test details. Link and ACH are not offered by Stripe; regional Mercado Pago choices follow the configured site. The session form appears only for a method that spends internal balance. No kit claims pixel equality with every country/account variant.

#Rules

  • No PrimeVue here. PrimeVue is the console's design system. A buyer-facing page that inherited an admin panel's component library would look like an admin panel, and each provider's kit could not be itself. The checkout styles itself.
  • The API is the only contract. Payment methods, currency, amounts and state come from GETting the checkout; nothing is hard-coded per provider outside its kit.
  • Copy is available in Spanish and English, in the checkout's own typed catalog. Code and comments are in English.
  • Accessibility is not optional. The method list is a role="radiogroup" driven with the arrow keys, every control has a real <label>, focus is visible, and touch targets are at least 44px. A payment form that cannot be completed with a keyboard is a broken payment form.

#The buyer's session

A buyer signs in with their own email and password against the API, exactly as a person does in the console — the difference is the role. A customer reaches the checkout and not the console; the API decides that, not the interface.

Paying from a wallet balance debits the ledger; paying by card, ticket or transfer brings money in from outside it. Which one a method does is the funding_source recorded with the payment, so a refund months later does not depend on a plugin's method list still saying the same thing today.

The three kits bundle the SVG beside their llms source. The icon workflow also covers the console cards and selectors.

#Language and simulation notice

app/core/i18n/checkout.ts owns ES/EN copy for all three kits, methods, optional sign-in, outcomes, errors and titles. Brand names, references, amounts and technical statuses received from the API retain their original values.

Language is resolved on the server: ?lang=es|en → the emulator_checkout_locale cookie → Accept-Language (regions and q weights) → es. Nuxt serializes the result so hydration uses the same language. The ES/EN control writes a one-year, SameSite=Lax, path / cookie and updates lang while preserving other parameters, including PayPal's token. The cookie is independent of the console and the HTML lang attribute follows the choice.

CheckoutFrame places a static <aside> before the kit, using --caution-* tokens: “These payments are fictitious.” and an instruction not to enter real data. It does not use role="alert". Index and error pages also show the notice.

The watermark is a two-line SVG, rotated −30°, fully encoded with encodeURIComponent and exposed as --wm. Its tile starts at 420 × 260 px and grows with the translation length. It repeats as a CSS mask on .checkout::before, with opacity 0.07 and --wm-color ink: this lets CSS control the color, which an SVG in background-image cannot inherit. It adds no DOM nodes, intercepts no events and stays behind opaque panels. A kit can tint it using .checkout:has(.kit-<id>) { --wm-color: ... } in its own theme.css. On mobile the visible notice carries the message when the card covers the tile.

Validation: npm run test:checkout covers negotiation, catalog parity and SVG encoding. scripts/checkout-browser.mjs checks SSR, hydration, language switching, overflow and axe; it saves all three kits in both languages at desktop and mobile sizes. It requires Playwright, axe and a running local stack. npm run smoke:stack validates the complete payment flow.

Payment Emulator Lab · RonuSoftwareMIT