Skip to content

fix(core): open the popup-OAuth window synchronously in the click - #688

Open
AminDhouib wants to merge 1 commit into
devfrom
fix/popup-auth-open-in-gesture
Open

AminDhouib wants to merge 1 commit into
devfrom
fix/popup-auth-open-in-gesture

Conversation

@AminDhouib

Copy link
Copy Markdown
Member

Fixes the "Popup was blocked by the browser" error on the OneDrive / Dropbox / Box sign-in popup. Found through uNotes task T-12111.

Evidence

In uNotes' PostHog, Popup was blocked by the browser appeared as about 21 $exception events from about 20 users in 14 days, mostly Edge on Windows. It is raised by PopupOAuthPlugin.authenticateViaPopup() and only the popup providers use it. Google Drive goes through Google Identity Services and is not affected.

Cause

authenticateViaPopup() did const url = await this.getAuthUrl() before window.open(...). getAuthUrl() awaits the PKCE digest (crypto.subtle.digest), so the open no longer ran inside the user gesture. Strict blockers treat that as an unrequested popup and return null.

Fix

  • authenticateViaPopup() now opens about:blank under the provider's popupName before any await, then builds the auth URL and assigns it to popup.location.href.
  • If building the URL throws, the blank popup is closed and the error is rethrown. An unconfigured clientId is refused before any window opens, so it never flashes a blank popup.
  • If sign-out or a newer attempt replaced the popup while the URL was being built, the stale one is not navigated.
  • The existing AUTH_POPUP_BLOCKED path (window.open returned null) is unchanged, and getAuthUrl() is no longer called when the window was refused.

The React side needed no change. DriveAuthFallback already starts sign-in from the tile click (on mount) and has a "Sign in" button whose click handler calls signIn directly; with the core change the window.open inside both runs in the same task as the gesture. The tests pin both call paths.

Tests

  • popup-oauth-plugin.test.ts: window.open is called synchronously with about:blank while the PKCE digest is still pending; the blank popup is closed and the error rethrown when URL building fails; no PKCE work when the window is refused; no window for an unconfigured provider; a popup closed by sign-out mid-build is not navigated. Four of these fail on the old code. The shared stubPopupWindow helper now records location.href assignments instead of letting them overwrite the simulated post-redirect href.
  • drive-browser-controller.test.ts: signIn() and retryAuth() start authenticateViaPopup() synchronously, with no await in between.
  • drive-auth-decline-reporting.test.tsx: the Sign in button calls onRetry from inside the click event, not from an effect.

Not run locally: pnpm run e2e (needs the MinIO setup) and the full turbo suite. @useupup/core (1893 tests) and @useupup/react unit suites, prettier-check, test:quality, test:scripts, eslint for core and react, and both typechecks were run.

Includes a patch changeset for @useupup/core; the packages are in a fixed group, so the release bumps all nine together.

…en point it at the provider, so Edge stops blocking it
@codesandbox

codesandbox Bot commented Nov 1, 2026

Copy link
Copy Markdown

Review or Edit in CodeSandbox

Open the branch in Web Editor • VS Code • Insiders

Open Preview

@AminDhouib

Copy link
Copy Markdown
Member Author

Review of 4ca57d8: APPROVE

No blocking defect. CI is green on all 18 checks at this head.

  • packages/core/src/drives/popup-oauth-plugin.ts:292-346: window.open('about:blank', popupName, same features) now runs before any await. The only work ahead of it is synchronous (browser check, requireClientId, cleanupPopup), and the unconfigured-client-id refusal happens before a window exists. The AUTH_POPUP_BLOCKED path is unchanged and now also skips the PKCE build. Window name and features are identical to before.
  • Popup lifecycle: a getAuthUrl() failure closes the blank popup and rethrows (:333-339). A popup superseded or signed out mid-build is not navigated (:343). Every later path (redirect, error redirect, poll failure, user closing the window) goes through the unchanged cleanupPopup at :356/391/442/518.
  • Origin and redirect handling: this plugin uses no postMessage. It polls popup.location.href and matches redirectUri. A blank popup reads as same-origin about:blank, which cannot match redirectUri. After navigation to the provider the read throws cross-origin and is swallowed exactly as before. Nothing about the origin check regresses.
  • Gesture claim holds, and no react change was needed. DriveAuthFallback's button calls onRetry directly (DriveAuthFallback.tsx:~95), and signIn/retryAuth -> runPopupAuth -> authenticateViaPopup reaches window.open with no await in between (drive-browser-controller.ts:416-442). The mount auto-start effect is run from the tile click's render, which React 18 flushes at the end of that discrete event, and in any case the open is now synchronous inside signIn, so it falls inside the ~5 s transient activation. If an auto-start is still blocked, error is set, which suppresses further auto-start and leaves the button as a click-initiated retry.
  • Tests are meaningful. I ran the head tests against the old popup-oauth-plugin.ts in a scratch copy: exactly the 4 behavioural tests fail (sync open before the digest settles, close blank popup on build failure, no PKCE when the window is refused, no navigation after sign-out mid-build). On head, core popup-oauth-plugin + drive-browser-controller give 71/71 passing. The controller test asserts authenticateViaPopup is called with no await after signIn()/retryAuth(). The react test records window.event?.type === 'click' inside onRetry, so a deferred call would fail it. The react and controller tests are guards, not regressions for this change, which is fine given no react change. The "unconfigured clientId opens no window" test also passes on the old code.
  • Security: no verifier, state or URL regression. codeVerifier is still set inside getAuthUrl, the URL is not logged, and the blank window carries nothing sensitive.
  • Changeset: '@useupup/core': patch sits in the fixed group, so all nine packages bump together. It matches the other changesets.

Non-blocking nits, no action required:

  1. popup-oauth-plugin.ts:343: a superseded or signed-out attempt returns undefined silently. runPopupAuth then records "Sign-in did not complete" for it. This is only reachable by a double activation within the PKCE digest (a few ms), and the comment explains it.
  2. :348: if the user closes the popup during the digest, popup.location.href = url runs on a closed window. The worst case is a loud rejection, handled in runPopupAuth's finally. A popup.closed guard would be tidier, but it is not worth blocking on.

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