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 { ToastProvider } from "./components/Toast";
|
||||||
import { SaveToAlbumGuideProvider } from "./components/SaveToAlbumGuide";
|
import { SaveToAlbumGuideProvider } from "./components/SaveToAlbumGuide";
|
||||||
import { FavoritesProvider } from "./favorites/FavoritesProvider";
|
import { FavoritesProvider } from "./favorites/FavoritesProvider";
|
||||||
|
import { AutoInjectedLogin } from "./wallet/AutoInjectedLogin";
|
||||||
import { RainbowWalletProvider } from "./wallet/RainbowWalletProvider";
|
import { RainbowWalletProvider } from "./wallet/RainbowWalletProvider";
|
||||||
import { WalletLoginModal } from "./wallet/WalletLoginModal";
|
import { WalletLoginModal } from "./wallet/WalletLoginModal";
|
||||||
import { WalletProvider } from "./wallet/WalletProvider";
|
import { WalletProvider } from "./wallet/WalletProvider";
|
||||||
|
import { WalletStackErrorBoundary } from "./wallet/WalletStackErrorBoundary";
|
||||||
import { PublicLayout } from "./layouts/PublicLayout";
|
import { PublicLayout } from "./layouts/PublicLayout";
|
||||||
import { LocalizedHomePage } from "./pages/LocalizedHome";
|
import { LocalizedHomePage } from "./pages/LocalizedHome";
|
||||||
import { Browse } from "./pages/Browse";
|
import { Browse } from "./pages/Browse";
|
||||||
@@ -32,15 +34,19 @@ export default function App() {
|
|||||||
<I18nProvider>
|
<I18nProvider>
|
||||||
<MotionProvider>
|
<MotionProvider>
|
||||||
<ToastProvider>
|
<ToastProvider>
|
||||||
<RainbowWalletProvider>
|
|
||||||
<WalletProvider>
|
<WalletProvider>
|
||||||
|
<AutoInjectedLogin />
|
||||||
|
<WalletStackErrorBoundary>
|
||||||
|
<RainbowWalletProvider>
|
||||||
|
<WalletLoginModal />
|
||||||
|
</RainbowWalletProvider>
|
||||||
|
</WalletStackErrorBoundary>
|
||||||
<FavoritesProvider>
|
<FavoritesProvider>
|
||||||
<SaveToAlbumGuideProvider>
|
<SaveToAlbumGuideProvider>
|
||||||
<AdminRouterModeProvider value="absolute">
|
<AdminRouterModeProvider value="absolute">
|
||||||
<ImageLightboxProvider>
|
<ImageLightboxProvider>
|
||||||
<VideoPlayerProvider>
|
<VideoPlayerProvider>
|
||||||
<PageTitleProvider>
|
<PageTitleProvider>
|
||||||
<WalletLoginModal />
|
|
||||||
<BrowserRouter>
|
<BrowserRouter>
|
||||||
<ScrollToTop />
|
<ScrollToTop />
|
||||||
<Routes>
|
<Routes>
|
||||||
@@ -48,9 +54,7 @@ export default function App() {
|
|||||||
{/* English (root, no prefix) */}
|
{/* English (root, no prefix) */}
|
||||||
<Route
|
<Route
|
||||||
path="/"
|
path="/"
|
||||||
element={
|
element={<LocalizedHomePage targetLang="en" />}
|
||||||
<LocalizedHomePage targetLang="en" />
|
|
||||||
}
|
|
||||||
/>
|
/>
|
||||||
<Route path="/browse" element={<Browse />} />
|
<Route path="/browse" element={<Browse />} />
|
||||||
<Route
|
<Route
|
||||||
@@ -65,10 +69,7 @@ export default function App() {
|
|||||||
path="/category/:slug"
|
path="/category/:slug"
|
||||||
element={<CategoryPage />}
|
element={<CategoryPage />}
|
||||||
/>
|
/>
|
||||||
<Route
|
<Route path="/search" element={<SearchPage />} />
|
||||||
path="/search"
|
|
||||||
element={<SearchPage />}
|
|
||||||
/>
|
|
||||||
<Route
|
<Route
|
||||||
path="/resource/:id"
|
path="/resource/:id"
|
||||||
element={<PostRedirect />}
|
element={<PostRedirect />}
|
||||||
@@ -140,7 +141,6 @@ export default function App() {
|
|||||||
</SaveToAlbumGuideProvider>
|
</SaveToAlbumGuideProvider>
|
||||||
</FavoritesProvider>
|
</FavoritesProvider>
|
||||||
</WalletProvider>
|
</WalletProvider>
|
||||||
</RainbowWalletProvider>
|
|
||||||
</ToastProvider>
|
</ToastProvider>
|
||||||
</MotionProvider>
|
</MotionProvider>
|
||||||
</I18nProvider>
|
</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 { LoaderCircle, X } from "lucide-react";
|
||||||
import { useEffect, useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
import { useI18n } from "../i18n";
|
import { useI18n } from "../i18n";
|
||||||
import type { WalletKind } from "./injected";
|
import { walletDeepLink } from "./deepLinks";
|
||||||
|
import { getInjectedWallet, type WalletKind } from "./injected";
|
||||||
import { useWallet } from "./WalletProvider";
|
import { useWallet } from "./WalletProvider";
|
||||||
import { useWalletConnectLogin } from "./useWalletConnectLogin";
|
import { useWalletConnectLogin } from "./useWalletConnectLogin";
|
||||||
import { WalletBrandIcon } from "./WalletBrandIcon";
|
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"];
|
const wallets: WalletKind[] = ["tokenPocket", "metaMask", "imToken"];
|
||||||
|
|
||||||
function isMobileDevice(): boolean {
|
function isMobileDevice(): boolean {
|
||||||
@@ -61,6 +74,19 @@ export function WalletLoginModal() {
|
|||||||
void wc.start(kind, mode);
|
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 (
|
return (
|
||||||
<div
|
<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"
|
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">
|
<div className="mt-3 grid grid-cols-2 gap-2">
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={() => startWalletLogin(kind, "deeplink")}
|
onClick={() => openWalletAppDirect(kind)}
|
||||||
disabled={!wc.available || busy}
|
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"
|
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")}
|
{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