Skip to content

app: use popup OAuth flow so Google/Apple sign-in works on Safari#970

Merged
bpowers merged 2 commits into
mainfrom
safari-oauth-popup
Jul 17, 2026
Merged

app: use popup OAuth flow so Google/Apple sign-in works on Safari#970
bpowers merged 2 commits into
mainfrom
safari-oauth-popup

Conversation

@bpowers

@bpowers bpowers commented Jul 17, 2026

Copy link
Copy Markdown
Owner

Google sign-in on app.simlin.com worked in Chrome but silently failed on iOS Safari: after returning from Google, the user was dropped back on the login screen with no error.

Root cause: signInWithRedirect's result pickup loads an auth.simlin.com iframe that must read storage the sign-in helper wrote while it was the top-level page. WebKit partitions iframe storage per-origin, while Chromium partitions per-site, so the same-site custom authDomain (auth.simlin.com under app.simlin.com) only rescues Chromium-family browsers. On Safari the iframe sees an empty partition and getRedirectResult reports null with no error anywhere (firebase/firebase-js-sdk#7824; WebKit's per-origin partitioning is confirmed in https://webkit.org/blog/14403/updates-to-storage-policy/ and https://developer.apple.com/forums/thread/725074).

Fix: use signInWithPopup, which delivers the result to the opener via postMessage and needs no storage shared with the auth domain, so it works under per-origin partitioning. signInWithRedirect remains only as a fallback when a popup blocker rejects the popup (the provider buttons are direct user gestures, so blocking is rare); a user closing the popup is treated as a cancel, not an error. A side effect is that OAuth should now also complete on cross-site canary hosts, which the redirect flow structurally never could -- docs/dev/deploy.md is updated to reflect the new flow and the remaining canary caveat.

Verified end-to-end against the dev stack (auth emulator): popup opens with authType=signInViaPopup, sign-in completes, the /session exchange succeeds, and the new-user dialog renders. Real-device Safari verification still needs to happen on app.simlin.com after this deploys.

https://claude.ai/code/session_01EQoRGRGfkerDZapRpyrgAJ

signInWithRedirect's result pickup loads an auth.simlin.com iframe that
must read storage the sign-in helper wrote while it was the top-level
page. WebKit partitions iframe storage per-origin (not per-site like
Chromium), so on Safari and iOS that iframe sees an empty partition and
the redirect sign-in silently never completes -- getRedirectResult
reports null with no error anywhere (firebase-js-sdk issue 7824). The
same-site auth.simlin.com authDomain only helps browsers that partition
by registrable domain, which is why Chrome worked.

signInWithPopup delivers the result to the opener via postMessage and
needs no shared storage, so it works under per-origin partitioning; it
should also complete on cross-site canary hosts, which the redirect flow
never could. Redirect remains only as a fallback when a popup blocker
rejects the popup (the buttons are direct user gestures, so blocking is
rare); a user closing the popup is treated as a cancel, not an error.
@bpowers

bpowers commented Jul 17, 2026

Copy link
Copy Markdown
Owner Author

@codex review

@chatgpt-codex-connector

Copy link
Copy Markdown

To use Codex here, create a Codex account and connect to github.

@claude

claude Bot commented Jul 17, 2026

Copy link
Copy Markdown

Reviewed the PR: no blocking bugs found.

The popup-first / redirect-fallback flow is cleanly implemented with a shared oauthLoginClick helper, and the special-case handling for auth/popup-closed-by-user, auth/cancelled-popup-request, and auth/popup-blocked matches Firebase's documented error codes. Type-only AuthProvider import is elided at emit time (no verbatimModuleSyntax), so no runtime import concern. Tests cover the popup path, the redirect fallback (both success and rejection), user-cancel paths, and the non-Error rejection generic-message fallback. Docs in deploy.md correctly call out the remaining canary caveat when a popup blocker forces the redirect fallback.

Overall correctness verdict: correct.

@codecov

codecov Bot commented Jul 17, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 91.52%. Comparing base (f9d7365) to head (d473f1e).
⚠️ Report is 1 commits behind head on main.

Additional details and impacted files
@@           Coverage Diff           @@
##             main     #970   +/-   ##
=======================================
  Coverage   91.52%   91.52%           
=======================================
  Files         234      234           
  Lines      156299   156299           
=======================================
+ Hits       143052   143053    +1     
+ Misses      13247    13246    -1     

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@bpowers

bpowers commented Jul 17, 2026

Copy link
Copy Markdown
Owner Author

@codex review

@chatgpt-codex-connector chatgpt-codex-connector Bot 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.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 67c55450e5

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread src/app/Login.tsx
Comment on lines +152 to +153
if (code === 'auth/popup-closed-by-user' || code === 'auth/cancelled-popup-request') {
return;

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Clear stale OAuth errors when a popup is cancelled

If an earlier OAuth attempt has populated emailError, a subsequent attempt that the user closes returns here without clearing that state. The landing screen therefore continues to display the previous failure—potentially even a Google error after cancelling an Apple popup—despite treating the current action as a harmless cancellation. Clear the existing OAuth error when starting an attempt or before returning from these cancellation branches.

Useful? React with 👍 / 👎.

Codex review on PR #970 caught a stale-state path: a failed OAuth
attempt sets emailError, and a later attempt the user cancels returns
early through the popup-closed branch without touching it, so the old
failure stays on screen misattributed to the action the user just
dismissed (even across providers). Clear the error when an attempt
starts rather than in each cancellation branch: a fresh attempt should
always present a clean slate, and a real failure sets a new error
anyway.
@claude

claude Bot commented Jul 17, 2026

Copy link
Copy Markdown

Code Review — reviewed the change end-to-end; no blocking issues found.

Notes:

  • oauthLoginClick correctly narrows Firebase error codes: auth/popup-closed-by-user and auth/cancelled-popup-request are treated as no-op cancels, while auth/popup-blocked triggers the redirect fallback. Every other rejection (including non-Error throws) still lands in emailError via the generic-message fallback, matching prior behavior.
  • The upfront setState({ emailError: undefined }) guarantees a cancelled retry does not leave a prior attempt error on screen — the new test starting a new OAuth attempt clears the previous attempt error, even when cancelled locks in that invariant.
  • The AuthProvider type-only import is safe at runtime: it is only referenced as a parameter type, so TypeScript elides it under the repo isolatedModules setting (no verbatimModuleSyntax).
  • docs/dev/deploy.md is internally consistent: the earlier paragraph describes the bare --no-promote case (host not authorized), while the new paragraph correctly notes that pnpm deploy:canary authorizes the host so the popup flow completes there — the residual caveat about the redirect fallback silently bouncing on the canary is called out.
  • Test coverage matches the new control flow: popup used (both providers), errors surfaced on rejection (Error and non-Error), popup-blocked → redirect fallback (silent), redirect fallback failure surfaced, cancel-then-new-attempt clears stale error, and both cancel-code paths short-circuit without invoking redirect.

Overall correctness: CORRECT. The patch appears free of blocking issues. Real-device Safari verification on app.simlin.com after deploy — already noted in the PR description — remains the outstanding validation step.

@bpowers

bpowers commented Jul 17, 2026

Copy link
Copy Markdown
Owner Author

@codex review

@chatgpt-codex-connector

Copy link
Copy Markdown

Codex Review: Didn't find any major issues. 🚀

Reviewed commit: d473f1e93d

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

@bpowers
bpowers merged commit dc31462 into main Jul 17, 2026
15 checks passed
@bpowers
bpowers deleted the safari-oauth-popup branch July 17, 2026 22:24
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