import { QRCodeSVG } from "qrcode.react"; import { LoaderCircle } from "lucide-react"; import { useEffect, useState } from "react"; import { useI18n } from "../i18n"; import { createTokenPocketLoginRequest, fetchTokenPocketLoginResult, verifyWalletSignature, type TokenPocketLoginRequest, } from "./api"; import { openWalletDeepLink, walletDownloadUrl } from "./deepLinks"; import { getInjectedEthereum, type WalletKind } from "./injected"; import { useWallet } from "./WalletProvider"; import { useWalletConnectLogin } from "./useWalletConnectLogin"; import { WalletBrandIcon } from "./WalletBrandIcon"; const pollIntervalMs = 1800; type ModalState = "idle" | "signing" | "tpLoading" | "tpPolling"; const appWallets: { kind: WalletKind; labelKey: string }[] = [ { kind: "tokenPocket", labelKey: "walletOpenTokenPocket" }, { kind: "metaMask", labelKey: "walletOpenMetaMask" }, { kind: "imToken", labelKey: "walletOpenImToken" }, ]; function isMobileDevice(): boolean { if (typeof navigator === "undefined") return false; const ua = navigator.userAgent || ""; if ( /Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini|Mobile/i.test( ua, ) ) { return true; } // iPadOS 13+ reports a desktop "Macintosh" UA. A genuine touch-primary iPad // exposes a coarse pointer; a Mac (even with a touch peripheral) keeps a fine // pointer, so it stays on the desktop flow instead of the wallet-app jump. const coarsePointer = typeof window !== "undefined" && typeof window.matchMedia === "function" && window.matchMedia("(pointer: coarse)").matches; return /Macintosh/i.test(ua) && navigator.maxTouchPoints > 1 && coarsePointer; } export function WalletLoginModal() { const { t } = useI18n(); const { closeLoginModal, completeLogin, loginModalOpen, signInInjected } = useWallet(); const wc = useWalletConnectLogin(); const [state, setState] = useState("idle"); const [error, setError] = useState(""); const [mobileDevice, setMobileDevice] = useState(false); const [hasInjected, setHasInjected] = useState(false); const [showOther, setShowOther] = useState(false); const [openingWallet, setOpeningWallet] = useState(null); const [tpRequest, setTpRequest] = useState( null, ); useEffect(() => { if (!loginModalOpen) return; setMobileDevice(isMobileDevice()); setHasInjected(Boolean(getInjectedEthereum())); setShowOther(false); setOpeningWallet(null); setTpRequest(null); setState("idle"); setError(""); wc.reset(); }, [loginModalOpen, wc]); useEffect(() => { if (!loginModalOpen || !tpRequest || state !== "tpPolling") return; let cancelled = false; const abortController = new AbortController(); const poll = async () => { try { const result = await fetchTokenPocketLoginResult( tpRequest.actionId, abortController.signal, ); if (cancelled) return; if (result.status === "completed") { const verified = await verifyWalletSignature({ address: result.address, message: result.message, signature: result.signature, }); if (cancelled) return; completeLogin(verified.token, verified.wallet); setState("idle"); setTpRequest(null); return; } if (result.status === "expired" || result.status === "failed") { setState("idle"); setTpRequest(null); setError(result.error || t("walletTpExpired")); } } catch (err) { if ( !cancelled && !(err instanceof DOMException && err.name === "AbortError") ) { setError(t("walletLoginFailed")); } } }; void poll(); const timer = window.setInterval(() => void poll(), pollIntervalMs); return () => { cancelled = true; abortController.abort(); window.clearInterval(timer); }; }, [completeLogin, loginModalOpen, state, t, tpRequest]); const busy = state === "signing" || state === "tpLoading"; const close = () => { if (busy) return; closeLoginModal(); setError(""); }; if (!loginModalOpen) return null; const withWallet = (key: string, kind: WalletKind) => t(key).replace("{wallet}", t(walletNameKey(kind))); const signInjected = async () => { setError(""); if (!getInjectedEthereum()) { setError(t("walletNoBrowserWalletDesc")); return; } setState("signing"); await signInInjected() .catch((err) => { setError(err instanceof Error ? err.message : t("walletLoginFailed")); }) .finally(() => setState("idle")); }; const openApp = (kind: WalletKind) => { setError(""); setOpeningWallet(kind); openWalletDeepLink(kind); }; // TokenPocket login. The backend returns a `tpoutside://pull.activity` deep // link (a one-off SIGN request, not a dApp-browser link). On mobile we open // it directly so TokenPocket only asks for a signature and then returns to // THIS browser — the poll below finishes login here, no in-app browser. On // desktop we render it as a QR to scan from a phone. const startTokenPocketLogin = async () => { setError(""); setState("tpLoading"); try { const req = await createTokenPocketLoginRequest(); setTpRequest(req); setState("tpPolling"); if (mobileDevice) window.location.href = req.qrUrl; } catch { setState("idle"); setError(t("walletTpQrFailed")); } }; return (

{t("walletLoginTitle")}

{t("walletLoginDesc")}

{/* Browser wallet: sign directly with the injected provider — the reliable path for a BNB-chain extension (desktop) or a wallet's in-app browser. No WalletConnect relay involved. */} {!mobileDevice || hasInjected ? ( ) : null} {/* TokenPocket login — universal path that returns to this browser. */}

{t("walletTokenPocketLogin")}

{mobileDevice ? t("walletTpMobileDesc") : t("walletTokenPocketQrDesc")}

{!tpRequest ? ( ) : mobileDevice ? (

{t("walletTpWaiting")}

) : (

{t("walletQrUseAnotherDevice")}

)}
{/* Other methods: open a wallet app (mobile) and WalletConnect QR. */}
{showOther ? (
{mobileDevice && !hasInjected ? (

{t("walletOpenWalletApp")}

{appWallets.map((option) => ( ))} {openingWallet ? (

{withWallet("walletOpening", openingWallet)}

{t("walletAppNotInstalled")}

{withWallet("walletDownloadApp", openingWallet)}
) : null}
) : null} {/* MetaMask / imToken QR via WalletConnect — needs a real id. */}

{t("walletRainbowFallback")}

{t("walletRainbowFallbackDesc")}

{wc.available ? ( <>

{t("walletNetworkWarning")}

) : (

{t("walletRainbowUnavailable")}

)}
) : null}
{error || wc.error ? (

{error || wc.error}

) : null}
); } function walletNameKey(kind: WalletKind): string { if (kind === "tokenPocket") return "walletTokenPocket"; if (kind === "metaMask") return "walletMetaMask"; return "walletImToken"; }