Skip to content

feat(ENG-294): make polling the default OAuth completion mechanism#155

Merged
adefreitas merged 4 commits into
StackOneHQ:mainfrom
adefreitas:ENG-294/polling-default-remove-window-communication
May 12, 2026
Merged

feat(ENG-294): make polling the default OAuth completion mechanism#155
adefreitas merged 4 commits into
StackOneHQ:mainfrom
adefreitas:ENG-294/polling-default-remove-window-communication

Conversation

@adefreitas

@adefreitas adefreitas commented May 12, 2026

Copy link
Copy Markdown
Collaborator

Summary

  • Drop the popup→hub postMessage / BroadcastChannel / localStorage signalling. Polling GET /hub/connection_attempts/{id} is now the only OAuth completion signal.
  • Require connection_attempt_id on the OAuth redirect; hard-fail with a user-visible error if createConnectionAttempt doesn't return one.
  • Add a 5-min poll timeout that cancels the attempt server-side and surfaces a <Alert type="warning" /> banner ("Connection timed out. Please try again.") at the top of the picker.
  • Re-add a lightweight popup-close watcher (1 s tick) for the non-COOP path: when the popup closes before OAuth completes, the watcher does one final poll — if the result is authenticated it fires handleSuccess, otherwise it cancels the attempt and resets state. COOP-affected environments still fall through to the 5-min banner.
  • Resolve the watcher-vs-poll race by re-checking pollingIntervalRef after every await pollConnectionAttempt(...), so whichever path observes the terminal status first wins and the other bails — no double-cancel, no double-fire of onSuccess.
  • Trim EventType to just AccountConnected (the other members were no longer referenced anywhere in the repo and weren't re-exported from src/index.ts).
  • Dev sandboxes (Vite default tab, Vite Suspense MRE, Next.js wrapper) now print Environment — api: <apiUrl> · app: <appUrl> above the manual connect-session-token input so it's obvious which environment the pasted token belongs to.

Net diff: 5 files changed, +121/−212.

Test plan

  • Happy path: open the OAuth popup, complete OAuth, confirm the polling endpoint flips to authenticated and the success view renders.
  • User-dismiss (non-COOP): click Connect, manually close the popup before authenticating, confirm the picker resets to idle within ~1 s and the attempt is cancelled server-side.
  • User-dismiss after success (race): complete OAuth, then close the popup before the next 2 s poll tick fires; confirm the success view renders (not the cancel branch).
  • COOP-blocked: simulate COOP by ensuring popup.closed === true right after window.open (or run in an environment that enforces it), close the popup early, confirm polling keeps running, then wait 5 min and confirm the warning banner renders.
  • Popup blocked by browser: confirm the existing "Popup blocked" error path still triggers and the in-progress attempt is cancelled.
  • Hub embedded as iframe: confirm the embedder still receives parent.postMessage({ type: 'AccountConnected', account: { id, provider } }) on success.
  • Run both dev sandboxes (Vite on :3001, Next.js on :3002) and verify the new "Environment — api/app" line shows the right URLs.

Copilot AI review requested due to automatic review settings May 12, 2026 11:26

@claude claude 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.

Claude Code Review

This pull request is from a fork — automated review is disabled. A repository maintainer can comment @claude review to run a one-time review.

Replace the previous postMessage/BroadcastChannel/localStorage signalling
between the OAuth popup and the hub with the polling endpoint as the sole
completion signal. Removes ~200 LOC of cross-window communication paths,
keeps a popup-closed watcher (gated by COOP detection) for fast cancel on
non-COOP browsers, and adds a 5-min poll timeout that surfaces a warning
banner at the top of the picker.

Race between the popup-close watcher and a just-completed poll is
resolved by re-checking pollingIntervalRef after every await, so whichever
path observes the terminal status first wins and the other bails without
double-cancelling or double-firing onSuccess.

Dev sandbox (Vite default tab, Vite Suspense MRE, Next.js) now shows the
resolved API/app URLs above the manual connect-session-token input so
it's obvious which environment the token belongs to.
@adefreitas adefreitas force-pushed the ENG-294/polling-default-remove-window-communication branch from 979da09 to bcdba57 Compare May 12, 2026 11:27
Comment thread src/modules/integration-picker/hooks/useIntegrationPicker.ts Outdated

Copilot AI 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.

Pull request overview

This PR updates the integration picker OAuth completion flow to rely solely on polling /hub/connection_attempts/{id}, removing prior popup→hub signaling mechanisms and adding UX/behavior for timeouts and early popup closes.

Changes:

  • Replace popup signaling (postMessage/BroadcastChannel/localStorage) with polling as the single OAuth completion mechanism.
  • Add a 5-minute polling timeout that cancels the connection attempt and surfaces a warning banner in the picker.
  • Improve dev sandboxes by displaying the active api/app environment URLs above the token input.

Reviewed changes

Copilot reviewed 5 out of 5 changed files in this pull request and generated 4 comments.

Show a summary per file
File Description
src/modules/integration-picker/IntegrationPicker.tsx Displays a timeout warning banner when polling exceeds the timeout.
src/modules/integration-picker/hooks/useIntegrationPicker.ts Implements polling-only OAuth completion, timeout cancellation, and popup-close watcher logic.
dev/vite/SuspenseMRE.tsx Prints api/app environment URLs in the Vite Suspense MRE sandbox UI.
dev/vite/main.tsx Prints api/app environment URLs in the Vite sandbox wrapper UI.
dev/nextjs/app/HubWrapper.tsx Prints api/app environment URLs in the Next.js sandbox wrapper UI.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread src/modules/integration-picker/hooks/useIntegrationPicker.ts
Comment thread src/modules/integration-picker/hooks/useIntegrationPicker.ts Outdated
Comment thread src/modules/integration-picker/hooks/useIntegrationPicker.ts Outdated
Comment thread src/modules/integration-picker/hooks/useIntegrationPicker.ts Outdated
- handle cancelConnectionAttempt rejections with debug-logged .catch (4 sites)
- build OAuth popup URL with URLSearchParams instead of manual concatenation
- log final poll failure in the popup watcher on debug
The /connect/oauth2 endpoint matches the token value in its raw form,
so percent-encoding the base64 padding `=` as `%3D` (which URLSearchParams
does automatically) causes the backend to reject the token. The popup
opens, closes immediately, and the watcher resets the picker to idle.

Reverting to the literal template-string concatenation. The .catch fixes
on cancelConnectionAttempt and the watcher's final poll stay.
…dow.open

The synchronous coopDetectedRef check after window.open always saw
about:blank — the destination URL's COOP header hadn't been applied yet,
so the flag was always false. ~1s later the popup loads api.stackone.com
which sets COOP, Chrome severs the parent's reference, and popup.closed
starts returning true falsely. The watcher then fires the cancel branch
mid-OAuth and resets the picker to its idle/edit state.

Move the COOP check to the watcher's first tick: by then the destination
URL has loaded and any COOP header is in effect, so popup.closed === true
strongly implies isolation rather than a real user-close (the user can't
realistically close a freshly-opened popup in <1s anyway).

Trade-off: a user who closes the popup within the first second on a
non-COOP destination will now be classified as COOP and won't get the
snappy cancel — they'll hit the 5-min poll timeout banner instead. Worth
it to keep the live OAuth flow from being killed under COOP, which is
the much more common case in practice.
@adefreitas adefreitas merged commit 9be098d into StackOneHQ:main May 12, 2026
3 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