From 6800a8e9b68ff17bca32d0ba0d3d5816a5e1ac22 Mon Sep 17 00:00:00 2001 From: TerryM Date: Wed, 3 Jun 2026 20:07:23 +0800 Subject: [PATCH] feat(wallet): bypass WalletConnect for TP/imToken on mobile to fix China users The WalletConnect relay (wss://relay.walletconnect.org) is unreliable/blocked in mainland China. Every wallet flow (desktop QR, mobile deeplink, mobile QR) depends on it, so Chinese users see the login button hang forever and the QR code never appears. When RainbowKit's render fails, the whole site goes white because nothing catches the error. Changes: - Add WalletStackErrorBoundary around + modal so RainbowKit init failures no longer blank the entire app. - Hoist above the boundary; it only depends on the injected provider, so useWallet keeps working for header / favorites / etc. even when the WC stack is dead. - On mobile, the TP/imToken 'Open Wallet App' button now navigates directly to tpdapp://open / imtokenv2://navigate/DappView with an ?autoLogin= query, pulling the site into the wallet's in-app browser without ever touching the WC relay. MetaMask still uses the WC path (no equivalent deeplink). - Add AutoInjectedLogin: when the page loads with ?autoLogin=, wait up to 8s for window.ethereum, then connectInjectedWallet + completeLogin. Strips the param via history.replaceState to avoid re-firing on reload. - Guard against the in-app-browser disconnect/reconnect case: if getInjectedWallet(kind) is already truthy, skip the deeplink and let useWalletConnectLogin's deeplink mode take the injected fast path (avoids TP trying to open TP recursively). --- src/App.tsx | 212 ++++++++++++------------ src/wallet/AutoInjectedLogin.tsx | 83 ++++++++++ src/wallet/WalletLoginModal.tsx | 34 +++- src/wallet/WalletStackErrorBoundary.tsx | 22 +++ 4 files changed, 242 insertions(+), 109 deletions(-) create mode 100644 src/wallet/AutoInjectedLogin.tsx create mode 100644 src/wallet/WalletStackErrorBoundary.tsx diff --git a/src/App.tsx b/src/App.tsx index f851274..2cae08b 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -4,9 +4,11 @@ import { MotionProvider } from "./motion"; import { ToastProvider } from "./components/Toast"; import { SaveToAlbumGuideProvider } from "./components/SaveToAlbumGuide"; import { FavoritesProvider } from "./favorites/FavoritesProvider"; +import { AutoInjectedLogin } from "./wallet/AutoInjectedLogin"; import { RainbowWalletProvider } from "./wallet/RainbowWalletProvider"; import { WalletLoginModal } from "./wallet/WalletLoginModal"; import { WalletProvider } from "./wallet/WalletProvider"; +import { WalletStackErrorBoundary } from "./wallet/WalletStackErrorBoundary"; import { PublicLayout } from "./layouts/PublicLayout"; import { LocalizedHomePage } from "./pages/LocalizedHome"; import { Browse } from "./pages/Browse"; @@ -32,115 +34,113 @@ export default function App() { - - - - - - - - - - - - - }> - {/* English (root, no prefix) */} - - } - /> - } /> - } - /> - } - /> - } - /> - } - /> - } - /> - } - /> - - {/* Each non-English language gets its own nested tree. */} - {localizedHomeRoutes.map((route) => ( - - - } - /> - } /> - } - /> - } - /> - } - /> - } - /> - } - /> - } - /> - - ))} - - - {adminEnabled ? ( - AdminRouteTree() - ) : ( - } - /> - )} - + + + + + + + + + + + + + + + + + }> + {/* English (root, no prefix) */} } + /> + } /> + } + /> + } + /> + } + /> + } /> + } + /> + } + /> + + {/* Each non-English language gets its own nested tree. */} + {localizedHomeRoutes.map((route) => ( + + + } + /> + } /> + } + /> + } + /> + } + /> + } + /> + } + /> + } + /> + + ))} + + + {adminEnabled ? ( + AdminRouteTree() + ) : ( + } /> - - - - - - - - - - + )} + + } + /> + + + + + + + + + diff --git a/src/wallet/AutoInjectedLogin.tsx b/src/wallet/AutoInjectedLogin.tsx new file mode 100644 index 0000000..dfc5178 --- /dev/null +++ b/src/wallet/AutoInjectedLogin.tsx @@ -0,0 +1,83 @@ +import { useEffect } from "react"; +import { + connectInjectedWallet, + getInjectedWallet, + type WalletKind, +} from "./injected"; +import { localWalletToken, useWallet } from "./WalletProvider"; + +const AUTO_LOGIN_PARAM = "autoLogin"; +const ETHEREUM_WAIT_MS = 8000; +const ETHEREUM_POLL_MS = 200; + +function parseKind(value: string | null): WalletKind | null { + if (value === "tokenPocket" || value === "metaMask" || value === "imToken") { + return value; + } + return null; +} + +function stripAutoLoginParam(): void { + const url = new URL(window.location.href); + url.searchParams.delete(AUTO_LOGIN_PARAM); + const qs = url.searchParams.toString(); + const next = url.pathname + (qs ? `?${qs}` : "") + url.hash; + window.history.replaceState({}, "", next); +} + +function waitForInjected(kind: WalletKind): Promise { + return new Promise((resolve) => { + const start = Date.now(); + const tick = () => { + if (getInjectedWallet(kind)) { + resolve(true); + return; + } + if (Date.now() - start >= ETHEREUM_WAIT_MS) { + resolve(false); + return; + } + window.setTimeout(tick, ETHEREUM_POLL_MS); + }; + tick(); + }); +} + +/** + * 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 complete a local wallet session automatically. Bypasses + * WalletConnect entirely so it works on networks where the WC relay is blocked. + */ +export function AutoInjectedLogin() { + const { completeLogin, status } = useWallet(); + + useEffect(() => { + if (typeof window === "undefined") return; + const params = new URLSearchParams(window.location.search); + const kind = parseKind(params.get(AUTO_LOGIN_PARAM)); + if (!kind) return; + + stripAutoLoginParam(); + if (status === "loggedIn") return; + + let cancelled = false; + void waitForInjected(kind).then(async (ready) => { + if (cancelled || !ready) return; + try { + const address = await connectInjectedWallet(kind); + if (cancelled) return; + completeLogin(localWalletToken(address), address); + } catch (err) { + // eslint-disable-next-line no-console + console.warn("[wallet-autologin] failed", err); + } + }); + return () => { + cancelled = true; + }; + // eslint-disable-next-line react-hooks/exhaustive-deps + }, []); + + return null; +} diff --git a/src/wallet/WalletLoginModal.tsx b/src/wallet/WalletLoginModal.tsx index 34d7645..1ed82ca 100644 --- a/src/wallet/WalletLoginModal.tsx +++ b/src/wallet/WalletLoginModal.tsx @@ -2,11 +2,24 @@ import { QRCodeSVG } from "qrcode.react"; import { LoaderCircle, X } from "lucide-react"; import { useEffect, useState } from "react"; import { useI18n } from "../i18n"; -import type { WalletKind } from "./injected"; +import { walletDeepLink } from "./deepLinks"; +import { getInjectedWallet, type WalletKind } from "./injected"; import { useWallet } from "./WalletProvider"; import { useWalletConnectLogin } from "./useWalletConnectLogin"; import { WalletBrandIcon } from "./WalletBrandIcon"; +const AUTO_LOGIN_PARAM = "autoLogin"; + +function supportsDirectPull(kind: WalletKind): boolean { + return kind === "tokenPocket" || kind === "imToken"; +} + +function buildAutoLoginDappUrl(kind: WalletKind): string { + const url = new URL(window.location.href); + url.searchParams.set(AUTO_LOGIN_PARAM, kind); + return url.toString(); +} + const wallets: WalletKind[] = ["tokenPocket", "metaMask", "imToken"]; function isMobileDevice(): boolean { @@ -61,6 +74,19 @@ export function WalletLoginModal() { void wc.start(kind, mode); }; + const openWalletAppDirect = (kind: WalletKind) => { + if (getInjectedWallet(kind)) { + startWalletLogin(kind, "deeplink"); + return; + } + if (mobileDevice && supportsDirectPull(kind)) { + const deeplink = walletDeepLink(kind, buildAutoLoginDappUrl(kind)); + window.location.href = deeplink; + return; + } + startWalletLogin(kind, "deeplink"); + }; + return (