import { useCallback, useEffect, useRef, useState } from "react"; import { useAccount, useConnect, useDisconnect } from "wagmi"; import { bsc } from "wagmi/chains"; import { hasWalletConnectProjectId } from "./RainbowWalletProvider"; import { connectInjectedWallet, getInjectedWallet, type WalletKind, } from "./injected"; import { 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 isWalletConnectUri(uri: string): boolean { return uri.startsWith("wc:"); } function metaMaskWalletConnectLink(uri: string): string { if (!isWalletConnectUri(uri)) return uri; return `https://metamask.app.link/wc?uri=${encodeURIComponent(uri)}`; } function walletConnectDeeplink( kind: WalletKind | undefined, uri: string, ): string | null { if (kind === "tokenPocket") { return isWalletConnectUri(uri) ? `tpoutside://wc?uri=${encodeURIComponent(uri)}` : uri; } if (kind === "metaMask") { return metaMaskWalletConnectLink(uri); } if (kind === "imToken") { return isWalletConnectUri(uri) ? `imtokenv2://wc?uri=${encodeURIComponent(uri)}` : 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); } function connectorMatchesWallet( connector: { id: string; name?: string; type?: string }, kind: WalletKind | undefined, ): boolean { if (!kind) return false; const id = connector.id.toLowerCase(); const name = connector.name?.toLowerCase() ?? ""; const type = connector.type?.toLowerCase() ?? ""; if (kind === "metaMask") { return id === "metamask" || type === "metamask" || name === "metamask"; } if (kind === "tokenPocket") { return id === "tokenpocket" || name === "tokenpocket"; } if (kind === "imToken") { return id === "imtoken" || name === "imtoken"; } return false; } export function useWalletConnectLogin() { const available = hasWalletConnectProjectId(); const { address: localAddress, loginAddress } = useWallet(); const { address: wagmiAddress, isConnected: wagmiConnected } = useAccount(); const { connectAsync, connectors } = useConnect(); const { disconnectAsync } = useDisconnect(); const [state, setState] = useState("idle"); const [error, setError] = useState(""); const [qrUri, setQrUri] = useState(""); const [connectedAddress, setConnectedAddress] = useState(""); const pendingRef = useRef(false); const completedAddressRef = useRef(null); const cleanupMessageRef = useRef<(() => void) | null>(null); const cleanupPollingRef = useRef<(() => void) | null>(null); const reset = useCallback(() => { pendingRef.current = false; completedAddressRef.current = null; cleanupMessageRef.current?.(); cleanupMessageRef.current = null; cleanupPollingRef.current?.(); cleanupPollingRef.current = null; setState("idle"); setError(""); setQrUri(""); setConnectedAddress(""); }, []); useEffect(() => { if (!wagmiConnected || !wagmiAddress) return; const alreadyCompleted = completedAddressRef.current?.toLowerCase() === wagmiAddress.toLowerCase(); if (alreadyCompleted) return; completedAddressRef.current = wagmiAddress; pendingRef.current = false; setConnectedAddress(wagmiAddress); setQrUri(""); setState("idle"); if (localAddress?.toLowerCase() !== wagmiAddress.toLowerCase()) { console.info("[wallet-login] wagmi account connected", { address: wagmiAddress, chain: "BNB Chain", chainId: bsc.id, }); void loginAddress(wagmiAddress) .then(() => { console.info("[wallet-login] wallet session completed", { address: wagmiAddress, }); }) .catch((err: unknown) => { setError(err instanceof Error ? err.message : "Wallet login failed"); }); } }, [localAddress, loginAddress, wagmiAddress, wagmiConnected]); const start = useCallback( async ( preferredWallet?: WalletKind, mode: WalletConnectLoginMode = "qr", ) => { if (!available) return; setError(""); setQrUri(""); setConnectedAddress(""); completedAddressRef.current = null; pendingRef.current = true; setState("connecting"); if ( mode === "deeplink" && preferredWallet && getInjectedWallet(preferredWallet) ) { try { const injectedAddress = await connectInjectedWallet(preferredWallet); console.info("[wallet-login] injected connected", { preferredWallet, address: injectedAddress, chain: "BNB Chain", chainId: bsc.id, }); await loginAddress(injectedAddress); setState("idle"); return; } catch (err) { console.info("[wallet-login] injected connect fallback to wc", { preferredWallet, message: err instanceof Error ? err.message : String(err), }); } } const walletConnectConnector = connectors.find((item) => item.type === "walletConnect") ?? connectors.find((item) => item.id === "walletConnect"); const walletSpecificConnector = connectors.find((item) => connectorMatchesWallet(item, preferredWallet), ); const connector = mode === "qr" ? walletConnectConnector : (walletSpecificConnector ?? walletConnectConnector); 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); cleanupPollingRef.current?.(); const finishFromAddress = (address: string, source: string) => { const alreadyCompleted = completedAddressRef.current?.toLowerCase() === address.toLowerCase(); if (alreadyCompleted) return; pendingRef.current = false; completedAddressRef.current = address; setConnectedAddress(address); setQrUri(""); setState("idle"); cleanupMessageRef.current?.(); cleanupMessageRef.current = null; cleanupPollingRef.current?.(); cleanupPollingRef.current = null; console.info("[wallet-login] wallet account connected", { source, preferredWallet, address, chain: "BNB Chain", chainId: bsc.id, }); void loginAddress(address) .then(() => { console.info("[wallet-login] wallet session completed", { address, }); }) .catch((err: unknown) => { setError( err instanceof Error ? err.message : "Wallet login failed", ); }); }; const pollId = window.setInterval(() => { void connector .getAccounts() .then((accounts) => { const account = accounts[0]; if (account) finishFromAddress(account, "connector-poll"); }) .catch(() => undefined); }, 1000); cleanupPollingRef.current = () => window.clearInterval(pollId); 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"); finishFromAddress(connectedAddress, "connectAsync"); } catch (err) { if (completedAddressRef.current) return; pendingRef.current = false; setState("idle"); setError( err instanceof Error ? err.message : "WalletConnect login failed", ); cleanupMessageRef.current?.(); cleanupMessageRef.current = null; cleanupPollingRef.current?.(); cleanupPollingRef.current = null; } }, [available, connectAsync, connectors, disconnectAsync, loginAddress], ); return { available, state, error, qrUri, address: connectedAddress, isConnected: Boolean(connectedAddress), start, reset, }; }