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
25 changes: 23 additions & 2 deletions apps/fxblox-web/src/components/setup/WalletSigner.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -150,9 +151,12 @@ export default function WalletSigner({
const [requestLink, setRequestLink] = useState<string | null>(null);
const requestLinkRef = useRef<string | null>(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);
Expand Down Expand Up @@ -199,6 +203,7 @@ export default function WalletSigner({
setShowNudge(false);
setShowStuckHint(false);
setWalletShowedNothing(false);
setHopSkippedForDebug(false);
wentToWalletRef.current = false;
return undefined;
}
Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -402,6 +418,11 @@ export default function WalletSigner({
{t('setup.linkPassword.walletStuckHint')}
</FxText>
)}
{hopSkippedForDebug && (
<FxText variant="bodyXSRegular" color="warningBase" textAlign="center" testID="debug-no-auto-open">
{t('setup.linkPassword.debugNoAutoOpen')}
</FxText>
)}
{showOpenWallet && walletLink && (
<FxButton size="large" onPress={openWallet} testID="open-wallet">
{t('setup.linkPassword.openWalletToApprove')}
Expand Down
3 changes: 2 additions & 1 deletion apps/fxblox-web/src/i18n/locales/en/setup.json
Original file line number Diff line number Diff line change
Expand Up @@ -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.",
Expand Down
3 changes: 2 additions & 1 deletion apps/fxblox-web/src/i18n/locales/zh/setup.json
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,8 @@
"walletConnectedTapSign": "钱包已连接。点击下方按钮进行签名 — 这会再次打开您的钱包。",
"approveInWallet": "请在您的钱包中确认此请求…",
"walletStuckHint": "钱包已打开但停在启动画面?请从最近任务中彻底关闭钱包,然后再次点击“打开钱包进行签名” — 不先关闭钱包而直接再次点击是没有用的。您的签名请求仍然有效。如果每次都这样,说明 Android 正在后台限制钱包:请在 设置 → 应用 → (应用)→ 电池 中,将 MetaMask 和您的浏览器设为“不受限制”,问题即可消除。",
"openWalletToApprove": "打开钱包进行签名"
"openWalletToApprove": "打开钱包进行签名",
"debugNoAutoOpen": "调试模式:未自动打开钱包。请求已在中继上等待 — 请从最近任务中切换到您的钱包并记录它显示的内容,或点击“打开钱包进行签名”。"
},
"connectToBlox": {
"intro": "打开您的 Blox,然后通过蓝牙连接。如果不成功,我们会为您提供其他方式。",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -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<string>((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();
Expand Down
Loading