Repository navigation
Re-enable buttons that leave the site after Back (AUTH-15, AUTH-16) - #194
Conversation
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>
ℹ️ Recent review info
📝 Walkthrough
Note 🎁 Summarized by CodeRabbit Free
Comment |
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
useExternalRedirectPendingowns 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 calledlocation.assign, these clear it:pageshowwithpersisted: true(the back/forward cache restore),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 thecreate-checkoutandcreate-portalcalls 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
reset(), which released the one-click guard whilecreate-checkoutwas 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 inlocation.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.Verification
npm run verifyat d299964, run locally (GitHub Actions minutes are used up, so CI did not run):npm test: 64 files, 436 tests passedtest:collaboration: 5 files, 30 tests passedtest:edge: 399 passed, 0 failedtest:sw(service worker offline): passThis is a local pass, not a CI pass. The first local run stopped at
test:swbecause no Chromium build was installed for Playwright 1.63.0 on the VPS. Afternpx playwright install chromium, everything passed.New tests:
pageshowclears 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.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 thepageshowreset, 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
notRestoredReasonssaid onlymasked. 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:🤖 Generated with Claude Code
Summary by CodeRabbit