import { useCallback, useEffect, useRef, useState } from "react"; import { useAccount, useConnect, useDisconnect } from "wagmi"; import { bsc } from "wagmi/chains"; import { hasWalletConnectProjectId } from "./RainbowWalletProvider"; import 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 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; } /** * 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 { address, isConnected } = useAccount(); 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, mode: WalletConnectLoginMode = "qr", ) => { if (!available) return; setError(""); setQrUri(""); pendingRef.current = true; setState("connecting"); // This modal is QR/WalletConnect-only. RainbowKit also exposes wallet- // specific injected connectors (for example `tokenPocket`) when an // extension is installed; using those here makes the click try the local // browser extension and can fail with "wallet must has at least one // account" before a QR is shown. 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()) { window.location.href = deeplink; } }; 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; completeLogin(localWalletToken(address), address); setQrUri(""); setState("idle"); disconnect(); }, [address, completeLogin, disconnect, isConnected]); return { available, state, error, qrUri, start, reset }; }