fix(wallet): stop swallowing our own app-switch, and stop resending the link the wallet wedged on - #33
Merged
Conversation
…he link the wallet wedged on Two defects behind one field report: on "Set Identity", the tap that signs opens MetaMask and MetaMask sits on its splash screen, every time. Killing it from recents and pressing "Open wallet to approve" then works. 1. Our own hop went through our own interceptor. `captureAutoRedirect()` patches `window.open` and swallows any URL carrying both `requestId=` and `sessionTopic=`, so WalletConnect's redirect cannot cut off the publish it is meant to accompany. But `hopToWallet` routed an https universal link back through `window.open` -- and that predicate matches the request link by construction, because the request link is exactly what it is built to swallow. So the hop was eaten by our own trap: `window.open` returned null, `openUrl`'s `false` was read by nobody, and no wallet came forward. Not a narrow window, either. The capture is released only after the request settles, so for the whole life of the signature request BOTH the automatic hop and the "Open wallet to approve" button did nothing at all for every wallet whose deep link is a universal link rather than a custom scheme. `hopToWallet` moves next to the interceptor it has to step over, and calls the saved browser `window.open` directly. 2. The retry repeated the link that had just failed. Asked to reproduce the hang and then retry WITHOUT force-quitting -- same URL, from a real tap, so user activation cannot be the difference -- the reporter says it hangs just the same. A cold wallet works; a warm one does not, whatever this page does. The fault is inside the wallet. The likeliest reading is that `.../wc?requestId=` puts MetaMask into the route that WAITS for that request to arrive over its OWN relay socket -- the socket Android suspended while it sat in the background -- and that wait is the full-screen "splash" the user is stuck on. A web page cannot reach into another app and reconnect its socket. What it can do is stop asking for the part that hangs: once the request-scoped link has been tried and produced nothing, the next tap sends the bare scheme, which asks for nothing but the app. A wallet that did receive the request shows it; one that did not at least comes up usable instead of wedged. Returning to this page with the request still unanswered is what marks that, and it is the strongest evidence available that the wallet showed nothing -- the user looked at it and came back. It also brings the recovery hint forward: the 12s timer that used to be the only trigger runs while the user is inside the wallet, so on its own it only ever tells them something they already found out for themselves. The request is never abandoned in any of this: a signature approved minutes later still lands. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_013BpqXrkEPA9odTyRdK5Mnx
…ed a force-quit The hint said "close it completely from your recent apps, then tap Open wallet to approve again". Anyone who follows that lands on a COLD wallet -- which handles `.../wc?requestId=` perfectly well -- so the bare-scheme retry added alongside it never runs. The cheaper fix was written and then routed around by the very text that is supposed to lead people to it. So: tap again first, force-quit only if that does not do it. Both locales. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_013BpqXrkEPA9odTyRdK5Mnx
This was referenced Sep 4, 2026
ehsan6sha
added a commit
that referenced
this pull request
Sep 4, 2026
…er -- say so (#35) #33 shipped a guess. When the request-scoped link had already been tried and produced nothing, the next tap sent a bare `metamask://` instead, on the theory that `.../wc?requestId=` was the part that wedged MetaMask -- that it put the wallet into a route waiting for a request to arrive over its own Android-suspended socket, and that asking only for the app would sidestep it. Tested on the reporter's phone: wrong. A warm MetaMask sits on its splash screen for the bare scheme exactly as it does for the request link. Both retries were made without force-quitting, and both hung; killing MetaMask and tapping again went through, as before. The wallet deadlocks on being resumed by a deep link at all, and there is no URL a web page can produce that avoids it. So the link stops changing. Always the request-scoped one, which is the right link for the case that does work -- a cold wallet, where it surfaces THIS prompt rather than the home screen -- and one less moving part on the path that is the user's only way through. The hint goes back to leading with the force-quit, and now says outright that tapping again without closing the wallet first will not help. #33 reordered it on the assumption that the cheap step worked; it does not, and a hint that offers a step that fails first is worse than no hint. Kept from #33, both still correct: the interceptor no longer swallows our own app-switch, and coming back to the page with the request unanswered shows the recovery hint at once instead of waiting out a 12s timer that ran while the user was inside the wallet. Claude-Session: https://claude.ai/code/session_013BpqXrkEPA9odTyRdK5Mnx Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
ehsan6sha
added a commit
to functionland/docs
that referenced
this pull request
Sep 4, 2026
…178) #177 reordered the stuck-wallet advice to lead with "tap Open wallet to approve again", on the theory -- shipped alongside it in functionland/fxblox-web#33 -- that a second tap opened the wallet a different way and would usually get past the splash screen. Tested on a phone since: it does not. A warm MetaMask hangs on the retry exactly as it hangs on the first attempt. Only closing MetaMask from recents and then tapping clears it, which is what these pages said before #177 and what functionland/fxblox-web#35 puts back in the app's own hint. So the force-quit leads again, and each place now says plainly that tapping without closing the wallet first returns you to the same stuck screen -- the thing a user will otherwise try twice before giving up. Also says where the fault is: inside the wallet app, where a web page cannot reach. The "give the page a few seconds when you come back" guidance from #177 is untouched -- that part was about our own reconnect and is still right. Claude-Session: https://claude.ai/code/session_013BpqXrkEPA9odTyRdK5Mnx Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Field report: on Setup → Set Identity, after connecting the wallet, the tap that signs opens MetaMask and MetaMask sits on its splash screen — every time. Force-quitting MetaMask from recents and then pressing Open wallet to approve works.
Two separate defects: one is ours and certain, one is the wallet's and this only mitigates it.
1. Our own app-switch went through our own interceptor — certain, fixed
captureAutoRedirect()patcheswindow.openand swallows any URL carrying bothrequestId=andsessionTopic=, so WalletConnect's redirect cannot cut off the publish it is meant to accompany.But
hopToWalletrouted an https universal link back throughwindow.open— and that predicate matches the request link by construction, because the request link is exactly what it is built to swallow. The hop was eaten by our own trap:window.openreturnednull,openUrl'sfalsereturn was read by nobody, and no wallet came forward.Not a narrow window either. The capture is released only after the request settles, so for the whole life of the signature request both the automatic hop and the "Open wallet to approve" button did nothing at all — for every wallet whose deep link is a universal link (
https://…app.link/wc?…) rather than a custom scheme.hopToWalletmoves next to the interceptor it has to step over, and calls the saved browserwindow.opendirectly. Covered by a regression test.2. The retry repeated the link that had just failed — mitigation, not a proven fix
The reporter says that retrying without force-quitting — same URL, from a real tap, so user activation is not the difference — hangs just the same. That is their account of the behaviour, not an instrumented run; nobody has attached a debugger to the phone yet. Taken at face value: a cold wallet works, a warm one does not, whatever this page does, so the hang itself is inside the wallet.
The likeliest reading is that
…/wc?requestId=puts MetaMask into the route that waits for that request to arrive over its own relay socket — the socket Android suspended while it sat in the background — and that wait is the full-screen "splash" the user is stuck on. A web page cannot reach into another app and reconnect its socket.What it can do is stop asking for the part that hangs. Once the request-scoped link has been tried and produced nothing, the next tap sends the bare scheme, which asks for nothing but the app: a wallet that did receive the request shows it, and one that did not at least comes up usable instead of wedged.
This is contingent on that reading. If the wallet instead deadlocks on any warm resume, the bare scheme lands on the same splash and the user still force-quits — no worse than today, but no better.
Returning to this page with the request still unanswered is what marks the first attempt as failed — the strongest evidence available that the wallet showed nothing, since the user looked at it and came back. It also brings the recovery hint forward: the 12 s timer that used to be its only trigger runs while the user is inside the wallet, so on its own it only ever told them something they had already worked out.
The second commit reorders that hint. It used to say "close it completely from your recent apps, then tap Open wallet to approve again" — and anyone following that reaches a cold wallet, which handles
…/wc?requestId=fine, so the bare-scheme retry would never have run. Tap again first; force-quit only if that does not do it.The request is never abandoned in any of this: a signature approved minutes later still lands.
Testing
npm test --workspaces— 943 passing, including 3 new cases: the self-swallow regression (hopToWalletunder a live capture), return-while-pending showing the hint immediately and dropping the request id from the retry, and an unrelated visibility change not being read as a wedged wallet.npm run typecheck --workspaces,npm run lint,npm run buildall clean.chrome://inspectattached, reproduce, and report whether[sign] opening the wallet by hand: metamask://(norequestId) lands on the prompt or on the splash.🤖 Generated with Claude Code
https://claude.ai/code/session_013BpqXrkEPA9odTyRdK5Mnx