diff --git a/.unipi/docs/fix/2026-06-04-wallet-verification-popup-logged-in-fix.md b/.unipi/docs/fix/2026-06-04-wallet-verification-popup-logged-in-fix.md new file mode 100644 index 0000000..1b5d386 --- /dev/null +++ b/.unipi/docs/fix/2026-06-04-wallet-verification-popup-logged-in-fix.md @@ -0,0 +1,39 @@ +--- +title: "Wallet verification popup blocks already logged-in TP browser — Quick Fix" +type: quick-fix +date: 2026-06-04 +--- + +# Wallet verification popup blocks already logged-in TP browser — Quick Fix + +## Bug + +After Chrome opens TokenPocket's in-app browser through the wallet deeplink, users who are already logged in inside the TP browser still see the new address verification popup. The popup sits on top of an already-authenticated page and blocks normal use. + +## Root Cause + +The `?autoLogin=` handler in `AutoInjectedLogin` showed the verification prompt as soon as the deeplink parameter existed. It did not wait for `WalletProvider` to finish loading the existing wallet session, and it did not skip the prompt when `status === "loggedIn"`. + +## Fix + +The auto-login handler now waits while wallet status is `loading`. Once the status is known: + +- `loggedIn` strips the deeplink parameter and does not show the verification popup. +- `loggedOut` strips the deeplink parameter and shows the manual address verification prompt. + +### Files Modified + +- `src/wallet/AutoInjectedLogin.tsx` — only shows the verification gate for logged-out deeplink sessions; already logged-in TP sessions are not blocked. +- `src/wallet/injected.ts` — supports reading a connected injected address without requesting wallet permission. +- `src/locales/zh-CN.ts` — verification popup copy. +- `src/locales/en.ts` — verification popup copy. + +## Verification + +- `npx tsc --noEmit` +- `npm run format:check` +- `npm test` + +## Notes + +This preserves the manual verification gate for logged-out Chrome → TokenPocket handoff, while avoiding a blocking popup for users who already have a valid wallet session in TokenPocket's in-app browser. diff --git a/src/locales/en.ts b/src/locales/en.ts index 2a6ad69..7ca97d5 100644 --- a/src/locales/en.ts +++ b/src/locales/en.ts @@ -197,6 +197,13 @@ export const enDict: Dict = { walletTpLoginBtn: "Log in with TokenPocket", walletTpWaiting: "Waiting for your signature in TokenPocket…", walletTpReopen: "Reopen TokenPocket", + walletVerifyAddressTitle: "Verify wallet address", + walletVerifyAddressDesc: + "Confirm the wallet address you want to use. After you tap the button below, your wallet app will ask you to sign and verify the address.", + walletDetectedAddress: "Detected wallet address", + walletVerifyAddressUnknown: + "The wallet address will be requested after verification", + walletVerifyAddressButton: "Verify this address", favoritesFilters: "Filters", favoriteSessionExpired: "Your session expired. Please sign in again.", loadFailed: "Could not load your favorites.", diff --git a/src/locales/zh-CN.ts b/src/locales/zh-CN.ts index 795a631..b1cd668 100644 --- a/src/locales/zh-CN.ts +++ b/src/locales/zh-CN.ts @@ -186,6 +186,12 @@ export const zhDict: Dict = { walletTpLoginBtn: "使用 TokenPocket 登录", walletTpWaiting: "等待你在 TokenPocket 中完成签名…", walletTpReopen: "重新打开 TokenPocket", + walletVerifyAddressTitle: "验证钱包地址", + walletVerifyAddressDesc: + "请确认将使用当前钱包地址登录。点击下方按钮后,钱包 App 会要求你签名验证地址。", + walletDetectedAddress: "检测到的钱包地址", + walletVerifyAddressUnknown: "点击验证后将请求钱包地址", + walletVerifyAddressButton: "验证此地址", favoritesFilters: "筛选", favoriteSessionExpired: "登录已过期,请重新登录。", loadFailed: "无法加载收藏,请稍后重试。", diff --git a/src/wallet/AutoInjectedLogin.tsx b/src/wallet/AutoInjectedLogin.tsx index 9a51002..31b2d0a 100644 --- a/src/wallet/AutoInjectedLogin.tsx +++ b/src/wallet/AutoInjectedLogin.tsx @@ -1,15 +1,25 @@ -import { useEffect } from "react"; +import { useCallback, useEffect, useState } from "react"; +import { useI18n } from "../i18n"; import { + getConnectedInjectedAddress, getInjectedWallet, signInWithInjectedWallet, type WalletKind, } from "./injected"; -import { useWallet } from "./WalletProvider"; +import { shortenAddress, useWallet } from "./WalletProvider"; const AUTO_LOGIN_PARAM = "autoLogin"; const ETHEREUM_WAIT_MS = 8000; const ETHEREUM_POLL_MS = 200; +type AutoLoginRequest = { + kind: WalletKind; + ready: boolean; + address: string | null; + signing: boolean; + error: string; +}; + function parseKind(value: string | null): WalletKind | null { if (value === "tokenPocket" || value === "metaMask" || value === "imToken") { return value; @@ -45,40 +55,151 @@ function waitForInjected(kind: WalletKind): Promise { /** * When the page is opened via a `?autoLogin=` deeplink (typically from - * inside TokenPocket / imToken in-app browsers), wait for the wallet to inject - * `window.ethereum`, then require a wallet signature and complete a verified - * backend wallet session. Bypasses WalletConnect entirely so it works on - * networks where the WC relay is blocked. + * inside TokenPocket / imToken in-app browsers), show an explicit verification + * prompt first. The wallet signature and backend verification only start after + * the user taps the verification button, so Chrome -> wallet handoff never logs + * in silently from a cached in-app-browser session. */ export function AutoInjectedLogin() { + const { t } = useI18n(); const { completeLogin, status } = useWallet(); + const [request, setRequest] = useState(null); useEffect(() => { if (typeof window === "undefined") return; const params = new URLSearchParams(window.location.search); const kind = parseKind(params.get(AUTO_LOGIN_PARAM)); if (!kind) return; + if (status === "loading") return; stripAutoLoginParam(); - if (status === "loggedIn") return; + if (status === "loggedIn") { + setRequest(null); + return; + } let cancelled = false; - void waitForInjected(kind).then(async (ready) => { - if (cancelled || !ready) return; - try { - const res = await signInWithInjectedWallet(kind); - if (cancelled) return; - completeLogin(res.token, res.wallet); - } catch (err) { - // eslint-disable-next-line no-console - console.warn("[wallet-autologin] failed", err); - } + setRequest({ + kind, + ready: false, + address: null, + signing: false, + error: "", }); + + void waitForInjected(kind).then(async (ready) => { + if (cancelled) return; + if (!ready) { + setRequest((current) => + current?.kind === kind + ? { ...current, ready: false, error: t("walletNoBrowserWallet") } + : current, + ); + return; + } + + const address = await getConnectedInjectedAddress(kind); + if (cancelled) return; + setRequest((current) => + current?.kind === kind + ? { ...current, ready: true, address, error: "" } + : current, + ); + }); + return () => { cancelled = true; }; - // eslint-disable-next-line react-hooks/exhaustive-deps - }, []); + }, [status, t]); - return null; + const verifyAddress = useCallback(async () => { + if (!request || !request.ready || request.signing) return; + + setRequest((current) => + current ? { ...current, signing: true, error: "" } : current, + ); + try { + const res = await signInWithInjectedWallet(request.kind); + completeLogin(res.token, res.wallet); + setRequest(null); + } catch (err) { + const message = + err instanceof Error ? err.message : t("walletLoginFailed"); + setRequest((current) => + current + ? { + ...current, + signing: false, + error: message || t("walletLoginFailed"), + } + : current, + ); + // eslint-disable-next-line no-console + console.warn("[wallet-autologin] verification failed", err); + } + }, [completeLogin, request, t]); + + if (!request) return null; + + const addressLabel = request.address + ? shortenAddress(request.address) + : t("walletVerifyAddressUnknown"); + + return ( +
+
+

