Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
65 changes: 65 additions & 0 deletions apps/fxblox-web/src/wallet/__tests__/appkit.test.ts
Original file line number Diff line number Diff line change
@@ -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<string, unknown> {
const call = createAppKitMock.mock.calls.at(-1)?.[0] as { metadata: Record<string, unknown> } | 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);
});
});
27 changes: 26 additions & 1 deletion apps/fxblox-web/src/wallet/appkit.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,22 +6,47 @@
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;

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<typeof providerMetadata, 'redirect'> {
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,
Expand Down
Loading