Skip to content

fix(checkout): reload page on expired session via error discriminator - #14168

Closed
detail-app[bot] wants to merge 1 commit into
mainfrom
detail/bug-fix/fix-checkout-reload-page-on-expired-session-via-er-da2415
Closed

detail-app[bot] wants to merge 1 commit into
mainfrom
detail/bug-fix/fix-checkout-reload-page-on-expired-session-via-er-da2415

Conversation

@detail-app

@detail-app detail-app Bot commented Sep 7, 2026 •

Copy link
Copy Markdown
Contributor

Detail bug report: View on Detail

Summary

Related Issue: polarsource/feedback#434

Reload the checkout page when the session expires while the checkout tab is open. Previously the reload was dead code: Checkout.tsx checked error instanceof ClientResponseError, but the checkout providers downgrade that error to a plain ErrorResponse body before re-throwing, so the instanceof was always false and window.location.reload() never ran.

What

  • Add isExpiredCheckoutError(error) to @polar-sh/checkout (src/utils/errors.ts), re-exported from @polar-sh/checkout/providers. It detects expiry via the thrown body's error.error === 'ExpiredCheckoutError' discriminator.
  • In Checkout.tsx, replace the dead error instanceof ClientResponseError && error.response.status === 410 checks in the update and confirm wrappers with isExpiredCheckoutError(error), and return a never-resolving promise after window.location.reload() so post-reload callers can't race the navigation.
  • Add unit tests for the helper (errors.test.ts, 20 cases) and the first component test for Checkout.tsx (Checkout.test.tsx, 6 cases covering reload-on-expired, no-reload-on-other-errors, loading-state preservation, and rethrow).

Why

A checkout that expired while the tab was left open kept showing a stale UI: confirm did nothing, and seat/unit updates showed a misleading "update failed" error instead of reloading to the expired state.

The bug was a regression introduced when the checkout flow migrated to the internal client: CheckoutProvider was changed to catch ClientResponseError and surface only the plain ErrorResponse body (losing .response and class identity), while Checkout.tsx was changed to check instanceof ClientResponseError — a class the new provider layer no longer lets through. The two sides of the change were not reconciled, and no test covered Checkout.tsx's wrapping layer, so the dead branch survived.

How

The fix detects the expired-checkout condition via the discriminator on the thrown body (error.error === 'ExpiredCheckoutError') — the shape that actually reaches Checkout.tsx after CheckoutProvider → CheckoutFormProvider re-throw the plain body. The never-resolving promise return after window.location.reload() matters specifically for the confirm path: it prevents setFullLoading(false) from briefly clearing the loading indicator before the reload navigates, and keeps update callers (autosave, seat/unit selectors, discount input) from flashing misleading errors. On reload, the server component re-runs and routes the now-expired session to notFound() via its existing 410 handling.

Testing

  • Unit tests (20) for isExpiredCheckoutError and component wiring tests (6) for Checkout.tsx pass; the full @polar-sh/checkout suite (460 tests) is green, including the existing CheckoutFormProvider cases that confirm the provider still throws the plain ExpiredCheckoutError body with no form error set.
  • Typecheck and lint pass clean for @polar-sh/checkout and web (web typecheck required rebuilding the checkout package so the re-export lands in dist).
  • Against the live local stack, verified the server emits the exact {"error":"ExpiredCheckoutError","detail":"This checkout session has expired."} body with HTTP 410 for GET, PATCH (the update path), and POST (the confirm path) on a session forced to expire via the DB — the contract the helper matches.
  • Confirmed navigating an already-expired checkout URL via SSR returns HTTP 404 with Next.js not-found markers, so the reload lands on the expired state; page.tsx is unchanged by this PR.
  • The literal browser click → reload → navigation flow could not be driven end-to-end: the environment has no Playwright browser MCP, and the system chromium runtime is missing the X11/GUI shared libraries it needs to launch (they can't be installed via the host or container package managers here). The reload-on-expired behaviour itself is covered by the component test, which mounts the real Checkout and asserts window.location.reload() is called exactly once with loading state preserved.

Checklist

  • This PR addresses a single concern (one bug fix, one feature, one refactor)
  • The diff is reasonably sized and easy to review
  • New functionality is covered by tests
  • Linting and type checking pass (uv run task lint && uv run task lint_types)
  • No unrelated changes or drive-by fixes are included

Automatic Fixes PRs can be configured here.

Review in cubic

@vercel

vercel Bot commented Sep 7, 2026 •

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated
orbit Ready Ready Preview Sep 7, 2026 1:27am UTC
polar-test Ready Ready Preview Sep 7, 2026 1:27am UTC

Request Review

@pieterbeulque pieterbeulque left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Works for me, though refreshing to a 404 is arguably also weird and confusing (but better than a broken checkout?

This branch was successfully deployed

2 active deployments
Preview – polar-test — 4552923c Deployed Sep 7, 2026 by vercel[bot]
Preview – orbit — 4552923c Deployed Sep 7, 2026 by vercel[bot]
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant