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 (