Skip to content

Re-enable buttons that leave the site after Back (AUTH-15, AUTH-16) - #194

Merged
Programcode merged 2 commits into
mainfrom
fix/t1-bfcache-redirect-buttons
Oct 22, 2026
Merged

Programcode merged 2 commits into
mainfrom
fix/t1-bfcache-redirect-buttons

Conversation

@Programcode

@Programcode Programcode commented Oct 22, 2026 •

Copy link
Copy Markdown
Owner

Fixes AUTH-15 and AUTH-16 from the 22 Oct 2026 QA report. Four buttons that leave the site (Continue with Google, Upgrade Now, Manage subscription, Connect Google Drive) stayed on "Redirecting..." and disabled after the user pressed Back. Each click handler set that flag before a full-page navigation and cleared it only on error. Chrome and Safari restore the page from the back/forward cache with React state as it was, so the button stayed stuck until a reload.

What changed

The new hook useExternalRedirectPending owns the flag. run(helper) sets it, awaits the redirect helper, and clears it if the helper throws. The caller's own catch then runs as before. After the helper has called location.assign, these clear it:

  • a pageshow with persisted: true (the back/forward cache restore),
  • the page becoming visible again after it was hidden,
  • an 8 second safety timeout,
  • reset(), which PricingModal and GoogleDriveAuthModal call when they close, because both stay mounted while closed.

While the helper is still running, nothing clears the flag, reset() included. That covers the create-checkout and create-portal calls and building the OAuth URL. One click sends exactly one request, even if the dialog is closed and reopened mid-request.

AUTH-15. LoginPage "Continue with Google" uses the hook.

AUTH-16. PricingModal "Upgrade Now", SettingsPage "Manage subscription", and GoogleDriveAuthModal "Connect Google Drive" use it. The two dialogs reset on close. Email "Send code" and "Demo code" don't leave the site and are unchanged.

No migrations, Edge Functions, config, or translation keys change. Nothing needs applying or deploying beyond the normal frontend deploy on merge.

Codex review

  • Round 1 (4638e49), medium: closing the dialog called reset(), which released the one-click guard while create-checkout was still in flight, so close, reopen and click again could start a second Checkout session. Drive had the same path. Fixed in d299964: reset() does nothing until the helper has finished. A reopened dialog shows "Redirecting..." until the request navigates away or fails. Cancelling instead wasn't an option because the helper ends in location.assign. The generation counter this made unnecessary is gone. Two new tests cover it: one for the hook, one for close, reopen and click again in PricingModal. Both fail against the 4638e49 hook.
  • Round 2 (d299964): no findings. Codex checked the diff against the repo standards, all four callers, the duplicate-request guard, event ordering, the timeout, StrictMode effect replay and unmount cleanup.

Verification

npm run verify at d299964, run locally (GitHub Actions minutes are used up, so CI did not run):

  • docs:check, lint, typecheck: pass
  • npm test: 64 files, 436 tests passed
  • test:collaboration: 5 files, 30 tests passed
  • test:edge: 399 passed, 0 failed
  • build and check-site: pass (47 HTML files, no problems)
  • test:sw (service worker offline): pass
  • e2e smoke (desktop Chromium): 43 passed

This is a local pass, not a CI pass. The first local run stopped at test:sw because no Chromium build was installed for Playwright 1.63.0 on the VPS. After npx playwright install chromium, everything passed.

New tests:

  • use-external-redirect-pending.test.tsx: a persisted pageshow clears the flag; two clicks send one request; tab switches and the timeout don't clear it while the request is running; the timeout clears it after navigation; an error clears it and rethrows; reset() during a running request keeps the second click blocked.
  • login-google-redirect.test.tsx: Continue with Google is disabled while redirecting, a second click sends nothing, and after a back/forward cache restore the button is enabled and a click starts a new sign-in.
  • pricing-modal-redirect.test.tsx: Upgrade Now works again after a restore; it is enabled after close and reopen; a close and reopen while checkout is still loading sends one request.

The LoginPage test and the first two PricingModal tests fail against the original components. I also broke the current hook on purpose four ways: letting the visibility reset fire while the request runs, letting reset() always clear, dropping the pageshow reset, and dropping the double-click guard. Each time at least one of these tests failed.

Needs a manual check in a real browser

Playwright runs Chromium with the back/forward cache disabled. I relaunched it with that flag removed, against a production build pointed at a local stub Supabase. The page still loaded fresh on Back instead of being restored, so this PR has no automated proof of the actual Back case. Chrome's notRestoredReasons said only masked. The full-Chromium run without request interception named the Jam recorder iframe (recorder.jam.dev/capture) as the masked frame. That doesn't show Jam blocks the cache on prod; QA saw the restore 3 times out of 3. Please check by hand:

  1. In Chrome, open /login, click Continue with Google, and press Back on Google's account chooser. The button should read "Continue with Google", and a click should reach Google again.
  2. Same in Safari.
  3. On staging, open Settings, click Upgrade, then Upgrade Now, and press Back on Stripe Checkout without paying. Upgrade Now should be enabled, and still enabled after closing and reopening the dialog.

