Try first
Frame your answer before reading the reference
Candidate prompt
Design the browser checkout and payment flow for a cart whose prices, discounts, tax, shipping, and stock can change. A customer may double-click Pay, lose the creation response, return from bank authentication, or continue the same checkout in another tab. Assign authority among client state, merchant backend, and payment provider. Explain how stable attempt identity, quote conflicts, status recovery, hosted card fields, accessible errors, and cross-tab signals prevent duplicate charges or false success.
Constraints
- Server quote sets totals and stock.
- One payment keeps one attempt identity.
- Redirects never prove payment success.
- Card secrets stay inside provider fields.
Helpful before you start
- Async form state
- HTTP retry basics
- Hosted payment fields
What good looks like in a 15-minute first pass
Must cover
- Authoritative quote version gates every payment attempt.
- Stable attempt and idempotency identities survive transport uncertainty.
Strong signals
- Lost responses trigger status reads or same-key retries, never a fresh charge.
- Accessible errors preserve input; cross-tab messages only accelerate server reconciliation.
Stretch if time
Webhook-led fulfillment and multi-tab supersession without browser authority.
Avoid
Redirect success or tab locks are treated as payment truth.
Need a hint? Three decisions to make
- Separate editable checkout input, authoritative quote, payment attempt, and order state.
- Reuse attempt and idempotency identities when a creation response is uncertain.
- Reconcile redirects, webhooks, and cross-tab observations through merchant server reads.
Guided mock requires a tablet or desktop viewport of at least 768px. It will not start automatically.
Requirements
- Name the customer outcomeA customer reviews a cart, enters delivery details, receives a current total, and authorizes one payment. Success means the merchant backend confirms the order; a provider redirect is insufficient. Follow one attempt through double-click, a lost response, bank authentication, and another tab so each decision is observable.
- Fix the authority boundaryThe browser owns editable form input and presentation. The merchant backend owns versioned price, discount, tax, shipping, stock, and checkout-to-order relationships. The merchant backend is an explicit frontend contract. The provider owns card collection and payment execution. Provider internals stay a black box; fulfillment is outside frontend scope.
- Preserve one payment identityOn Pay, freeze the accepted quote version and create a clientAttemptId with one idempotencyKey. Disable repeated activation for usability, but expect duplicate events. If the response disappears, read by clientAttemptId; retransmission uses the same immutable payload and key. A new identity is allowed only after merchant status proves a terminal no-charge state: failed, canceled, or quote-conflict.
- Reconcile uncertain outcomesSubmitting can become requires-action, processing, succeeded, failed, or quote-conflict. After bank return, refresh, reconnect, or a sibling-tab message, read merchant attempt and order state. Redirect parameters and SDK callbacks are hints, never proof. A changed quote returns to review with corrected totals and preserved form input.
- Close with trust and accessHosted provider fields keep PAN and CVC outside application state, logs, analytics, and storage. Validation describes problems in text, links fields, focuses a summary when appropriate, and preserves correctable input. Cross-tab messages prompt refresh, but only server reads settle conflicts. Completion requires confirmed order state.
| Concern | Authority | Browser responsibility |
|---|---|---|
| Customer details | Customer before submission; server after requests | Preserve input and label errors |
| Price, discount, tax, shipping, stock | Versioned server quote | Display quoteId and quoteVersion without recalculating totals |
| Card number and security code | Hosted provider fields | Request a reference without reading PAN or CVC |
| Payment and order outcome | Merchant backend after provider reconciliation | Refresh status and render truthful recovery or completion |
Architecture
| Owner | Keeps | Does not claim |
|---|---|---|
| Checkout form | Contact, shipping, consent, error UI, and hosted-field mounts | Payable totals, card secrets, or final payment state |
| Checkout coordinator | Current quote and attempt, cancellation request, recovery, and navigation | Permission to replace an uncertain attempt |
| Merchant backend | Quotes, attempt lookup, idempotency, provider mapping, orders, and webhooks | PAN or CVC handled by hosted provider fields |
| Payment provider | Hosted inputs, authentication, and payment execution | Merchant order completion |
- QuoteShipping S1 produces quote q_18 version 4. The coordinator displays its server total and stores its identifiers, expiry, and lines. A local preview never changes the payable amount.
- CreateThe first Pay intent creates ca_7 and idem_ca_7. The coordinator freezes quote version 4 and the payment-method reference, then submits once. A double-click cannot create another command.
- RecoverThe response disappears after server acceptance. The UI shows checking and calls getAttempt with ca_7. A permitted retransmission repeats the unchanged command with idem_ca_7; uncertainty never creates ca_8.
- AuthenticateAttempt ca_7 requires bank action. The adapter opens authentication. On return, the route restores checkoutId and ca_7 and reads server status; provider values cannot confirm the order.
- ConfirmA provider webhook lets the merchant advance order o_31. The next status read reports succeeded. The coordinator renders the receipt and broadcasts an advisory observation.
Read diagram as text
Text fallback: The form requests a versioned quote. Hosted fields return a payment-method reference. The backend creates one idempotent attempt; authentication and a webhook may follow. The browser reads merchant order status.
Data
type CheckoutPhase =
| 'quoting'
| 'ready'
| 'submitting'
| 'requires-action'
| 'processing'
| 'succeeded'
| 'failed'
| 'canceled'
| 'quote-conflict';
interface CheckoutLineItem {
productId: string;
quantity: number;
unitPriceMinor: number;
lineTotalMinor: number;
}
interface CheckoutQuote {
quoteId: string;
quoteVersion: number;
currency: string;
lineItems: CheckoutLineItem[];
subtotalMinor: number;
discountMinor: number;
taxMinor: number;
shippingMinor: number;
totalMinor: number;
expiresAt: string;
}
interface CheckoutAttempt {
checkoutId: string;
clientAttemptId: string;
idempotencyKey: string;
quoteId: string;
quoteVersion: number;
phase: CheckoutPhase;
orderId: string | null;
failureCode: string | null;
updatedAt: string;
}
interface CheckoutFormState {
email: string;
shippingAddress: Record<string, string>;
shippingOptionId: string | null;
fieldErrors: Record<string, string>;
}
interface CheckoutViewState {
form: CheckoutFormState;
quote: CheckoutQuote | null;
attempt: CheckoutAttempt | null;
phase: CheckoutPhase;
}| Identity | Reuse rule | Reason |
|---|---|---|
| quoteId + quoteVersion | Freeze for one attempt; replace with a new quote | The server can reject stale totals or availability |
| clientAttemptId | Reuse while recovering the same Pay intent | A lost response remains recoverable |
| idempotencyKey | Reuse with the identical attempt payload | Transport retry cannot duplicate provider work |
| orderId | Accept only from merchant status | A return URL cannot imply an order |
Interfaces
interface CreateAttemptInput {
checkoutId: string;
quoteId: string;
quoteVersion: number;
clientAttemptId: string;
idempotencyKey: string;
paymentMethodReference: string;
returnUrl: string;
signal: AbortSignal;
}
interface AttemptStatusResponse {
attempt: CheckoutAttempt;
requiredAction: { providerActionToken: string; expiresAt: string } | null;
}
interface OrderStatus {
orderId: string;
checkoutId: string;
status: 'pending-payment' | 'confirmed' | 'payment-failed';
updatedAt: string;
}
interface CheckoutGateway {
getQuote(input: { checkoutId: string; signal: AbortSignal }): Promise<CheckoutQuote>;
createAttempt(input: CreateAttemptInput): Promise<AttemptStatusResponse>;
getAttempt(input: { checkoutId: string; clientAttemptId: string; signal: AbortSignal }): Promise<AttemptStatusResponse>;
cancelAttempt(input: { checkoutId: string; clientAttemptId: string; signal: AbortSignal }): Promise<CheckoutAttempt>;
getOrder(input: { checkoutId: string; orderId: string; signal: AbortSignal }): Promise<OrderStatus>;
}
interface HostedPaymentFieldAdapter {
mount(input: { container: HTMLElement; onError(message: string): void }): void;
createPaymentMethodReference(input: { signal: AbortSignal }): Promise<string>;
handleRequiredAction(input: { providerActionToken: string; signal: AbortSignal }): Promise<void>;
unmount(): void;
}
type CrossTabCheckoutEvent =
| {
type: 'checkout.quote-observed';
checkoutId: string;
sourceTabId: string;
quoteVersion: number;
observedAt: string;
}
| {
type: 'checkout.attempt-observed';
checkoutId: string;
sourceTabId: string;
clientAttemptId: string;
phase: CheckoutPhase;
observedAt: string;
};| Operation | Required identity | Client behavior |
|---|---|---|
| GET current quote | checkoutId | Replace the snapshot; require review after a version change |
| POST payment attempt | quoteId, quoteVersion, clientAttemptId, and Idempotency-Key | Retain the command until server state is known |
| GET payment attempt | checkoutId and clientAttemptId | Recover timeout, redirect, reconnect, or tab observations; consume action tokens transiently |
| POST cancel attempt | checkoutId and clientAttemptId | Replace only after canceled; otherwise keep recovering the old attempt |
| GET order | Merchant-issued checkoutId and orderId | Complete only when confirmed |
- Quote before authorizationFetch after material cart or shipping changes. Cancel superseded reads, accept only the current input generation, and display returned totals exactly.
- Create through one commandGet an opaque hosted-field reference, freeze the quote pair, generate identities once, and send Idempotency-Key. Retain the command while its outcome is uncertain.
- Read before retryAfter timeout, call getAttempt. A permitted retransmission repeats the same payload and key. Replace it only after merchant status reports failed, canceled, or quote-conflict, including successful cancelAttempt.
- Restore after navigationThe return route carries identities, not success. It reads attempt state, sends action tokens directly to the adapter, and reads order state after a merchant-issued orderId appears.
Optimizations
| Failure | Recovery | Preserved invariant |
|---|---|---|
| Quote changes before Pay | Replace the quote and require review | Only a current server quote enters an attempt |
| Attempt response is lost | Read by clientAttemptId; permitted retry keeps the same Idempotency-Key and payload | One intent maps to at most one provider operation |
| Authentication return arrives first | Show checking and refresh merchant state | Redirect and SDK results never prove success |
| Attempt remains processing | Use bounded, visibility-aware polling plus manual refresh | The UI stays truthful without another charge |
| Customer requests another payment | Read the old attempt; replace only after failed, canceled, or quote-conflict | Processing cannot become an unexpected second charge |
| Provider field rejects input | Keep input and present linked text errors | Recovery stays accessible; card secrets remain excluded |
Cross-tab reconciliation without browser authority
- Older tab leaves for authenticationTab A creates ca_7 on quote version 4 and leaves for authentication. Its identity remains recoverable.
- Newer tab changes shippingTab B gets quote version 5 but cannot replace uncertain ca_7. It proceeds only after merchant status reports failed, canceled, or quote-conflict.
- Return reconciles both identitiesTab A reads ca_7 and the quote. A confirmed order stops Tab B; failure or quote-conflict resumes review.
- Advisory event speeds convergenceAn attempt observation prompts refresh. Crashes strand local locks, and browsers cannot settle webhook truth.
Read diagram as text
Text fallback: Tab A returns for ca_7 while Tab B holds a newer quote. Both read merchant state. Confirmation stops competing work; failure restores review.
Verification, accessibility, and measurement
- Replay Pay; assert one clientAttemptId, one Idempotency-Key, and one merchant attempt.
- Lose creation response; assert lookup or same-key retry reaches the existing attempt.
- Return before webhook reconciliation; assert processing remains and no receipt appears.
- Request replacement while processing; assert no new identity before terminal state or successful cancellation.
- Change quote version; assert review is required and correctable input survives.
- Reorder cross-tab events; assert receivers refresh instead of copying browser claims.
- Check keyboard and screen-reader recovery, linked errors, focus, preserved values, and redacted telemetry.
Use the Question Library for baseline coverage, then move into a Study Plan before targeted Company Prep.