terry-staging #16
20
src/App.tsx
20
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,15 +34,19 @@ export default function App() {
|
||||
<I18nProvider>
|
||||
<MotionProvider>
|
||||
<ToastProvider>
|
||||
<RainbowWalletProvider>
|
||||
<WalletProvider>
|
||||
<AutoInjectedLogin />
|
||||
<WalletStackErrorBoundary>
|
||||
<RainbowWalletProvider>
|
||||
<WalletLoginModal />
|
||||
</RainbowWalletProvider>
|
||||
</WalletStackErrorBoundary>
|
||||
<FavoritesProvider>
|
||||
<SaveToAlbumGuideProvider>
|
||||
<AdminRouterModeProvider value="absolute">
|
||||
<ImageLightboxProvider>
|
||||
<VideoPlayerProvider>
|
||||
<PageTitleProvider>
|
||||
<WalletLoginModal />
|
||||
<BrowserRouter>
|
||||
<ScrollToTop />
|
||||
<Routes>
|
||||
@@ -48,9 +54,7 @@ export default function App() {
|
||||
{/* English (root, no prefix) */}
|
||||
<Route
|
||||
path="/"
|
||||
element={
|
||||
<LocalizedHomePage targetLang="en" />
|
||||
}
|
||||
element={<LocalizedHomePage targetLang="en" />}
|
||||
/>
|
||||
<Route path="/browse" element={<Browse />} />
|
||||
<Route
|
||||
@@ -65,10 +69,7 @@ export default function App() {
|
||||
path="/category/:slug"
|
||||
element={<CategoryPage />}
|
||||
/>
|
||||
<Route
|
||||
path="/search"
|
||||
element={<SearchPage />}
|
||||
/>
|
||||
<Route path="/search" element={<SearchPage />} />
|
||||
<Route
|
||||
path="/resource/:id"
|
||||
element={<PostRedirect />}
|
||||
@@ -140,7 +141,6 @@ export default function App() {
|
||||
</SaveToAlbumGuideProvider>
|
||||
</FavoritesProvider>
|
||||
</WalletProvider>
|
||||
</RainbowWalletProvider>
|
||||
</ToastProvider>
|
||||
</MotionProvider>
|
||||
</I18nProvider>
|
||||
|
||||
83
src/wallet/AutoInjectedLogin.tsx
Normal file
83
src/wallet/AutoInjectedLogin.tsx
Normal file
@@ -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<boolean> {
|
||||
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=<wallet>` 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;
|
||||
}
|
||||
@@ -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 (
|
||||
<div
|
||||
className="fixed inset-0 z-[120] flex items-end justify-center bg-black/70 px-3 pb-3 pt-10 backdrop-blur-sm md:items-center md:p-6"
|
||||
@@ -137,8 +163,10 @@ export function WalletLoginModal() {
|
||||
<div className="mt-3 grid grid-cols-2 gap-2">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => startWalletLogin(kind, "deeplink")}
|
||||
disabled={!wc.available || busy}
|
||||
onClick={() => openWalletAppDirect(kind)}
|
||||
disabled={
|
||||
supportsDirectPull(kind) ? busy : !wc.available || busy
|
||||
}
|
||||
className="rounded-full bg-ark-gold px-3 py-2 text-sm font-bold text-black transition hover:bg-ark-gold2 disabled:cursor-wait disabled:opacity-70"
|
||||
>
|
||||
{t("walletOpenWalletApp")}
|
||||
|
||||
22
src/wallet/WalletStackErrorBoundary.tsx
Normal file
22
src/wallet/WalletStackErrorBoundary.tsx
Normal file
@@ -0,0 +1,22 @@
|
||||
import { Component, type ReactNode } from "react";
|
||||
|
||||
type Props = { children: ReactNode; fallback?: ReactNode };
|
||||
type State = { hasError: boolean };
|
||||
|
||||
export class WalletStackErrorBoundary extends Component<Props, State> {
|
||||
state: State = { hasError: false };
|
||||
|
||||
static getDerivedStateFromError(): State {
|
||||
return { hasError: true };
|
||||
}
|
||||
|
||||
componentDidCatch(error: unknown): void {
|
||||
// eslint-disable-next-line no-console
|
||||
console.error("[wallet-stack] error boundary caught", error);
|
||||
}
|
||||
|
||||
render(): ReactNode {
|
||||
if (this.state.hasError) return this.props.fallback ?? null;
|
||||
return this.props.children;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user