import { useCallback, useRef, useState } from "react"; import { useConnect, useDisconnect } from "wagmi"; import { bsc } from "wagmi/chains"; import { hasWalletConnectProjectId } from "./RainbowWalletProvider"; import { connectInjectedWallet, getInjectedWallet, type WalletKind, } from "./injected"; import { localWalletToken, useWallet } from "./WalletProvider"; export type WalletConnectLoginState = "idle" | "connecting" | "signing"; export type WalletConnectLoginMode = "deeplink" | "qr"; function isMobileDevice(): boolean { if (typeof navigator === "undefined") return false; return /Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini|Mobile/i.test( navigator.userAgent || "", ); } function currentUrl(): string { if (typeof window === "undefined") return "https://ark-library.com"; return window.location.href; } function walletConnectDeeplink( kind: WalletKind | undefined, uri: string, ): string | null { if (kind === "tokenPocket") { return `tpoutside://wc?uri=${encodeURIComponent(uri)}`; } if (kind === "metaMask") { return `https://metamask.app.link/wc?uri=${encodeURIComponent(uri)}`; } if (kind === "imToken") { return `imtokenv2://wc?uri=${encodeURIComponent(uri)}`; } return null; } function inAppBrowserFallback(kind: WalletKind | undefined): string | null { if (kind === "imToken") { return `imtokenv2://navigate/DappView?url=${encodeURIComponent(currentUrl())}`; } return null; } function openWalletDeeplink( kind: WalletKind | undefined, deeplink: string, ): void { window.location.href = deeplink; const fallback = inAppBrowserFallback(kind); if (!fallback) return; window.setTimeout(() => { if (document.visibilityState === "visible") { window.location.href = fallback; } }, 1500); } /** * MetaMask / imToken QR fallback via RainbowKit + WalletConnect. * * Flow: connect through RainbowKit/Wagmi on BNB Chain -> once an account is * connected, complete a local frontend wallet session. No message signature, * backend nonce, or verify call is required. * * Entirely gated behind a real `VITE_WALLETCONNECT_PROJECT_ID`: when it is * missing `available` is false and `start` is a no-op, so callers can hide or * disable the entry instead of triggering a connect with a fake project id. */ export function useWalletConnectLogin() { const available = hasWalletConnectProjectId(); const { completeLogin } = useWallet(); const { connectAsync, connectors } = useConnect(); const { disconnect, disconnectAsync } = useDisconnect(); const [state, setState] = useState("idle"); const [error, setError] = useState(""); const [qrUri, setQrUri] = useState(""); const [connectedAddress, setConnectedAddress] = useState(""); const pendingRef = useRef(false); const cleanupMessageRef = useRef<(() => void) | null>(null); const reset = useCallback(() => { pendingRef.current = false; cleanupMessageRef.current?.(); cleanupMessageRef.current = null; setState("idle"); setError(""); setQrUri(""); setConnectedAddress(""); }, []); const start = useCallback( async ( preferredWallet?: WalletKind, mode: WalletConnectLoginMode = "qr", ) => { if (!available) return; setError(""); setQrUri(""); setConnectedAddress(""); pendingRef.current = true; setState("connecting"); if (preferredWallet && getInjectedWallet(preferredWallet)) { try { const injectedAddress = await connectInjectedWallet(preferredWallet); console.info("[wallet-login] injected connected", { preferredWallet, address: injectedAddress, chain: "BNB Chain", chainId: bsc.id, }); completeLogin(localWalletToken(injectedAddress), injectedAddress); setState("idle"); return; } catch (err) { console.info("[wallet-login] injected connect fallback to wc", { preferredWallet, message: err instanceof Error ? err.message : String(err), }); } } // This modal is QR/WalletConnect-only. RainbowKit also exposes wallet- // specific injected connectors (for example `tokenPocket`) when an const connector = connectors.find((item) => item.type === "walletConnect") ?? connectors.find((item) => item.id === "walletConnect"); if (!connector) { pendingRef.current = false; setQrUri(""); setState("idle"); setError("WalletConnect is not available"); return; } console.info("[wallet-login] walletconnect connector", { preferredWallet, connectorId: connector.id, connectorName: connector.name, connectorType: connector.type, }); const onMessage = (message: { type: string; data?: unknown }) => { if ( message.type !== "display_uri" || typeof message.data !== "string" ) { return; } console.info("[wallet-login] walletconnect display_uri", { preferredWallet, connectorId: connector.id, }); if (mode === "qr") setQrUri(message.data); const deeplink = walletConnectDeeplink(preferredWallet, message.data); if (mode === "deeplink" && deeplink && isMobileDevice()) { openWalletDeeplink(preferredWallet, deeplink); } }; cleanupMessageRef.current?.(); connector.emitter.on("message", onMessage); cleanupMessageRef.current = () => connector.emitter.off("message", onMessage); try { await disconnectAsync().catch(() => undefined); await connector.disconnect().catch(() => undefined); const result = await connectAsync({ chainId: bsc.id, connector }); const connectedAddress = result.accounts[0]; if (!connectedAddress) throw new Error("Wallet connected without an account"); pendingRef.current = false; setConnectedAddress(connectedAddress); console.info("[wallet-login] walletconnect connected", { address: connectedAddress, chain: "BNB Chain", chainId: bsc.id, }); window.alert(`扫码成功,已拿到钱包地址:\n${connectedAddress}`); completeLogin(localWalletToken(connectedAddress), connectedAddress); console.info("[wallet-login] local wallet session completed", { address: connectedAddress, }); setQrUri(""); setState("idle"); disconnect(); } catch (err) { pendingRef.current = false; setState("idle"); setError( err instanceof Error ? err.message : "WalletConnect login failed", ); cleanupMessageRef.current?.(); cleanupMessageRef.current = null; } }, [ available, completeLogin, connectAsync, connectors, disconnect, disconnectAsync, ], ); return { available, state, error, qrUri, address: connectedAddress, isConnected: Boolean(connectedAddress), start, reset, }; }