From 2e7eaad59329a56d7c7d1431e1c6c74dc273ba47 Mon Sep 17 00:00:00 2001 From: ehsan shariati Date: Sat, 5 Sep 2026 16:23:07 -0400 Subject: [PATCH] wallet: retire the two debug-mode experiments, and say what actually clears a stuck wallet Reverts #40 (debug mode: publish the request but do not open the wallet) and #41 (debug mode: hand the wallet no return address). Both were diagnostics, and they did their job: four device logs showed MetaMask wedged on its splash screen on every WARM resume -- by our deep link, by the manual button, by recent apps, with the redirect on, with it off -- and working on every COLD launch, with or without a deep link. That isolates the hang to MetaMask's own resume on that phone. Nothing in the app distinguishes the working case from the failing one. Left in, the two toggles would give anyone who enables debug mode for support an altered wallet flow for no further gain. Untouched, deliberately: the return path. #36/#38's wake and #39's park-on-hidden are what took the return from the wallet from 8.6 s to under a second, and the same logs confirm it three times over. `git revert` of exactly the two experiment commits; relayWake.ts and diag.ts are not in the diff. The stuck-wallet hint stops blaming Android's battery restriction, which the same logs ruled out (the wedge appears within seconds of backgrounding; battery restriction acts over minutes). It now says the thing every log showed working: close the wallet from recent apps and open it again -- from its icon or with the button -- and it shows the request. Co-Authored-By: Claude Fable 5.1 Claude-Session: https://claude.ai/code/session_013BpqXrkEPA9odTyRdK5Mnx --- .../src/components/setup/WalletSigner.tsx | 25 +------ .../fxblox-web/src/i18n/locales/en/setup.json | 5 +- .../fxblox-web/src/i18n/locales/zh/setup.json | 5 +- .../__tests__/LinkPassword.test.tsx | 52 --------------- .../src/wallet/__tests__/appkit.test.ts | 65 ------------------- apps/fxblox-web/src/wallet/appkit.ts | 27 +------- 6 files changed, 7 insertions(+), 172 deletions(-) delete mode 100644 apps/fxblox-web/src/wallet/__tests__/appkit.test.ts diff --git a/apps/fxblox-web/src/components/setup/WalletSigner.tsx b/apps/fxblox-web/src/components/setup/WalletSigner.tsx index 144c0c4..7e95c0e 100644 --- a/apps/fxblox-web/src/components/setup/WalletSigner.tsx +++ b/apps/fxblox-web/src/components/setup/WalletSigner.tsx @@ -42,7 +42,6 @@ import { useCallback, useEffect, useRef, useState } from 'react'; import { useTranslation } from 'react-i18next'; import { FxBox, FxButton, FxSpinner, FxText } from '@functionland/fx-ui'; -import { useLogger } from '@/hooks/useLogger'; import { useColorMode } from '@/stores/useSettingsStore'; import { getAppKit, setAppKitTheme } from '@/wallet/appkit'; import { diag } from '@/wallet/diag'; @@ -151,12 +150,9 @@ export default function WalletSigner({ const [requestLink, setRequestLink] = useState(null); const requestLinkRef = useRef(null); requestLinkRef.current = requestLink; - // Debug mode turns the automatic app-switch off — see the hop below for why that is a diagnostic, not a feature. - const { isDebugModeEnable: debug } = useLogger(); - const [hopSkippedForDebug, setHopSkippedForDebug] = useState(false); // Latest props / wallet state for the stable callbacks below (the mobile effects closed over stale state). - const latest = useRef({ password, onLinkingChange, onPhaseChange, onSignature, onError, wallet, debug }); - latest.current = { password, onLinkingChange, onPhaseChange, onSignature, onError, wallet, debug }; + const latest = useRef({ password, onLinkingChange, onPhaseChange, onSignature, onError, wallet }); + latest.current = { password, onLinkingChange, onPhaseChange, onSignature, onError, wallet }; useEffect(() => { setAppKitTheme(mode); @@ -203,7 +199,6 @@ export default function WalletSigner({ setShowNudge(false); setShowStuckHint(false); setWalletShowedNothing(false); - setHopSkippedForDebug(false); wentToWalletRef.current = false; return undefined; } @@ -327,17 +322,6 @@ export default function WalletSigner({ // remaining case is a navigation in a shape we did not recognise: the wallet is already in front, and // hopping again would bounce the user twice. if (capture.captured() || !capture.sawOpen()) { - // Debug mode: leave the wallet alone. Every retry so far went into a MetaMask that the FIRST hop had - // already wedged on its splash screen, so none of them tested whether the deep link itself is what - // wedges a healthy warm wallet — while the one recovery the logs show working was a cold MetaMask - // picking the request up with no deep link at all. With the request on the relay and no hop, the - // user can switch to the wallet by hand and see what it shows; the button is still there for the - // deep link afterwards. Debug mode is an explicit opt-in used for exactly this kind of report. - if (latest.current.debug) { - diag('[sign] debug mode: NOT opening the wallet — switch to it by hand, or tap the button'); - setHopSkippedForDebug(true); - return; - } diag(`[sign] opening the wallet on the request: ${link} activation=${userActivation()}`); wentToWalletRef.current = true; hopToWallet(link); @@ -418,11 +402,6 @@ export default function WalletSigner({ {t('setup.linkPassword.walletStuckHint')} )} - {hopSkippedForDebug && ( - - {t('setup.linkPassword.debugNoAutoOpen')} - - )} {showOpenWallet && walletLink && ( {t('setup.linkPassword.openWalletToApprove')} diff --git a/apps/fxblox-web/src/i18n/locales/en/setup.json b/apps/fxblox-web/src/i18n/locales/en/setup.json index 1ee8522..d16a629 100644 --- a/apps/fxblox-web/src/i18n/locales/en/setup.json +++ b/apps/fxblox-web/src/i18n/locales/en/setup.json @@ -58,9 +58,8 @@ "signaturePortalHint": "Sign at fxblox.fx.land, then paste the signature below together with the wallet address you signed with.", "walletConnectedTapSign": "Wallet connected. Tap below to approve the signature — this opens your wallet again.", "approveInWallet": "Approve the request in your wallet…", - "walletStuckHint": "Wallet opened but stuck on its splash screen? Close it completely from your recent apps, then tap Open wallet to approve again — tapping without closing it first will not help. Your request stays valid. If this happens every time, Android is restricting the wallet in the background: set MetaMask and your browser to Unrestricted under Settings → Apps → (app) → Battery, and it stops.", - "openWalletToApprove": "Open wallet to approve", - "debugNoAutoOpen": "Debug mode: the wallet was not opened automatically. The request is waiting on the relay — switch to your wallet from your recent apps and note what it shows, or tap Open wallet to approve." + "walletStuckHint": "Wallet opened but stuck on its splash screen? Close it completely from your recent apps, then open it again — from its icon, or with Open wallet to approve — and it will show the request. Opening it again without closing it first will not help. Your request stays valid, so nothing is lost.", + "openWalletToApprove": "Open wallet to approve" }, "connectToBlox": { "intro": "Turn your Blox on, then connect to it with Bluetooth. We'll suggest other ways if that doesn't work.", diff --git a/apps/fxblox-web/src/i18n/locales/zh/setup.json b/apps/fxblox-web/src/i18n/locales/zh/setup.json index d519dcb..f529ef0 100644 --- a/apps/fxblox-web/src/i18n/locales/zh/setup.json +++ b/apps/fxblox-web/src/i18n/locales/zh/setup.json @@ -58,9 +58,8 @@ "signaturePortalHint": "请在 fxblox.fx.land 完成签名,然后将签名与用于签名的钱包地址一起粘贴到下方。", "walletConnectedTapSign": "钱包已连接。点击下方按钮进行签名 — 这会再次打开您的钱包。", "approveInWallet": "请在您的钱包中确认此请求…", - "walletStuckHint": "钱包已打开但停在启动画面?请从最近任务中彻底关闭钱包,然后再次点击“打开钱包进行签名” — 不先关闭钱包而直接再次点击是没有用的。您的签名请求仍然有效。如果每次都这样,说明 Android 正在后台限制钱包:请在 设置 → 应用 → (应用)→ 电池 中,将 MetaMask 和您的浏览器设为“不受限制”,问题即可消除。", - "openWalletToApprove": "打开钱包进行签名", - "debugNoAutoOpen": "调试模式:未自动打开钱包。请求已在中继上等待 — 请从最近任务中切换到您的钱包并记录它显示的内容,或点击“打开钱包进行签名”。" + "walletStuckHint": "钱包已打开但停在启动画面?请从最近任务中彻底关闭钱包,然后重新打开 — 点击钱包图标或点击“打开钱包进行签名”均可 — 它会立即显示签名请求。不先关闭钱包而直接再次打开是没有用的。您的签名请求仍然有效,不会丢失。", + "openWalletToApprove": "打开钱包进行签名" }, "connectToBlox": { "intro": "打开您的 Blox,然后通过蓝牙连接。如果不成功,我们会为您提供其他方式。", diff --git a/apps/fxblox-web/src/screens/InitialSetup/__tests__/LinkPassword.test.tsx b/apps/fxblox-web/src/screens/InitialSetup/__tests__/LinkPassword.test.tsx index 5764ed3..019386f 100644 --- a/apps/fxblox-web/src/screens/InitialSetup/__tests__/LinkPassword.test.tsx +++ b/apps/fxblox-web/src/screens/InitialSetup/__tests__/LinkPassword.test.tsx @@ -52,7 +52,6 @@ vi.mock('@/platform/linking', async (importOriginal) => { }); import { WALLET_NUDGE_MS, WALLET_STUCK_MS } from '@/components/setup/WalletSigner'; -import { useSettingsStore } from '@/stores/useSettingsStore'; import { useUserProfileStore } from '@/stores/useUserProfileStore'; import * as linking from '@/platform/linking'; import * as secureStore from '@/platform/secureStore'; @@ -456,57 +455,6 @@ describe('LinkPassword', () => { } }); - it('wallet path: in debug mode the request goes out but the wallet is NOT opened automatically', async () => { - // A diagnostic, not a feature. Every retry so far went into a wallet the first hop had already wedged, so - // nothing has tested whether the deep link itself wedges a healthy warm wallet. With the request on the - // relay and no hop, the user can switch to the wallet by hand and report what it shows; the button stays - // for the deep link afterwards. - const user = userEvent.setup(); - vi.mocked(linking.assign).mockClear(); - const listeners = new Set<(payload: unknown) => void>(); - let approve: (sig: string) => void = () => undefined; - wallet.state.account = '0xABC'; - wallet.state.connected = true; - wallet.state.provider = { - request: vi.fn(() => { - for (const fn of [...listeners]) - fn({ topic: 'topic-1', request: {}, chainId: 'eip155:1', id: 42 }); - return new Promise((resolve) => (approve = resolve)); - }), - session: { peer: { metadata: { redirect: { native: 'metamask://' } } } }, - client: { - on: (_event: string, fn: (payload: unknown) => void) => listeners.add(fn), - off: (_event: string, fn: (payload: unknown) => void) => listeners.delete(fn), - }, - } as never; - useSettingsStore.setState({ - debugMode: { uniqueId: 'dbg', endDate: new Date(Date.now() + 24 * 60 * 60 * 1000) }, - }); - try { - await renderSetupAt('/setup/link-password'); - await fillPasswordAndConsent(user); - await user.click(await screen.findByTestId('sign-with-wallet')); - - // Past the macrotask the hop is deferred by, so "not yet" cannot masquerade as "never". - expect(await screen.findByTestId('debug-no-auto-open')).toBeInTheDocument(); - await act(async () => { - await new Promise((resolve) => setTimeout(resolve, 0)); - }); - expect(linking.assign).not.toHaveBeenCalled(); - - // The deep link is still one tap away. - await user.click(screen.getByTestId('open-wallet')); - expect(linking.assign).toHaveBeenCalledWith('metamask://wc?requestId=42&sessionTopic=topic-1'); - - await act(async () => { - approve('0xlate'); - }); - await waitFor(() => expect(useUserProfileStore.getState().signiture).toBe('0xlate')); - } finally { - useSettingsStore.setState({ debugMode: undefined }); - } - }); - it('wallet path: a visibility change before any hop is not treated as a wedged wallet', async () => { // Tab switches happen for all sorts of reasons. Only a return from a wallet WE sent them to is evidence. const user = userEvent.setup(); diff --git a/apps/fxblox-web/src/wallet/__tests__/appkit.test.ts b/apps/fxblox-web/src/wallet/__tests__/appkit.test.ts deleted file mode 100644 index ac311d0..0000000 --- a/apps/fxblox-web/src/wallet/__tests__/appkit.test.ts +++ /dev/null @@ -1,65 +0,0 @@ -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 bc8c410..f7c0900 100644 --- a/apps/fxblox-web/src/wallet/appkit.ts +++ b/apps/fxblox-web/src/wallet/appkit.ts @@ -6,9 +6,7 @@ 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; @@ -16,37 +14,14 @@ 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, + metadata: providerMetadata, features: { analytics: false, email: false,