From f3ab5c86c66f6480d3bf8f12b1211b8e8730db79 Mon Sep 17 00:00:00 2001 From: ehsan shariati Date: Sat, 5 Sep 2026 12:30:06 -0400 Subject: [PATCH] wallet: in debug mode, do not tell the wallet where to send the user back to The third diagnostic log from the reporter's phone, run with #40's "do not open the wallet" debug mode: 16:21:54 [tab] visible -> connected=true (+609ms) MetaMask approved the pairing, sent the user back 16:21:56 [sign] request on the relay ... NOT opening the wallet 16:22:06 [tab] hidden user switched to MetaMask by hand -- no deep link 16:22:14 [tab] visible -- "it was hung on splash screen" MetaMask was already wedged BEFORE any deep link was sent to it. Twelve seconds after it approved the pairing and returned the user to the browser, it sat on its splash screen for a plain resume from recent apps. So the deep link was never the trigger, and every theory built on it -- URL shape, gesture, warm resume by intent -- is closed. Whatever puts the wallet in that state happens on the way it returns the user; the one thing here that makes it return the user at all is `providerMetadata.redirect` (chains.ts, added in #8 -- before that, MetaMask left the user in the wallet). So with debug mode on, `initAppKit` omits the redirect from the metadata it hands to the wallet. The wallet leaves the user where they are after approving, they switch back to the browser by hand, and when they switch to the wallet again for the signature the log says whether it is still healthy. Decided inside `initAppKit` because there are two call sites (LinkPassword's loader and WalletGate) and the first to run wins; AppKit reads the metadata once, so a change of debug mode needs a page reload. Default behaviour is unchanged. Co-Authored-By: Claude Fable 5.1 Claude-Session: https://claude.ai/code/session_013BpqXrkEPA9odTyRdK5Mnx --- .../src/wallet/__tests__/appkit.test.ts | 65 +++++++++++++++++++ apps/fxblox-web/src/wallet/appkit.ts | 27 +++++++- 2 files changed, 91 insertions(+), 1 deletion(-) create mode 100644 apps/fxblox-web/src/wallet/__tests__/appkit.test.ts diff --git a/apps/fxblox-web/src/wallet/__tests__/appkit.test.ts b/apps/fxblox-web/src/wallet/__tests__/appkit.test.ts new file mode 100644 index 0000000..ac311d0 --- /dev/null +++ b/apps/fxblox-web/src/wallet/__tests__/appkit.test.ts @@ -0,0 +1,65 @@ +import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; + +const createAppKitMock = vi.hoisted(() => + vi.fn((_options: unknown) => ({ setThemeMode: vi.fn(), disconnect: vi.fn() })), +); +vi.mock('@reown/appkit/react', () => ({ createAppKit: createAppKitMock })); +vi.mock('@reown/appkit-adapter-ethers5', () => ({ Ethers5Adapter: class {} })); + +import { useSettingsStore } from '@/stores/useSettingsStore'; + +/** `initAppKit` is a module-level singleton; each test wants a fresh one. */ +async function freshInitAppKit() { + vi.resetModules(); + const mod = await import('../appkit'); + return mod.initAppKit; +} + +function metadataPassed(): Record { + const call = createAppKitMock.mock.calls.at(-1)?.[0] as { metadata: Record } | undefined; + if (!call) throw new Error('createAppKit was not called'); + return call.metadata; +} + +describe('initAppKit metadata', () => { + beforeEach(() => { + createAppKitMock.mockClear(); + useSettingsStore.setState({ debugMode: undefined }); + }); + afterEach(() => { + useSettingsStore.setState({ debugMode: undefined }); + }); + + it('tells the wallet where to send the user back to, by default', async () => { + const initAppKit = await freshInitAppKit(); + initAppKit({ themeMode: 'dark' }); + const metadata = metadataPassed(); + expect(metadata.redirect).toEqual(expect.objectContaining({ universal: expect.any(String) })); + expect(metadata.name).toBe('FxBlox'); + }); + + it('in debug mode, omits the return redirect so the wallet leaves the user where they are', async () => { + // A diagnostic: MetaMask was found already wedged on its splash screen after it sent the user back from + // a pairing approval, before any deep link reached it. Without a redirect it does not send them back, + // and the next log says whether it is still healthy when they switch to it by hand. + useSettingsStore.setState({ + debugMode: { uniqueId: 'dbg', endDate: new Date(Date.now() + 24 * 60 * 60 * 1000) }, + }); + const initAppKit = await freshInitAppKit(); + initAppKit({ themeMode: 'dark' }); + const metadata = metadataPassed(); + expect('redirect' in metadata).toBe(false); + // Everything else the wallet shows the user is untouched. + expect(metadata.name).toBe('FxBlox'); + expect(metadata.url).toEqual(expect.any(String)); + }); + + it('an expired debug mode counts as off', async () => { + useSettingsStore.setState({ + debugMode: { uniqueId: 'dbg', endDate: new Date(Date.now() - 1000) }, + }); + const initAppKit = await freshInitAppKit(); + initAppKit({ themeMode: 'dark' }); + expect('redirect' in metadataPassed()).toBe(true); + }); +}); diff --git a/apps/fxblox-web/src/wallet/appkit.ts b/apps/fxblox-web/src/wallet/appkit.ts index f7c0900..bc8c410 100644 --- a/apps/fxblox-web/src/wallet/appkit.ts +++ b/apps/fxblox-web/src/wallet/appkit.ts @@ -6,7 +6,9 @@ import { createAppKit, type AppKit } from '@reown/appkit/react'; import { Ethers5Adapter } from '@reown/appkit-adapter-ethers5'; import { env } from '@/config/env'; +import { isDebugModeActive, useSettingsStore } from '@/stores/useSettingsStore'; import { APPKIT_NETWORKS, providerMetadata, skaleEuropaHub } from './chains'; +import { diag } from './diag'; let instance: AppKit | null = null; @@ -14,14 +16,37 @@ export interface InitAppKitOptions { themeMode?: 'light' | 'dark'; } +/** + * The metadata handed to the wallet in the session proposal — with one debug-mode difference. + * + * `providerMetadata.redirect` tells the wallet where to send the user back to after they approve (see + * chains.ts). A diagnostic log from the reporter's phone showed MetaMask already wedged on its splash screen + * twelve seconds after it approved a pairing and sent the user back — before any deep link was sent to it, on + * a plain resume from recent apps. Whatever puts it in that state happens on the way it returns the user, and + * the one thing here that makes it return the user is that redirect. So with debug mode on it is omitted: the + * wallet leaves the user where they are after approving, they switch back by hand, and the log says whether + * the wallet is then still healthy when they switch to it for the signature. Debug mode is an explicit opt-in + * used for exactly this kind of report; the default is unchanged. + * + * Decided here, not at the call sites, because there are two (LinkPassword's loader and WalletGate) and the + * first to run wins. AppKit reads the metadata once, so a change of debug mode needs a page reload to apply. + */ +function metadataForInit(): typeof providerMetadata | Omit { + if (!isDebugModeActive(useSettingsStore.getState().debugMode)) return providerMetadata; + const { redirect: _omitted, ...withoutRedirect } = providerMetadata; + return withoutRedirect; +} + export function initAppKit(opts: InitAppKitOptions = {}): AppKit { if (instance) return instance; + const metadata = metadataForInit(); + diag(`[wallet] AppKit init — return redirect ${'redirect' in metadata ? 'on' : 'OFF (debug mode)'}`); instance = createAppKit({ adapters: [new Ethers5Adapter()], networks: APPKIT_NETWORKS, defaultNetwork: skaleEuropaHub, projectId: env.REOWN_PROJECT_ID, - metadata: providerMetadata, + metadata, features: { analytics: false, email: false,