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,