terry-staging #16

Merged
terry merged 96 commits from terry-staging into main 2026-06-05 16:33:12 +00:00
5 changed files with 204 additions and 20 deletions
Showing only changes of commit 469e53a860 - Show all commits

View File

@@ -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.

View File

@@ -197,6 +197,13 @@ export const enDict: Dict = {
walletTpLoginBtn: "Log in with TokenPocket", walletTpLoginBtn: "Log in with TokenPocket",
walletTpWaiting: "Waiting for your signature in TokenPocket…", walletTpWaiting: "Waiting for your signature in TokenPocket…",
walletTpReopen: "Reopen 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", favoritesFilters: "Filters",
favoriteSessionExpired: "Your session expired. Please sign in again.", favoriteSessionExpired: "Your session expired. Please sign in again.",
loadFailed: "Could not load your favorites.", loadFailed: "Could not load your favorites.",

View File

@@ -186,6 +186,12 @@ export const zhDict: Dict = {
walletTpLoginBtn: "使用 TokenPocket 登录", walletTpLoginBtn: "使用 TokenPocket 登录",
walletTpWaiting: "等待你在 TokenPocket 中完成签名…", walletTpWaiting: "等待你在 TokenPocket 中完成签名…",
walletTpReopen: "重新打开 TokenPocket", walletTpReopen: "重新打开 TokenPocket",
walletVerifyAddressTitle: "验证钱包地址",
walletVerifyAddressDesc:
"请确认将使用当前钱包地址登录。点击下方按钮后,钱包 App 会要求你签名验证地址。",
walletDetectedAddress: "检测到的钱包地址",
walletVerifyAddressUnknown: "点击验证后将请求钱包地址",
walletVerifyAddressButton: "验证此地址",
favoritesFilters: "筛选", favoritesFilters: "筛选",
favoriteSessionExpired: "登录已过期,请重新登录。", favoriteSessionExpired: "登录已过期,请重新登录。",
loadFailed: "无法加载收藏,请稍后重试。", loadFailed: "无法加载收藏,请稍后重试。",

View File

@@ -1,15 +1,25 @@
import { useEffect } from "react"; import { useCallback, useEffect, useState } from "react";
import { useI18n } from "../i18n";
import { import {
getConnectedInjectedAddress,
getInjectedWallet, getInjectedWallet,
signInWithInjectedWallet, signInWithInjectedWallet,
type WalletKind, type WalletKind,
} from "./injected"; } from "./injected";
import { useWallet } from "./WalletProvider"; import { shortenAddress, useWallet } from "./WalletProvider";
const AUTO_LOGIN_PARAM = "autoLogin"; const AUTO_LOGIN_PARAM = "autoLogin";
const ETHEREUM_WAIT_MS = 8000; const ETHEREUM_WAIT_MS = 8000;
const ETHEREUM_POLL_MS = 200; 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 { function parseKind(value: string | null): WalletKind | null {
if (value === "tokenPocket" || value === "metaMask" || value === "imToken") { if (value === "tokenPocket" || value === "metaMask" || value === "imToken") {
return value; return value;
@@ -45,40 +55,151 @@ function waitForInjected(kind: WalletKind): Promise<boolean> {
/** /**
* When the page is opened via a `?autoLogin=<wallet>` deeplink (typically from * When the page is opened via a `?autoLogin=<wallet>` deeplink (typically from
* inside TokenPocket / imToken in-app browsers), wait for the wallet to inject * inside TokenPocket / imToken in-app browsers), show an explicit verification
* `window.ethereum`, then require a wallet signature and complete a verified * prompt first. The wallet signature and backend verification only start after
* backend wallet session. Bypasses WalletConnect entirely so it works on * the user taps the verification button, so Chrome -> wallet handoff never logs
* networks where the WC relay is blocked. * in silently from a cached in-app-browser session.
*/ */
export function AutoInjectedLogin() { export function AutoInjectedLogin() {
const { t } = useI18n();
const { completeLogin, status } = useWallet(); const { completeLogin, status } = useWallet();
const [request, setRequest] = useState<AutoLoginRequest | null>(null);
useEffect(() => { useEffect(() => {
if (typeof window === "undefined") return; if (typeof window === "undefined") return;
const params = new URLSearchParams(window.location.search); const params = new URLSearchParams(window.location.search);
const kind = parseKind(params.get(AUTO_LOGIN_PARAM)); const kind = parseKind(params.get(AUTO_LOGIN_PARAM));
if (!kind) return; if (!kind) return;
if (status === "loading") return;
stripAutoLoginParam(); stripAutoLoginParam();
if (status === "loggedIn") return; if (status === "loggedIn") {
setRequest(null);
return;
}
let cancelled = false; let cancelled = false;
void waitForInjected(kind).then(async (ready) => { setRequest({
if (cancelled || !ready) return; kind,
try { ready: false,
const res = await signInWithInjectedWallet(kind); address: null,
if (cancelled) return; signing: false,
completeLogin(res.token, res.wallet); error: "",
} catch (err) {
// eslint-disable-next-line no-console
console.warn("[wallet-autologin] failed", err);
}
}); });
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 () => { return () => {
cancelled = true; 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 (
<div
className="fixed inset-0 z-[130] flex items-end justify-center bg-black/75 px-3 pb-3 pt-10 backdrop-blur-sm md:items-center md:p-6"
role="dialog"
aria-modal="true"
aria-labelledby="wallet-verify-title"
>
<div className="w-full max-w-[420px] rounded-3xl border border-white/10 bg-[#17171d] p-5 shadow-2xl shadow-black/70 md:p-6">
<p className="text-xs font-semibold uppercase tracking-[0.24em] text-ark-gold">
{t(walletNameKey(request.kind))}
</p>
<h2
id="wallet-verify-title"
className="mt-3 text-xl font-semibold text-white"
>
{t("walletVerifyAddressTitle")}
</h2>
<p className="mt-2 text-sm leading-6 text-neutral-400">
{t("walletVerifyAddressDesc")}
</p>
<div className="mt-5 rounded-2xl border border-white/10 bg-white/[0.03] px-4 py-3">
<p className="text-xs text-neutral-500">
{t("walletDetectedAddress")}
</p>
<p className="mt-1 break-all font-mono text-sm text-neutral-100">
{addressLabel}
</p>
</div>
{request.error ? (
<p className="mt-4 rounded-2xl border border-red-500/30 bg-red-500/10 px-4 py-3 text-sm text-red-200">
{request.error}
</p>
) : null}
<button
type="button"
onClick={verifyAddress}
disabled={!request.ready || request.signing}
className="mt-5 w-full rounded-full bg-ark-gold px-4 py-3 text-sm font-bold text-black transition hover:bg-ark-gold2 disabled:cursor-wait disabled:opacity-70"
>
{request.signing
? t("walletSigning")
: request.ready
? t("walletVerifyAddressButton")
: t("walletConnecting")}
</button>
</div>
</div>
);
}
function walletNameKey(kind: WalletKind): string {
if (kind === "tokenPocket") return "walletTokenPocket";
if (kind === "metaMask") return "walletMetaMask";
return "walletImToken";
} }

View File

@@ -153,6 +153,17 @@ export function getInjectedWallet(kind?: WalletKind): EthereumProvider | null {
return match ?? null; return match ?? null;
} }
export async function getConnectedInjectedAddress(
kind?: WalletKind,
): Promise<string | null> {
const ethereum = getInjectedWallet(kind);
if (!ethereum) return null;
const accounts = await ethereum
.request<unknown[]>({ method: "eth_accounts" })
.catch((): unknown[] => []);
return accounts.find(isAddress) ?? null;
}
/** Diagnostic: log what injected providers the browser exposes. */ /** Diagnostic: log what injected providers the browser exposes. */
export function logWalletProviders(): void { export function logWalletProviders(): void {
const ethereum = getInjectedEthereum(); const ethereum = getInjectedEthereum();