🤖 Generated with Claude Code

Summary by CodeRabbit

  • Bug Fixes
    • Google sign-in, subscription checkout, and billing-portal redirects now show consistent pending states. Returning to the app after navigation or reopening a pricing dialog restores the relevant action when it is available.
    • Duplicate sign-in or checkout requests are prevented while an earlier request is still pending, and failed navigation no longer leaves the interface stuck in a loading state.
  • Tests
    • Added coverage for redirect recovery, retry behavior, and pending-state handling.

Programcode and others added 2 commits October 22, 2026 20:11
Continue with Google, Upgrade Now, Manage subscription and Connect Google
Drive set a "Redirecting..." flag before a full-page navigation and only
cleared it on error. When Back restored the page from the back/forward
cache, React state came back frozen and the button stayed disabled until a
reload.

A new useExternalRedirectPending hook owns that flag. It clears it on a
persisted pageshow, when the page becomes visible again after the
navigation started, and after an 8 second safety timeout. The visibility
and timeout resets wait for the request to finish, so one click still
sends one request. The pricing and Drive dialogs stay mounted while
closed, so they also reset the flag when they close.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Closing the pricing or Drive dialog called reset(), which released the
button while create-checkout or the OAuth URL request was still in
flight. Close, reopen and click again then started a second Checkout
session. reset() now does nothing until the helper has finished, which
also makes the generation counter unnecessary.

Found by the Codex review of 4638e49.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Copilot AI balanced review requested due to automatic review settings October 22, 2026 20:29
@coderabbitai

coderabbitai Bot commented Oct 22, 2026 •

Copy link
Copy Markdown

Review in Change Stack →

ℹ️ Recent review info
⚙️ Run configuration
📥 Commits

Reviewing files that changed from the base of the PR and between ce3836e and d299964.

📒 Files selected for processing: 9
  • src/components/features/export/GoogleDriveAuthModal.tsx
  • src/components/features/subscription/PricingModal.tsx
  • src/hooks/use-external-redirect-pending.test.tsx
  • src/hooks/use-external-redirect-pending.ts
  • src/pages/LoginPage.tsx
  • src/pages/SettingsPage.tsx
  • src/test/auth/login-google-redirect.test.tsx
  • src/test/billing/pricing-modal-redirect.test.tsx
  • src/test/helpers/page-lifecycle.ts

Included review availability: This review used your included allowance. Your plan provides up to 1 included review per hour; 0 remain after this review.

📝 Walkthrough

A shared useExternalRedirectPending hook now tracks external navigation state. Login, billing, and Google Drive flows use it. Tests cover navigation failures, duplicate calls, page lifecycle restoration, timeouts, and modal close and reopen behavior.

Changes: External redirect tracking

Layer / File(s) Summary
Track external redirect lifecycle
src/hooks/use-external-redirect-pending.ts, src/hooks/use-external-redirect-pending.test.tsx, src/test/helpers/page-lifecycle.ts
The hook tracks pending navigation, blocks duplicate runs, clears state after errors or qualifying page lifecycle events, and schedules a safety timeout. Tests cover these behaviors.
Use shared redirect state
src/pages/LoginPage.tsx, src/pages/SettingsPage.tsx, src/components/features/export/GoogleDriveAuthModal.tsx, src/components/features/subscription/PricingModal.tsx, src/test/auth/login-google-redirect.test.tsx, src/test/billing/pricing-modal-redirect.test.tsx
These components use the hook to track redirect activity. The modals reset pending state when closed. Tests cover login restoration and checkout restoration, reopening, and duplicate prevention.

Estimated code review effort: 3 (Moderate) | ~20 minutes

Sequence Diagram(s)

sequenceDiagram
  actor User
  participant LoginPage
  participant useExternalRedirectPending
  participant signInWithGoogle
  participant Browser
  User->>LoginPage: Select Continue with Google
  LoginPage->>useExternalRedirectPending: run sign-in
  useExternalRedirectPending->>signInWithGoogle: Start authentication
  Browser-->>useExternalRedirectPending: Restore persisted page
  useExternalRedirectPending-->>LoginPage: Clear pending state
Loading
  • Autofix · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts

Note

🎁 Summarized by CodeRabbit Free

Your organization is on the Free plan. CodeRabbit will generate a high-level summary and a walkthrough for each pull request. For a comprehensive line-by-line review, please upgrade your subscription to CodeRabbit Essentials by visiting https://app.coderabbit.ai/settings/billing.

Comment @coderabbitai help to get the list of available commands.

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

@Programcode
Programcode merged commit 5770299 into main Oct 22, 2026
3 of 9 checks passed
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.

2 participants