Skip to content

fix: reject promise when WalletConnect modal is dismissed without pairing#497

Merged
jannik-stacks merged 1 commit intostx-labs:mainfrom
jfstn:fix/walletconnect-modal-dismiss-hang
Mar 2, 2026
Merged

fix: reject promise when WalletConnect modal is dismissed without pairing#497
jannik-stacks merged 1 commit intostx-labs:mainfrom
jfstn:fix/walletconnect-modal-dismiss-hang

Conversation

@jfstn
Copy link
Copy Markdown
Contributor

@jfstn jfstn commented Feb 25, 2026

Description

When using WalletConnect via @reown/appkit-universal-connector, closing the AppKit pairing dialog (QR code modal) causes the request() promise to hang indefinitely because connector.connect() never settles after the dialog is dismissed.

This PR fixes that by detecting when the AppKit dialog closes without establishing a session and rejecting the promise with an error, so consuming apps can handle it in a catch block.

Motivation

The connector.connect() from @reown/appkit-universal-connector internally calls provider.connect() on the WalletConnect relay, which waits indefinitely for a pairing that will never arrive once the dialog is closed. The dialog closes visually, but nothing tells the underlying promise to stop waiting — leaving any consuming app stuck in a pending state.

What was changed

In WalletConnectProvider.connect() (packages/connect/src/walletconnect/index.ts):

  • Subscribe to the AppKit modal state via appKit.subscribeState()
  • When the dialog closes (open: false) and no session was established on the provider → reject with an error
  • When the dialog closes but a session exists → successful connection closing its own modal, no interference
  • Subscription is always cleaned up in both success and error paths

How this impacts application developers

// Before: promise hangs forever, app is stuck
try {
  const result = await request(
    { walletConnect: { projectId: '...' } },
    'getAddresses',
    {}
  );
} catch (error) {
  // Never reached if user dismissed the dialog
}

// After: promise rejects, app can handle it
try {
  const result = await request(
    { walletConnect: { projectId: '...' } },
    'getAddresses',
    {}
  );
} catch (error) {
  // error.message === 'User closed the WalletConnect modal'
}

Prior art

Same class of bug as wagmi + MetaMask SDK (wevm/wagmi#4504). Their position was that the wallet SDK should handle rejection properly. In our case, @reown/appkit-universal-connector doesn't propagate the dialog dismissal as a rejection, so we work around it at the WalletConnectProvider layer.

Type of Change

  • New feature
  • Bug fix
  • API reference/documentation update
  • Other

Does this introduce a breaking change?

No. The only behavioral change is that a previously-hanging promise now rejects with an error. Any app that was "handling" this by never getting a response will now get a catchable error instead.

Are documentation updates required?

  • Link to documentation updates:

No documentation updates required.

Testing information

Tested manually with an example app consuming @stacks/connect with a WalletConnect config.

  1. Reproduction steps:

    • Call request({ walletConnect: { projectId: '...' } }, 'getAddresses', {})
    • AppKit pairing dialog opens showing QR code
    • Close the dialog without scanning/pairing
    • Before: Promise hangs forever
    • After: Promise rejects with Error: User closed the WalletConnect modal
  2. Happy path (verified still works):

    • Call request() with WalletConnect
    • Complete the pairing via QR code
    • Promise resolves with addresses as before
  3. Affected code paths: WalletConnectProvider.connect()getAddresses()request()

Checklist

  • Code is commented where needed
  • Unit test coverage for new or modified code paths
  • Changelog is updated
  • Tag @jannik-stacks for review

…ring

When calling request() with a WalletConnect provider, closing the
AppKit modal without completing pairing left the returned promise
hanging indefinitely. This happened because connector.connect()
internally awaits provider.connect() on the WalletConnect relay,
which never resolves when the modal is dismissed.

Fix: subscribe to the AppKit modal state via appKit.subscribeState().
When the modal closes and no session was established, the promise
is rejected with an error so consuming apps can handle it in a
catch block.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@jannik-stacks jannik-stacks self-requested a review March 2, 2026 09:56
@jannik-stacks jannik-stacks merged commit 0ac25a8 into stx-labs:main Mar 2, 2026
3 checks passed
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.

2 participants