+ {t(walletNameKey(request.kind))} +

+

+ {t("walletVerifyAddressTitle")} +

+

+ {t("walletVerifyAddressDesc")} +

+ +
+

+ {t("walletDetectedAddress")} +

+

+ {addressLabel} +

+
+ + {request.error ? ( +

+ {request.error} +

+ ) : null} + + +
+
+ ); +} + +function walletNameKey(kind: WalletKind): string { + if (kind === "tokenPocket") return "walletTokenPocket"; + if (kind === "metaMask") return "walletMetaMask"; + return "walletImToken"; } diff --git a/src/wallet/injected.ts b/src/wallet/injected.ts index a2c0d4a..f1ecb57 100644 --- a/src/wallet/injected.ts +++ b/src/wallet/injected.ts @@ -153,6 +153,17 @@ export function getInjectedWallet(kind?: WalletKind): EthereumProvider | null { return match ?? null; } +export async function getConnectedInjectedAddress( + kind?: WalletKind, +): Promise { + const ethereum = getInjectedWallet(kind); + if (!ethereum) return null; + const accounts = await ethereum + .request({ method: "eth_accounts" }) + .catch((): unknown[] => []); + return accounts.find(isAddress) ?? null; +} + /** Diagnostic: log what injected providers the browser exposes. */ export function logWalletProviders(): void { const ethereum = getInjectedEthereum();