From 0709cddea4a1d624e0aff22f30ff9b4383415abd Mon Sep 17 00:00:00 2001 From: ehsan shariati Date: Sat, 5 Sep 2026 12:01:39 -0400 Subject: [PATCH] setup: in debug mode, put the request on the relay but do not open the wallet A diagnostic, not a feature. Two logs from the reporter's phone now say the same thing about the splash-screen hang. Every "retry" tested so far -- the request link from a real tap, the bare scheme -- 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. Meanwhile the one recovery the logs show working is a cold MetaMask picking the request up with no deep link at all: the 25 s window in the first log has no return to the page and no second hop in it, and the signature arrived 1.5 s after the tab came back. So the untested question is the only one left: does a healthy warm MetaMask show the prompt when it is resumed WITHOUT a deep link? With debug mode on, the sign tap now publishes the request and stops there -- the request is on the relay, the "Open wallet to approve" button is on screen, and a line under it says so -- and the user can switch to the wallet from recent apps and report what it shows. The button still fires the deep link afterwards, so the same run also tests the deep link against a wallet that has NOT already been wedged. Debug mode is an explicit opt-in used for exactly this kind of report; the default path is unchanged. 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 | 3 +- .../fxblox-web/src/i18n/locales/zh/setup.json | 3 +- .../__tests__/LinkPassword.test.tsx | 52 +++++++++++++++++++ 4 files changed, 79 insertions(+), 4 deletions(-) diff --git a/apps/fxblox-web/src/components/setup/WalletSigner.tsx b/apps/fxblox-web/src/components/setup/WalletSigner.tsx index 7e95c0e..144c0c4 100644 --- a/apps/fxblox-web/src/components/setup/WalletSigner.tsx +++ b/apps/fxblox-web/src/components/setup/WalletSigner.tsx @@ -42,6 +42,7 @@ 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'; @@ -150,9 +151,12 @@ 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 }); - latest.current = { password, onLinkingChange, onPhaseChange, onSignature, onError, wallet }; + const latest = useRef({ password, onLinkingChange, onPhaseChange, onSignature, onError, wallet, debug }); + latest.current = { password, onLinkingChange, onPhaseChange, onSignature, onError, wallet, debug }; useEffect(() => { setAppKitTheme(mode); @@ -199,6 +203,7 @@ export default function WalletSigner({ setShowNudge(false); setShowStuckHint(false); setWalletShowedNothing(false); + setHopSkippedForDebug(false); wentToWalletRef.current = false; return undefined; } @@ -322,6 +327,17 @@ 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); @@ -402,6 +418,11 @@ 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 95e1eb7..1ee8522 100644 --- a/apps/fxblox-web/src/i18n/locales/en/setup.json +++ b/apps/fxblox-web/src/i18n/locales/en/setup.json @@ -59,7 +59,8 @@ "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" + "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." }, "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 8cc7368..d519dcb 100644 --- a/apps/fxblox-web/src/i18n/locales/zh/setup.json +++ b/apps/fxblox-web/src/i18n/locales/zh/setup.json @@ -59,7 +59,8 @@ "walletConnectedTapSign": "钱包已连接。点击下方按钮进行签名 — 这会再次打开您的钱包。", "approveInWallet": "请在您的钱包中确认此请求…", "walletStuckHint": "钱包已打开但停在启动画面?请从最近任务中彻底关闭钱包,然后再次点击“打开钱包进行签名” — 不先关闭钱包而直接再次点击是没有用的。您的签名请求仍然有效。如果每次都这样,说明 Android 正在后台限制钱包:请在 设置 → 应用 → (应用)→ 电池 中,将 MetaMask 和您的浏览器设为“不受限制”,问题即可消除。", - "openWalletToApprove": "打开钱包进行签名" + "openWalletToApprove": "打开钱包进行签名", + "debugNoAutoOpen": "调试模式:未自动打开钱包。请求已在中继上等待 — 请从最近任务中切换到您的钱包并记录它显示的内容,或点击“打开钱包进行签名”。" }, "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 019386f..5764ed3 100644 --- a/apps/fxblox-web/src/screens/InitialSetup/__tests__/LinkPassword.test.tsx +++ b/apps/fxblox-web/src/screens/InitialSetup/__tests__/LinkPassword.test.tsx @@ -52,6 +52,7 @@ 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'; @@ -455,6 +456,57 @@ 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();