import { useCallback, useEffect, useRef, useState } from "react"; import { useAccount, useConnect, useDisconnect, useSignMessage } from "wagmi"; import { bsc } from "wagmi/chains"; import { requestWalletNonce, verifyWalletSignature } from "./api"; import { hasWalletConnectProjectId } from "./RainbowWalletProvider"; import type { WalletKind } from "./injected"; import { useWallet } from "./WalletProvider"; export type WalletConnectLoginState = "idle" | "connecting" | "signing"; function isMobileDevice(): boolean { if (typeof navigator === "undefined") return false; return /Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini|Mobile/i.test( navigator.userAgent || "", ); } /** * MetaMask / imToken QR fallback via RainbowKit + WalletConnect. * * Flow: open the RainbowKit connect modal (WalletConnect QR) -> once an account * is connected, request a nonce, sign it with `personal_sign` through wagmi, * verify against the backend and complete our own JWT login. The wagmi/WC * session is only needed for the signature, so we disconnect right after. * * 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 { address, isConnected } = useAccount(); const { signMessageAsync } = useSignMessage(); const { connectAsync, connectors } = useConnect(); const { disconnect } = useDisconnect(); const [state, setState] = useState("idle"); const [error, setError] = useState(""); const [qrUri, setQrUri] = 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(""); }, []); const start = useCallback( async (preferredWallet?: WalletKind) => { if (!available) return; setError(""); setQrUri(""); pendingRef.current = true; setState("connecting"); const connector = connectors.find((item) => item.id === preferredWallet) ?? connectors.find((item) => item.id === "walletConnect") ?? connectors.find((item) => item.type === "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, }); setQrUri(message.data); if (preferredWallet === "tokenPocket" && isMobileDevice()) { window.location.href = `tpoutside://wc?uri=${encodeURIComponent( message.data, )}`; } }; cleanupMessageRef.current?.(); connector.emitter.on("message", onMessage); cleanupMessageRef.current = () => connector.emitter.off("message", onMessage); try { await connector.disconnect().catch(() => undefined); await connectAsync({ chainId: bsc.id, connector }); } catch (err) { pendingRef.current = false; setState("idle"); setError( err instanceof Error ? err.message : "WalletConnect login failed", ); cleanupMessageRef.current?.(); cleanupMessageRef.current = null; } }, [available, connectAsync, connectors], ); useEffect(() => { if (!pendingRef.current || !isConnected || !address) return; pendingRef.current = false; setState("signing"); let cancelled = false; void (async () => { try { const nonce = await requestWalletNonce(address); const signature = await signMessageAsync({ message: nonce.message }); const verified = await verifyWalletSignature({ address, message: nonce.message, signature, }); if (cancelled) return; completeLogin(verified.token, verified.wallet); setState("idle"); } catch (err) { if (!cancelled) { setError( err instanceof Error ? err.message : "WalletConnect login failed", ); setQrUri(""); setState("idle"); } } finally { // We only needed a one-off signature, not a persistent wagmi session. disconnect(); } })(); return () => { cancelled = true; }; }, [address, completeLogin, disconnect, isConnected, signMessageAsync]); return { available, state, error, qrUri, start, reset }; }