From 00bc32c920e5fb7c517a8b50692a09ac95b947ed Mon Sep 17 00:00:00 2001 From: Enes Date: Mon, 29 Jun 2026 23:08:08 +0300 Subject: [PATCH 1/2] feat(controllers): expose WalletConnect URI on public state for headless QR MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `getState()` / `subscribeState()` now include `wcUri`, `wcError`, and `wcFetchingUri` — the symmetric *read* for the existing `getWalletConnectUri()` trigger. They're mirrored one-way from `ConnectionController` onto `PublicStateController` via a single subscription, so a headless host can render a QR through the AppKit instance without the `useAppKitWallets` React hook. The connection-level `subscribeConnections` is gated behind the `multiWallet` remote feature and so can't serve the URI for a single-wallet QR; the public state is ungated. This completes the headless read surface started in #5695. Co-Authored-By: Claude Opus 4.8 --- .changeset/expose-wcuri-on-public-state.md | 32 +++++++++++++++++++ .../appkit/src/client/appkit-base-client.ts | 5 +-- .../src/controllers/ConnectionController.ts | 16 ++++++++++ .../src/controllers/PublicStateController.ts | 23 ++++++++++++- .../controllers/ConnectionController.test.ts | 20 ++++++++++++ .../controllers/PublicStateController.test.ts | 15 +++++++-- 6 files changed, 105 insertions(+), 6 deletions(-) create mode 100644 .changeset/expose-wcuri-on-public-state.md diff --git a/.changeset/expose-wcuri-on-public-state.md b/.changeset/expose-wcuri-on-public-state.md new file mode 100644 index 0000000000..739399dafc --- /dev/null +++ b/.changeset/expose-wcuri-on-public-state.md @@ -0,0 +1,32 @@ +--- +'@reown/appkit-utils': patch +'@reown/appkit': patch +'@reown/appkit-cdn': patch +'@reown/appkit-cli': patch +'@reown/appkit-codemod': patch +'@reown/appkit-common': patch +'@reown/appkit-core': patch +'@reown/appkit-experimental': patch +'@reown/appkit-pay': patch +'@reown/appkit-polyfills': patch +'@reown/appkit-scaffold-ui': patch +'@reown/appkit-siwe': patch +'@reown/appkit-siwx': patch +'@reown/appkit-testing': patch +'@reown/appkit-ui': patch +'@reown/appkit-universal-connector': patch +'@reown/appkit-wallet-button': patch +'@reown/appkit-wallet': patch +'@reown/appkit-controllers': patch +'@reown/appkit-adapter-bitcoin': patch +'@reown/appkit-adapter-ethers': patch +'@reown/appkit-adapter-ethers5': patch +'@reown/appkit-adapter-solana': patch +'@reown/appkit-adapter-ton': patch +'@reown/appkit-adapter-tron': patch +'@reown/appkit-adapter-wagmi': patch +--- + +Expose the WalletConnect URI on the public AppKit state, so a headless host can render a QR without the `useAppKitWallets` React hook. + +`getState()` / `subscribeState()` now include `wcUri`, `wcError`, and `wcFetchingUri` — the symmetric *read* for the existing `getWalletConnectUri()` trigger. These are mirrored from the connection layer onto the public state, so headless consumers read them ungated through the AppKit instance (the connection-level `subscribeConnections` is gated behind the `multiWallet` remote feature and so can't serve the URI for a single-wallet QR). diff --git a/packages/appkit/src/client/appkit-base-client.ts b/packages/appkit/src/client/appkit-base-client.ts index c79d601427..cd141a9122 100644 --- a/packages/appkit/src/client/appkit-base-client.ts +++ b/packages/appkit/src/client/appkit-base-client.ts @@ -2348,8 +2348,9 @@ export abstract class AppKitBaseClient { } /** - * Pre-fetch the WalletConnect URI (read from {@link getState} / `subscribeConnections`). - * Call when a wallet is selected so a later connect can deeplink synchronously (iOS). + * Pre-fetch the WalletConnect URI. The resulting `wcUri` (plus `wcError` / `wcFetchingUri`) is + * exposed on the public state — read it via {@link getState} or {@link subscribeState}. Call + * when a wallet is selected so a later connect can deeplink synchronously (iOS) or render a QR. */ public async getWalletConnectUri(options?: ConnectOptions) { await HeadlessWalletUtil.getWalletConnectUri(options) diff --git a/packages/controllers/src/controllers/ConnectionController.ts b/packages/controllers/src/controllers/ConnectionController.ts index fe36aba733..57bd0fcecd 100644 --- a/packages/controllers/src/controllers/ConnectionController.ts +++ b/packages/controllers/src/controllers/ConnectionController.ts @@ -700,3 +700,19 @@ const controller = { // Export the controller wrapped with our error boundary export const ConnectionController = withErrorBoundary(controller) + +/* + * Mirror the WalletConnect URI / connection signals onto the public state so headless hosts can + * read them ungated via `appKit.getState()` / `appKit.subscribeState()` — the symmetric read for + * `getWalletConnectUri()`. (`subscribeConnections` is gated behind the `multiWallet` remote + * feature, so it can't serve the URI for a single-wallet QR.) Driving every wcUri/wcError/ + * wcFetchingUri mutation through one subscription keeps the mirror in sync regardless of which + * setter changed it; it's one-way (PublicState never writes back here), so there's no loop. + */ +sub(state, () => { + PublicStateController.set({ + wcUri: state.wcUri, + wcError: Boolean(state.wcError), + wcFetchingUri: state.wcFetchingUri + }) +}) diff --git a/packages/controllers/src/controllers/PublicStateController.ts b/packages/controllers/src/controllers/PublicStateController.ts index 3fb52fbbd0..1386783836 100644 --- a/packages/controllers/src/controllers/PublicStateController.ts +++ b/packages/controllers/src/controllers/PublicStateController.ts @@ -38,6 +38,24 @@ export interface PublicStateControllerState { * @type {WalletItem | undefined} */ connectingWallet: WalletItem | undefined + + /** + * @description The active WalletConnect pairing URI (for rendering a QR / deeplink), or + * `undefined` when none is active. Mirrored from the connection layer so headless hosts can + * read it ungated — the symmetric read for `getWalletConnectUri()`. + * @type {string | undefined} + */ + wcUri: string | undefined + /** + * @description Whether the last WalletConnect URI fetch / connection attempt errored. + * @type {boolean} + */ + wcError: boolean + /** + * @description Whether a WalletConnect URI is currently being fetched. + * @type {boolean} + */ + wcFetchingUri: boolean } // -- State --------------------------------------------- // @@ -47,7 +65,10 @@ const state = proxy({ selectedNetworkId: undefined, activeChain: undefined, initialized: false, - connectingWallet: undefined + connectingWallet: undefined, + wcUri: undefined, + wcError: false, + wcFetchingUri: false }) // -- Controller ---------------------------------------- // diff --git a/packages/controllers/tests/controllers/ConnectionController.test.ts b/packages/controllers/tests/controllers/ConnectionController.test.ts index 742053d8c8..9089e75307 100644 --- a/packages/controllers/tests/controllers/ConnectionController.test.ts +++ b/packages/controllers/tests/controllers/ConnectionController.test.ts @@ -283,6 +283,26 @@ describe('ConnectionController', () => { expect(ConnectionController.state.wcPairingExpiry).toEqual(ConstantsUtil.FOUR_MINUTES_MS) }) + it('mirrors wcUri / wcError / wcFetchingUri onto the public state', async () => { + // The mirror is a valtio subscription that batches notifications on a microtask, so flush + // the queue before asserting on the public state. + vi.useRealTimers() + const flush = () => new Promise(resolve => setTimeout(resolve, 0)) + + ConnectionController.setUri('wc:mirror-test') + await flush() + expect(PublicStateController.state.wcUri).toEqual('wc:mirror-test') + expect(PublicStateController.state.wcFetchingUri).toEqual(false) + + ConnectionController.setWcError(true) + await flush() + expect(PublicStateController.state.wcError).toEqual(true) + + ConnectionController.resetUri() + await flush() + expect(PublicStateController.state.wcUri).toBeUndefined() + }) + it('should disconnect correctly', async () => { const disconnectSpy = vi.spyOn(client, 'disconnect') await ConnectionController.disconnect() diff --git a/packages/controllers/tests/controllers/PublicStateController.test.ts b/packages/controllers/tests/controllers/PublicStateController.test.ts index e7862a4bd8..76029a9fb3 100644 --- a/packages/controllers/tests/controllers/PublicStateController.test.ts +++ b/packages/controllers/tests/controllers/PublicStateController.test.ts @@ -10,7 +10,10 @@ describe('PublicStateController', () => { open: false, selectedNetworkId: undefined, activeChain: undefined, - initialized: false + initialized: false, + wcUri: undefined, + wcError: false, + wcFetchingUri: false }) }) @@ -21,7 +24,10 @@ describe('PublicStateController', () => { selectedNetworkId: undefined, activeChain: undefined, initialized: false, - open: true + open: true, + wcUri: undefined, + wcError: false, + wcFetchingUri: false }) PublicStateController.set({ selectedNetworkId: 'eip155:1' }) expect(PublicStateController.state).toEqual({ @@ -29,7 +35,10 @@ describe('PublicStateController', () => { open: true, selectedNetworkId: 'eip155:1', activeChain: undefined, - initialized: false + initialized: false, + wcUri: undefined, + wcError: false, + wcFetchingUri: false }) }) }) From 633c3ab9c041c87a75e77b1ece3715923586f777 Mon Sep 17 00:00:00 2001 From: Enes Date: Thu, 2 Jul 2026 14:14:57 +0300 Subject: [PATCH 2/2] refactor(controllers): read WalletConnect URI via instance methods, not public state Replace the PublicStateController mirror with a direct read/subscribe pair on HeadlessWalletUtil, mirroring the existing getWalletList/subscribeWalletList pattern. A headless host reads the URI ungated through the AppKit instance with a single source of truth and no controllers import. - Add HeadlessWalletUtil.getWalletConnectUri() (returns { wcUri, wcError, wcFetchingUri }) and subscribeWalletConnectUri(), both reading ConnectionController directly; export WalletConnectUriSnapshot. - Rename the pre-fetch trigger getWalletConnectUri() -> prefetchWalletConnectUri() (breaking) to free the natural name for the read. - Revert the wcUri/wcError/wcFetchingUri fields on PublicStateController and the module-scope mirror subscription on ConnectionController. - Update tests + changeset accordingly. Co-Authored-By: Claude Opus 4.8 (1M context) --- .changeset/expose-wcuri-on-public-state.md | 32 ----------- .changeset/headless-read-wcuri.md | 34 +++++++++++ .../appkit/src/client/appkit-base-client.ts | 25 +++++++-- packages/controllers/exports/index.ts | 1 + .../src/controllers/ConnectionController.ts | 16 ------ .../src/controllers/PublicStateController.ts | 23 +------- .../src/utils/HeadlessWalletUtil.ts | 50 +++++++++++++++-- .../controllers/ConnectionController.test.ts | 20 ------- .../controllers/PublicStateController.test.ts | 15 +---- .../tests/utils/HeadlessWalletUtil.test.ts | 56 ++++++++++++++++++- 10 files changed, 158 insertions(+), 114 deletions(-) delete mode 100644 .changeset/expose-wcuri-on-public-state.md create mode 100644 .changeset/headless-read-wcuri.md diff --git a/.changeset/expose-wcuri-on-public-state.md b/.changeset/expose-wcuri-on-public-state.md deleted file mode 100644 index 739399dafc..0000000000 --- a/.changeset/expose-wcuri-on-public-state.md +++ /dev/null @@ -1,32 +0,0 @@ ---- -'@reown/appkit-utils': patch -'@reown/appkit': patch -'@reown/appkit-cdn': patch -'@reown/appkit-cli': patch -'@reown/appkit-codemod': patch -'@reown/appkit-common': patch -'@reown/appkit-core': patch -'@reown/appkit-experimental': patch -'@reown/appkit-pay': patch -'@reown/appkit-polyfills': patch -'@reown/appkit-scaffold-ui': patch -'@reown/appkit-siwe': patch -'@reown/appkit-siwx': patch -'@reown/appkit-testing': patch -'@reown/appkit-ui': patch -'@reown/appkit-universal-connector': patch -'@reown/appkit-wallet-button': patch -'@reown/appkit-wallet': patch -'@reown/appkit-controllers': patch -'@reown/appkit-adapter-bitcoin': patch -'@reown/appkit-adapter-ethers': patch -'@reown/appkit-adapter-ethers5': patch -'@reown/appkit-adapter-solana': patch -'@reown/appkit-adapter-ton': patch -'@reown/appkit-adapter-tron': patch -'@reown/appkit-adapter-wagmi': patch ---- - -Expose the WalletConnect URI on the public AppKit state, so a headless host can render a QR without the `useAppKitWallets` React hook. - -`getState()` / `subscribeState()` now include `wcUri`, `wcError`, and `wcFetchingUri` — the symmetric *read* for the existing `getWalletConnectUri()` trigger. These are mirrored from the connection layer onto the public state, so headless consumers read them ungated through the AppKit instance (the connection-level `subscribeConnections` is gated behind the `multiWallet` remote feature and so can't serve the URI for a single-wallet QR). diff --git a/.changeset/headless-read-wcuri.md b/.changeset/headless-read-wcuri.md new file mode 100644 index 0000000000..446f4c3c97 --- /dev/null +++ b/.changeset/headless-read-wcuri.md @@ -0,0 +1,34 @@ +--- +'@reown/appkit-utils': patch +'@reown/appkit': patch +'@reown/appkit-cdn': patch +'@reown/appkit-cli': patch +'@reown/appkit-codemod': patch +'@reown/appkit-common': patch +'@reown/appkit-core': patch +'@reown/appkit-experimental': patch +'@reown/appkit-pay': patch +'@reown/appkit-polyfills': patch +'@reown/appkit-scaffold-ui': patch +'@reown/appkit-siwe': patch +'@reown/appkit-siwx': patch +'@reown/appkit-testing': patch +'@reown/appkit-ui': patch +'@reown/appkit-universal-connector': patch +'@reown/appkit-wallet-button': patch +'@reown/appkit-wallet': patch +'@reown/appkit-controllers': patch +'@reown/appkit-adapter-bitcoin': patch +'@reown/appkit-adapter-ethers': patch +'@reown/appkit-adapter-ethers5': patch +'@reown/appkit-adapter-solana': patch +'@reown/appkit-adapter-ton': patch +'@reown/appkit-adapter-tron': patch +'@reown/appkit-adapter-wagmi': patch +--- + +Add a headless read for the WalletConnect URI, so a host can render a QR without the `useAppKitWallets` React hook. + +The AppKit instance now exposes `getWalletConnectUri()` — returning `{ wcUri, wcError, wcFetchingUri }` — and `subscribeWalletConnectUri()`. Both read the connection layer directly (mirroring the existing `getWalletList()` / `subscribeWalletList()` pair), so a headless host gets the URI ungated through the instance without importing `@reown/appkit-controllers` (which can otherwise resolve to a different valtio singleton). This replaces the connection-level `subscribeConnections`, which is gated behind the `multiWallet` remote feature and so can't serve the URI for a single-wallet QR. + +**Breaking:** the imperative pre-fetch trigger previously named `getWalletConnectUri()` is renamed to `prefetchWalletConnectUri()`, freeing `getWalletConnectUri()` for the new read. diff --git a/packages/appkit/src/client/appkit-base-client.ts b/packages/appkit/src/client/appkit-base-client.ts index cd141a9122..b5caee5e56 100644 --- a/packages/appkit/src/client/appkit-base-client.ts +++ b/packages/appkit/src/client/appkit-base-client.ts @@ -42,6 +42,7 @@ import type { UseAppKitAccountReturn, UseAppKitNetworkReturn, User, + WalletConnectUriSnapshot, WalletFeature, WalletItem, WalletListSnapshot, @@ -2348,12 +2349,26 @@ export abstract class AppKitBaseClient { } /** - * Pre-fetch the WalletConnect URI. The resulting `wcUri` (plus `wcError` / `wcFetchingUri`) is - * exposed on the public state — read it via {@link getState} or {@link subscribeState}. Call - * when a wallet is selected so a later connect can deeplink synchronously (iOS) or render a QR. + * Pre-fetch the WalletConnect URI. Read the result with {@link getWalletConnectUri}; subscribe + * with {@link subscribeWalletConnectUri}. Call when a wallet is selected so a later connect can + * deeplink synchronously (iOS) or render a QR. */ - public async getWalletConnectUri(options?: ConnectOptions) { - await HeadlessWalletUtil.getWalletConnectUri(options) + public async prefetchWalletConnectUri(options?: ConnectOptions) { + await HeadlessWalletUtil.prefetchWalletConnectUri(options) + } + + /** + * The current WalletConnect URI state (QR / deeplink URI + fetch/error signals) — the + * symmetric read for {@link prefetchWalletConnectUri}. Reads the connection layer directly, so + * a headless host gets it ungated through the AppKit instance. + */ + public getWalletConnectUri(): WalletConnectUriSnapshot { + return HeadlessWalletUtil.getWalletConnectUri() + } + + /** Subscribe to WalletConnect URI state changes. Returns an unsubscribe. */ + public subscribeWalletConnectUri(callback: () => void) { + return HeadlessWalletUtil.subscribeWalletConnectUri(callback) } /** diff --git a/packages/controllers/exports/index.ts b/packages/controllers/exports/index.ts index ecc8984c43..3a785dab5a 100644 --- a/packages/controllers/exports/index.ts +++ b/packages/controllers/exports/index.ts @@ -134,6 +134,7 @@ export { HeadlessWalletUtil } from '../src/utils/HeadlessWalletUtil.js' export type { ConnectOptions, FetchWalletsOptions, + WalletConnectUriSnapshot, WalletListSnapshot } from '../src/utils/HeadlessWalletUtil.js' export * from '../src/utils/ChainControllerUtil.js' diff --git a/packages/controllers/src/controllers/ConnectionController.ts b/packages/controllers/src/controllers/ConnectionController.ts index 57bd0fcecd..fe36aba733 100644 --- a/packages/controllers/src/controllers/ConnectionController.ts +++ b/packages/controllers/src/controllers/ConnectionController.ts @@ -700,19 +700,3 @@ const controller = { // Export the controller wrapped with our error boundary export const ConnectionController = withErrorBoundary(controller) - -/* - * Mirror the WalletConnect URI / connection signals onto the public state so headless hosts can - * read them ungated via `appKit.getState()` / `appKit.subscribeState()` — the symmetric read for - * `getWalletConnectUri()`. (`subscribeConnections` is gated behind the `multiWallet` remote - * feature, so it can't serve the URI for a single-wallet QR.) Driving every wcUri/wcError/ - * wcFetchingUri mutation through one subscription keeps the mirror in sync regardless of which - * setter changed it; it's one-way (PublicState never writes back here), so there's no loop. - */ -sub(state, () => { - PublicStateController.set({ - wcUri: state.wcUri, - wcError: Boolean(state.wcError), - wcFetchingUri: state.wcFetchingUri - }) -}) diff --git a/packages/controllers/src/controllers/PublicStateController.ts b/packages/controllers/src/controllers/PublicStateController.ts index 1386783836..3fb52fbbd0 100644 --- a/packages/controllers/src/controllers/PublicStateController.ts +++ b/packages/controllers/src/controllers/PublicStateController.ts @@ -38,24 +38,6 @@ export interface PublicStateControllerState { * @type {WalletItem | undefined} */ connectingWallet: WalletItem | undefined - - /** - * @description The active WalletConnect pairing URI (for rendering a QR / deeplink), or - * `undefined` when none is active. Mirrored from the connection layer so headless hosts can - * read it ungated — the symmetric read for `getWalletConnectUri()`. - * @type {string | undefined} - */ - wcUri: string | undefined - /** - * @description Whether the last WalletConnect URI fetch / connection attempt errored. - * @type {boolean} - */ - wcError: boolean - /** - * @description Whether a WalletConnect URI is currently being fetched. - * @type {boolean} - */ - wcFetchingUri: boolean } // -- State --------------------------------------------- // @@ -65,10 +47,7 @@ const state = proxy({ selectedNetworkId: undefined, activeChain: undefined, initialized: false, - connectingWallet: undefined, - wcUri: undefined, - wcError: false, - wcFetchingUri: false + connectingWallet: undefined }) // -- Controller ---------------------------------------- // diff --git a/packages/controllers/src/utils/HeadlessWalletUtil.ts b/packages/controllers/src/utils/HeadlessWalletUtil.ts index 1ecf1c3dd7..96f3b41cf7 100644 --- a/packages/controllers/src/utils/HeadlessWalletUtil.ts +++ b/packages/controllers/src/utils/HeadlessWalletUtil.ts @@ -41,7 +41,7 @@ export interface FetchWalletsOptions { sort?: 'default' | 'wcpay' } -/** Options for {@link HeadlessWalletUtil.connect} / {@link HeadlessWalletUtil.getWalletConnectUri}. */ +/** Options for {@link HeadlessWalletUtil.connect} / {@link HeadlessWalletUtil.prefetchWalletConnectUri}. */ export interface ConnectOptions { /** * A WalletConnect Pay deeplink appended to the WC URI, so a WCPay-capable wallet @@ -62,6 +62,16 @@ export interface WalletListSnapshot { count: number } +/** The WalletConnect URI + connection-attempt signals, read imperatively. */ +export interface WalletConnectUriSnapshot { + /** Active WalletConnect pairing URI (for rendering a QR / deeplink), or undefined when none. */ + wcUri: string | undefined + /** Whether the last WalletConnect URI fetch / connection attempt errored. */ + wcError: boolean + /** Whether a WalletConnect URI is currently being fetched. */ + wcFetchingUri: boolean +} + /** * Framework-agnostic headless wallet-list logic — the imperative core behind the * `useAppKitWallets` React hook and the `AppKit` client's wallet methods. @@ -120,15 +130,45 @@ export const HeadlessWalletUtil = { }, /** - * Pre-fetch the WalletConnect URI (read from `ConnectionController.state.wcUri`). - * Call when a wallet is selected so a later connect can deeplink synchronously - * (required for iOS Safari). Uses 'auto' cache to reuse a valid URI or fetch a new one. + * Pre-fetch the WalletConnect URI. Read the result with {@link getWalletConnectUri}; + * subscribe with {@link subscribeWalletConnectUri}. Call when a wallet is selected so a + * later connect can deeplink synchronously (required for iOS Safari) or render a QR. Uses + * 'auto' cache to reuse a valid URI or fetch a new one. */ - async getWalletConnectUri(_options?: ConnectOptions): Promise { + async prefetchWalletConnectUri(_options?: ConnectOptions): Promise { this.resetWcUri() await ConnectionController.connectWalletConnect({ cache: 'auto' }) }, + /** + * Read the current WalletConnect URI state (the QR / deeplink URI plus the fetch/error + * signals) — the symmetric read for {@link prefetchWalletConnectUri}. Reads + * `ConnectionController` directly, so a headless host gets it ungated through the AppKit + * instance without importing the controllers package. + */ + getWalletConnectUri(): WalletConnectUriSnapshot { + return { + wcUri: ConnectionController.state.wcUri, + wcError: Boolean(ConnectionController.state.wcError), + wcFetchingUri: ConnectionController.state.wcFetchingUri + } + }, + + /** + * Subscribe to WalletConnect URI state changes (wcUri / wcError / wcFetchingUri). The + * callback receives no arguments — read the current values with {@link getWalletConnectUri}. + * Returns an unsubscribe. + */ + subscribeWalletConnectUri(callback: () => void): () => void { + const unsubscribers = [ + subKey(ConnectionController.state, 'wcUri', callback), + subKey(ConnectionController.state, 'wcError', callback), + subKey(ConnectionController.state, 'wcFetchingUri', callback) + ] + + return () => unsubscribers.forEach(unsubscribe => unsubscribe()) + }, + /** * Connect a chosen wallet programmatically (headless — no modal). * diff --git a/packages/controllers/tests/controllers/ConnectionController.test.ts b/packages/controllers/tests/controllers/ConnectionController.test.ts index 9089e75307..742053d8c8 100644 --- a/packages/controllers/tests/controllers/ConnectionController.test.ts +++ b/packages/controllers/tests/controllers/ConnectionController.test.ts @@ -283,26 +283,6 @@ describe('ConnectionController', () => { expect(ConnectionController.state.wcPairingExpiry).toEqual(ConstantsUtil.FOUR_MINUTES_MS) }) - it('mirrors wcUri / wcError / wcFetchingUri onto the public state', async () => { - // The mirror is a valtio subscription that batches notifications on a microtask, so flush - // the queue before asserting on the public state. - vi.useRealTimers() - const flush = () => new Promise(resolve => setTimeout(resolve, 0)) - - ConnectionController.setUri('wc:mirror-test') - await flush() - expect(PublicStateController.state.wcUri).toEqual('wc:mirror-test') - expect(PublicStateController.state.wcFetchingUri).toEqual(false) - - ConnectionController.setWcError(true) - await flush() - expect(PublicStateController.state.wcError).toEqual(true) - - ConnectionController.resetUri() - await flush() - expect(PublicStateController.state.wcUri).toBeUndefined() - }) - it('should disconnect correctly', async () => { const disconnectSpy = vi.spyOn(client, 'disconnect') await ConnectionController.disconnect() diff --git a/packages/controllers/tests/controllers/PublicStateController.test.ts b/packages/controllers/tests/controllers/PublicStateController.test.ts index 76029a9fb3..e7862a4bd8 100644 --- a/packages/controllers/tests/controllers/PublicStateController.test.ts +++ b/packages/controllers/tests/controllers/PublicStateController.test.ts @@ -10,10 +10,7 @@ describe('PublicStateController', () => { open: false, selectedNetworkId: undefined, activeChain: undefined, - initialized: false, - wcUri: undefined, - wcError: false, - wcFetchingUri: false + initialized: false }) }) @@ -24,10 +21,7 @@ describe('PublicStateController', () => { selectedNetworkId: undefined, activeChain: undefined, initialized: false, - open: true, - wcUri: undefined, - wcError: false, - wcFetchingUri: false + open: true }) PublicStateController.set({ selectedNetworkId: 'eip155:1' }) expect(PublicStateController.state).toEqual({ @@ -35,10 +29,7 @@ describe('PublicStateController', () => { open: true, selectedNetworkId: 'eip155:1', activeChain: undefined, - initialized: false, - wcUri: undefined, - wcError: false, - wcFetchingUri: false + initialized: false }) }) }) diff --git a/packages/controllers/tests/utils/HeadlessWalletUtil.test.ts b/packages/controllers/tests/utils/HeadlessWalletUtil.test.ts index ccb4bab764..b2c5d00672 100644 --- a/packages/controllers/tests/utils/HeadlessWalletUtil.test.ts +++ b/packages/controllers/tests/utils/HeadlessWalletUtil.test.ts @@ -198,7 +198,7 @@ describe('HeadlessWalletUtil.connect', () => { }) }) -describe('HeadlessWalletUtil.getWalletConnectUri', () => { +describe('HeadlessWalletUtil.prefetchWalletConnectUri', () => { it('resets then fetches the URI with auto cache', async () => { const resetUri = vi.spyOn(ConnectionController, 'resetUri').mockImplementation(vi.fn()) const setWcLinking = vi.spyOn(ConnectionController, 'setWcLinking').mockImplementation(vi.fn()) @@ -206,7 +206,7 @@ describe('HeadlessWalletUtil.getWalletConnectUri', () => { .spyOn(ConnectionController, 'connectWalletConnect') .mockResolvedValue(undefined as never) - await HeadlessWalletUtil.getWalletConnectUri() + await HeadlessWalletUtil.prefetchWalletConnectUri() expect(resetUri).toHaveBeenCalled() expect(setWcLinking).toHaveBeenCalledWith(undefined) @@ -214,6 +214,58 @@ describe('HeadlessWalletUtil.getWalletConnectUri', () => { }) }) +describe('HeadlessWalletUtil.getWalletConnectUri', () => { + it('reads wcUri / wcError / wcFetchingUri from the connection layer', () => { + ConnectionController.state.wcUri = 'wc:read-test' + ConnectionController.state.wcError = true + ConnectionController.state.wcFetchingUri = false + + expect(HeadlessWalletUtil.getWalletConnectUri()).toEqual({ + wcUri: 'wc:read-test', + wcError: true, + wcFetchingUri: false + }) + }) + + it('coerces a missing wcError to a boolean', () => { + ConnectionController.state.wcUri = undefined + ConnectionController.state.wcError = undefined + ConnectionController.state.wcFetchingUri = true + + expect(HeadlessWalletUtil.getWalletConnectUri()).toEqual({ + wcUri: undefined, + wcError: false, + wcFetchingUri: true + }) + }) +}) + +describe('HeadlessWalletUtil.subscribeWalletConnectUri', () => { + it('fires on wcUri / wcError / wcFetchingUri changes and unsubscribes cleanly', async () => { + const flush = () => new Promise(resolve => setTimeout(resolve, 0)) + const callback = vi.fn() + + // Establish a known baseline before subscribing so each mutation below is a real change. + ConnectionController.state.wcUri = undefined + ConnectionController.state.wcFetchingUri = false + + const unsubscribe = HeadlessWalletUtil.subscribeWalletConnectUri(callback) + + ConnectionController.state.wcUri = 'wc:sub-test' + await flush() + expect(callback).toHaveBeenCalledTimes(1) + + ConnectionController.state.wcFetchingUri = true + await flush() + expect(callback).toHaveBeenCalledTimes(2) + + unsubscribe() + ConnectionController.state.wcUri = 'wc:after-unsub' + await flush() + expect(callback).toHaveBeenCalledTimes(2) + }) +}) + describe('HeadlessWalletUtil resets', () => { it('resetWcUri resets the URI + linking', () => { const resetUri = vi.spyOn(ConnectionController, 'resetUri').mockImplementation(vi.fn())