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 (