Skip to content

fix(wallet): stop swallowing our own app-switch, and stop resending the link the wallet wedged on - #33

Merged
ehsan6sha merged 2 commits into
mainfrom
fix/wallet-splash-hang-and-self-swallowed-hop
Sep 4, 2026
Merged

fix(wallet): stop swallowing our own app-switch, and stop resending the link the wallet wedged on#33
ehsan6sha merged 2 commits into
mainfrom
fix/wallet-splash-hang-and-self-swallowed-hop

Conversation

@ehsan6sha

@ehsan6sha ehsan6sha commented Sep 3, 2026

Copy link
Copy Markdown
Member

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() 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. The hop was eaten by our own trap: window.open returned null, openUrl's false return 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.

hopToWallet moves next to the interceptor it has to step over, and calls the saved browser window.open directly. 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 (hopToWallet under 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 build all clean.
  • Not reproduced on a device. Part 2 wants one observation to confirm or kill it: with chrome://inspect attached, reproduce, and report whether [sign] opening the wallet by hand: metamask:// (no requestId) lands on the prompt or on the splash.

🤖 Generated with Claude Code

https://claude.ai/code/session_013BpqXrkEPA9odTyRdK5Mnx

ehsan6sha and others added 2 commits September 3, 2026 19:08
…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
@ehsan6sha
ehsan6sha merged commit 8e4234e into main Sep 4, 2026
2 checks passed
@ehsan6sha
ehsan6sha deleted the fix/wallet-splash-hang-and-self-swallowed-hop branch September 4, 2026 13:51
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>
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