terry-staging #16

Merged
terry merged 96 commits from terry-staging into main 2026-06-05 16:33:12 +00:00
4 changed files with 242 additions and 109 deletions
Showing only changes of commit 6800a8e9b6 - Show all commits

View File

@@ -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>

View 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;
}

View File

@@ -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")}

View 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;
}
}