Summary
XamanAdapter.openSignWindow() opens a popup window for the Xaman sign page but the library never closes it after the WebSocket signature event arrives. After the user signs, the popup stays open showing the Xaman sign page.
On mobile this is more than cosmetic. window.open(url, 'Xaman Sign', ...) opens a new tab. After signing in the Xaman app, the deeplink return lands on that orphan tab (still rendering the Xaman sign page) instead of the original tab where the WebSocket has already resolved the sign promise. The user gets stuck on a stale Xaman screen with no way back.
The connect path doesn't have this bug because Xaman's OAuth callback page calls window.close() itself (standard PKCE flow). Only the sign popup is affected.
Reproduction
- Use any app that calls
walletManager.sign() / walletManager.signAndSubmit() with the Xaman adapter on mobile (e.g. examples/vanilla-js after connect).
- Tap into Xaman via the deeplink, sign on phone.
- Browser returns to the popup tab — it stays on the Xaman sign confirmation. The original tab quietly completes the sign in the background.
The vitepress docs site (docs/components/TryItOut.vue) does not exhibit this because it only exercises connect, not sign.
Code path
In packages/adapters/xaman/src/xaman-adapter.ts:
openSignWindow (line ~361) does window.open(...) and discards the returned Window handle.
createAndWaitForPayload (line ~215) calls openSignWindow(payload.created.next.always), awaits waitForSignature(...), returns — popup is never tracked or closed.
signMessage (line ~309) follows the same pattern.
In packages/ui/src/services/WalletService.ts:14-132, connectWallet only wires onQRCode for walletconnect. For xaman the default UI falls back to the orphan-popup path.
Proposed fix
Capture the popup window in openSignWindow and close it once createAndWaitForPayload / signMessage settle:
private signPopup: Window | null = null;
private openSignWindow(url: string): void {
if (this.options.onQRCode) {
this.options.onQRCode(url);
return;
}
// existing window.open(...) — but capture the ref
this.signPopup = window.open(url, 'Xaman Sign', `...`);
}
private closeSignPopup(): void {
if (this.signPopup && !this.signPopup.closed) {
try { this.signPopup.close(); } catch { /* cross-origin navigation */ }
}
this.signPopup = null;
}
Then wrap the awaits in createAndWaitForPayload and signMessage with try { ... } finally { this.closeSignPopup(); }.
Alternatively (or additionally), have WalletService.connectWallet wire an onQRCode callback for xaman so the default UI bypasses the popup altogether and renders the QR in-component, the way it already does for WalletConnect.
Versions
Verified against bundled 0.5.1 and 0.6.0 source — same behavior in both.
Summary
XamanAdapter.openSignWindow()opens a popup window for the Xaman sign page but the library never closes it after the WebSocket signature event arrives. After the user signs, the popup stays open showing the Xaman sign page.On mobile this is more than cosmetic.
window.open(url, 'Xaman Sign', ...)opens a new tab. After signing in the Xaman app, the deeplink return lands on that orphan tab (still rendering the Xaman sign page) instead of the original tab where the WebSocket has already resolved the sign promise. The user gets stuck on a stale Xaman screen with no way back.The connect path doesn't have this bug because Xaman's OAuth callback page calls
window.close()itself (standard PKCE flow). Only the sign popup is affected.Reproduction
walletManager.sign()/walletManager.signAndSubmit()with the Xaman adapter on mobile (e.g.examples/vanilla-jsafter connect).The vitepress docs site (
docs/components/TryItOut.vue) does not exhibit this because it only exercises connect, not sign.Code path
In
packages/adapters/xaman/src/xaman-adapter.ts:openSignWindow(line ~361) doeswindow.open(...)and discards the returnedWindowhandle.createAndWaitForPayload(line ~215) callsopenSignWindow(payload.created.next.always), awaitswaitForSignature(...), returns — popup is never tracked or closed.signMessage(line ~309) follows the same pattern.In
packages/ui/src/services/WalletService.ts:14-132,connectWalletonly wiresonQRCodeforwalletconnect. Forxamanthe default UI falls back to the orphan-popup path.Proposed fix
Capture the popup window in
openSignWindowand close it oncecreateAndWaitForPayload/signMessagesettle:Then wrap the awaits in
createAndWaitForPayloadandsignMessagewithtry { ... } finally { this.closeSignPopup(); }.Alternatively (or additionally), have
WalletService.connectWalletwire anonQRCodecallback forxamanso the default UI bypasses the popup altogether and renders the QR in-component, the way it already does for WalletConnect.Versions
Verified against bundled
0.5.1and0.6.0source — same behavior in both.