Commit 8c6166e
fix(mship): return the chat connect flow to the tab that started it (#6403)
* fix(mship): return the chat connect flow to the tab that started it
Connecting an integration from a chat credential chip opened OAuth in a
new tab and returned there, so the user landed on a second copy of the
app while the conversation they started from sat stale behind it.
The flow now runs in a popup and returns through a new self-closing page
at /oauth/chat-complete, which publishes its verdict to the shared
attempt record and closes. The chat tab picks that up over its storage
listener and updates in place, so it never navigates. A blocked popup
takes the same route in a new tab and still lands on the completion
page, so both paths share one verdict source.
That verdict is now the server's: reaching the completion page means
Better Auth routed the flow to its success callback. The previous check
diffed the workspace credential list, which reported failure whenever a
user re-authorized an account they had already linked -- that path
updates the account row and creates no new credential.
The lock is stricter than the label. A failure to create the credential
from its draft is swallowed server-side, so a flow can report success
with nothing in the workspace; the row stays retryable unless the
credential actually appears.
Also: the popup is named per attempt so sibling rows cannot renavigate
each other's window; a cross-origin connect URL keeps the anchor's
noopener instead of taking the popup path; the focus verifier reads the
attempt after its refetch so a verdict published mid-flight is not
overwritten; and the verifier treats a popup parked on a terminal page
(/oauth-error, the workspace error exit) as finished rather than
waiting on it forever.
* fix(mship): settle the connect row from the popup, not from focus alone
Addresses the review findings on the chat OAuth return leg.
- Watch the popup on an interval. A provider interstitial bouncing to the
workspace root, a denied consent on /oauth-error, or a closed window all end
the flow without publishing a verdict or firing any event in this tab, so the
row waited forever. The focus handler also no longer consumes the away flag
when it defers to a live popup.
- Focus an already-running popup on a repeat click instead of starting a rival
attempt, which orphaned the first flow's verdict on an attempt id the row had
stopped reading.
- Settle from the refetched credentials on the popup success path, so the row's
lock is corroborated and a connected row stops being clickable.
Extracts the shared refetch-then-decide step into settleFromCredentials, used
by the focus handler, the popup watcher, and the success path.
* fix(mship): never read a disowned popup handle as a finished flow
A provider page with COOP same-origin disowns the popup, and the disowned
handle reports closed for a consent screen still running. The watcher took
that as an ending and published 'failed' against a live flow.
- Replace the boolean with a three-state observation. Only a same-origin
terminal page counts as 'ended'; a closed-or-disowned handle is
'unobservable' and publishes no verdict. Closing a popup hands focus back
to this tab anyway, so the focus verification settles that case.
- Stop the interval before settling. The refetch leaves the status pending
for its duration, so a running interval could fire again and resolve an
attempt a retry had since replaced.
- Gate the success toast on a launched-attempt ref rather than the window
handle, which the watcher clears before React applies the verdict.
* fix(oauth): keep the chat connect return leg in its opener's browsing context
/oauth/chat-complete runs as a popup but fell into the strict COOP rule, so
same-origin moved it into its own browsing-context group the moment it loaded
— disowning it from the tab that opened it. That is the documented cause of a
popup that is not reliably script-closable and whose opener sees window.closed
report true for a live window.
Matches it to its opener's same-origin-allow-popups instead, which is the
directive the platform provides for exactly this case.
* fix(oauth): keep every page an OAuth popup lands on observable to its opener
The popup watcher settles on a same-origin terminal page, but both entries in
OAUTH_POPUP_TERMINAL_PATHS were served strict same-origin COOP, which moves the
popup into its own browsing-context group. The opener could then neither read
its location nor trust window.closed, so the terminal-page branch could never
fire in production and a flow exiting through one of those pages left the row
waiting until the user happened to refocus the tab.
Serves /oauth-error and the /workspace root the same same-origin-allow-popups
their opener uses. The workspace root previously fell under the strict rule
while every /workspace/... route already got the permissive one.
* test(mship): cover the announcement surviving an early popup release
The success toast is gated on the launched-attempt ref rather than the window
handle; nothing pinned that. Adds the regression test, and trims the comment
duplication the fix left behind.
* fix(mship): bound the wait on a popup whose outcome became unobservable
A closed handle and a COOP-disowned one are indistinguishable, so the watcher
published no verdict for either and relied on the focus verification to settle
it. That recovers the normal case — closing a popup hands focus back — but not
one where the opener was never blurred, leaving the row waiting indefinitely.
Arms the same safety timeout the MCP OAuth popup uses for the same reason: past
it, the row decides from the credential list rather than waiting on a verdict
that is never going to arrive. Cleared as soon as a real verdict lands.
* fix(mship): survive a remount while an attempt is still pending
The unobservable deadline lived in the watcher effect's closure, so it was
armed only by the mount that launched the popup. The transcript virtualizes:
a row scrolled away mid-connect came back with no window handle and no blur
behind it, and nothing re-armed the bound.
Derives the deadline from the attempt's own requestedAt and arms it for any
pending attempt, so a remount inherits the time remaining rather than
restarting the clock or losing it. A demonstrably live popup still owns the
flow and is left to the watcher.
* fix(mship): bind a settle to its own attempt and keep the deadline armed
Two races the previous rounds left behind.
A settle read the attempt only after its refetch, so a retry landing during
that window was resolved by a run it never triggered — failing a replacement
whose popup was still going. The attempt id is now captured before the await
and the verdict only lands if it still matches; the status is still re-read
after, so a verdict published mid-refetch is not overwritten.
The safety deadline was one-shot. A consent screen that outlived it consumed
the timeout while still live, leaving nothing to catch the popup dying
unobservably afterwards. It now re-checks at the poll interval instead of
expiring against a live window.
* chore(mship): tighten the comments on the OAuth popup flow
Trims the COOP rationale in next.config.ts to the point, and condenses the
longest blocks in the connect hook without dropping the reasoning a reader
needs to keep the invariants.
---------
Co-authored-by: Waleed Latif <walif6@gmail.com>1 parent b5e5ca5 commit 8c6166e
8 files changed
Lines changed: 1205 additions & 63 deletions
File tree
- apps/sim
- app
- oauth/chat-complete
- workspace/[workspaceId]/home/components/message-content/components/special-tags
- lib/credentials
Lines changed: 135 additions & 0 deletions
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
| 1 | + | |
| 2 | + | |
| 3 | + | |
| 4 | + | |
| 5 | + | |
| 6 | + | |
| 7 | + | |
| 8 | + | |
| 9 | + | |
| 10 | + | |
| 11 | + | |
| 12 | + | |
| 13 | + | |
| 14 | + | |
| 15 | + | |
| 16 | + | |
| 17 | + | |
| 18 | + | |
| 19 | + | |
| 20 | + | |
| 21 | + | |
| 22 | + | |
| 23 | + | |
| 24 | + | |
| 25 | + | |
| 26 | + | |
| 27 | + | |
| 28 | + | |
| 29 | + | |
| 30 | + | |
| 31 | + | |
| 32 | + | |
| 33 | + | |
| 34 | + | |
| 35 | + | |
| 36 | + | |
| 37 | + | |
| 38 | + | |
| 39 | + | |
| 40 | + | |
| 41 | + | |
| 42 | + | |
| 43 | + | |
| 44 | + | |
| 45 | + | |
| 46 | + | |
| 47 | + | |
| 48 | + | |
| 49 | + | |
| 50 | + | |
| 51 | + | |
| 52 | + | |
| 53 | + | |
| 54 | + | |
| 55 | + | |
| 56 | + | |
| 57 | + | |
| 58 | + | |
| 59 | + | |
| 60 | + | |
| 61 | + | |
| 62 | + | |
| 63 | + | |
| 64 | + | |
| 65 | + | |
| 66 | + | |
| 67 | + | |
| 68 | + | |
| 69 | + | |
| 70 | + | |
| 71 | + | |
| 72 | + | |
| 73 | + | |
| 74 | + | |
| 75 | + | |
| 76 | + | |
| 77 | + | |
| 78 | + | |
| 79 | + | |
| 80 | + | |
| 81 | + | |
| 82 | + | |
| 83 | + | |
| 84 | + | |
| 85 | + | |
| 86 | + | |
| 87 | + | |
| 88 | + | |
| 89 | + | |
| 90 | + | |
| 91 | + | |
| 92 | + | |
| 93 | + | |
| 94 | + | |
| 95 | + | |
| 96 | + | |
| 97 | + | |
| 98 | + | |
| 99 | + | |
| 100 | + | |
| 101 | + | |
| 102 | + | |
| 103 | + | |
| 104 | + | |
| 105 | + | |
| 106 | + | |
| 107 | + | |
| 108 | + | |
| 109 | + | |
| 110 | + | |
| 111 | + | |
| 112 | + | |
| 113 | + | |
| 114 | + | |
| 115 | + | |
| 116 | + | |
| 117 | + | |
| 118 | + | |
| 119 | + | |
| 120 | + | |
| 121 | + | |
| 122 | + | |
| 123 | + | |
| 124 | + | |
| 125 | + | |
| 126 | + | |
| 127 | + | |
| 128 | + | |
| 129 | + | |
| 130 | + | |
| 131 | + | |
| 132 | + | |
| 133 | + | |
| 134 | + | |
| 135 | + | |
Lines changed: 66 additions & 0 deletions
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
| 1 | + | |
| 2 | + | |
| 3 | + | |
| 4 | + | |
| 5 | + | |
| 6 | + | |
| 7 | + | |
| 8 | + | |
| 9 | + | |
| 10 | + | |
| 11 | + | |
| 12 | + | |
| 13 | + | |
| 14 | + | |
| 15 | + | |
| 16 | + | |
| 17 | + | |
| 18 | + | |
| 19 | + | |
| 20 | + | |
| 21 | + | |
| 22 | + | |
| 23 | + | |
| 24 | + | |
| 25 | + | |
| 26 | + | |
| 27 | + | |
| 28 | + | |
| 29 | + | |
| 30 | + | |
| 31 | + | |
| 32 | + | |
| 33 | + | |
| 34 | + | |
| 35 | + | |
| 36 | + | |
| 37 | + | |
| 38 | + | |
| 39 | + | |
| 40 | + | |
| 41 | + | |
| 42 | + | |
| 43 | + | |
| 44 | + | |
| 45 | + | |
| 46 | + | |
| 47 | + | |
| 48 | + | |
| 49 | + | |
| 50 | + | |
| 51 | + | |
| 52 | + | |
| 53 | + | |
| 54 | + | |
| 55 | + | |
| 56 | + | |
| 57 | + | |
| 58 | + | |
| 59 | + | |
| 60 | + | |
| 61 | + | |
| 62 | + | |
| 63 | + | |
| 64 | + | |
| 65 | + | |
| 66 | + | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
| 1 | + | |
| 2 | + | |
| 3 | + | |
| 4 | + | |
| 5 | + | |
| 6 | + | |
| 7 | + | |
| 8 | + | |
| 9 | + | |
| 10 | + | |
| 11 | + | |
| 12 | + | |
| 13 | + | |
| 14 | + | |
| 15 | + | |
| 16 | + | |
| 17 | + | |
| 18 | + | |
| 19 | + | |
| 20 | + | |
| 21 | + | |
| 22 | + | |
| 23 | + | |
| 24 | + | |
| 25 | + | |
| 26 | + | |
| 27 | + | |
| 28 | + | |
0 commit comments