Skip to content

Checkout loader lifecycle ​

Implementation scope: the route-local Paddle payment-link page and the shared plan button's pending treatment. No provider switch, server changes, deployment, production payment, subscription mutation, credentials or CSP widening.

Authoritative Paddle references ​

Verified against the Paddle developer documentation during implementation:

State and recovery decisions ​

  • Loading spans summary, CDN script and the interval between Checkout.open returning and checkout.loaded. Resolving openPaddleCheckout is NOT readiness.
  • The checkout summary uses its own ["billing", "checkout", transaction] snapshot rather than sharing the sidebar credits query's in-flight promise. Otherwise the sidebar's request can bypass the checkout timeout and network-error handling. The sidebar still makes its ordinary summary request even for an invalid link.
  • Summary fetch, script load and overlay startup each have a 20-second bound. These are local UX limits, not Paddle's SLA or a payment deadline. There is no deadline on a loaded overlay while the customer is entering payment details.
  • HTTP, network, SDK-global/script, invalid transaction, disabled/wrong provider, missing/invalid configuration, thrown startup and checkout.error failures render an alert instead of an animation. No arbitrary provider event detail is rendered. The SDK is never requested with an invalid transaction/config/provider.
  • Loaded events are idempotent; completed/closed/failure are terminal. Duplicates and late events cannot restart the animation, cancel a completed checkout, or schedule repeated redirects. Completion retains the existing 1.5-second delay; cancellation retains the full navigation to /billing?checkout=canceled.
  • Retry is an ordinary same-origin link to the validated payment-link URL. It reloads the document instead of calling Initialize a second time. Back to billing is also a full navigation and is available during loading and errors. Leaving via the SPA navigation during SDK load suppresses late initialization; revisiting without a reload offers the same safe recovery rather than re-init.
  • Existing sandbox selection, trusted URL/transaction helpers, missing-email remediation, route-local SDK loading and nginx CSP are preserved.

Accessibility and presentation ​

A lime-accent ring and lock mark match the existing dark dashboard; animation is purely decorative (aria-hidden). A stable polite role=status, atomic text and screen-reader-only neutral label announce "Loading checkout…" once, not again for each asynchronous stage. Loading errors use an alert and clear that status. The spinner is removed at checkout.loaded/terminal/failure, not simply hidden behind the provider. The card keeps the same bounding box between loading and ready. motion-safe:animate-spin with motion-reduce:animate-none leaves a static mark under reduced motion. Selected plan buttons show a small decorative spinner plus "Processing…", retain their plan-specific accessible name, expose aria-busy and disable all plan actions while pending. No full-page loader is used in buttons. Those buttons render inside the shared Billing Upgrade picker, so the two /billing plan-control regressions open that picker first; the pending treatment itself is unchanged by that composition.

Scoped verification ​

Run inside the isolated worktree with Node 24 (system Node 12 is insufficient):

export PATH=/home/nyanpasu/.local/share/fnm/node-versions/v24.22.1/installation/bin:$PATH
corepack pnpm install --frozen-lockfile
corepack pnpm --filter @scrapingcow/ui typecheck
corepack pnpm --filter @scrapingcow/ui build
BROWSER_SWEEP_REQUIRED=1 node --test apps/ui/tests/billing.test.mjs apps/ui/tests/paths.test.mjs apps/ui/tests/billing-routes.test.mjs apps/ui/tests/checkout-loader.test.mjs apps/ui/tests/upgrade-sheet.test.mjs

checkout-loader.test.mjs drives the actual built route and SDK adapter in Chrome. Only API and CDN boundaries are mocked; the SDK mock is intentionally inert until tests fire documented events. Unexpected external requests are aborted, service workers are blocked, and no provider is contacted. Set CHECKOUT_EVIDENCE_DIR to save desktop, mobile, reduced-motion, ready, completed and failure screenshots. The "ready" screenshot shows our background state, NOT a real Paddle overlay. Tests also measure bounding boxes, live-region text mutations, reduced-motion computed styles, safe reload recovery, one-time initialization and terminal events.

Full UI suites and independent integrated-commit desktop/mobile/accessibility and local live-stack verification belong to reviewer t_93d09268. A local mocked pass does not prove provider availability, real overlay rendering, production behavior, webhook delivery or deployment. Manual assistive-technology listening and real sandbox provider compatibility remain distinct from DOM/announcement evidence.