Skip to content

XamanAdapter: sign popup is never closed after signature resolves (orphan tab on mobile) #49

@LeJamon

Description

@LeJamon

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

  1. Use any app that calls walletManager.sign() / walletManager.signAndSubmit() with the Xaman adapter on mobile (e.g. examples/vanilla-js after connect).
  2. Tap into Xaman via the deeplink, sign on phone.
  3. 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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions