// NAC XIC Presale — Wallet Selector Component // Detects installed EVM wallets and shows connect/install buttons for each // v5: improved error handling, MetaMask permission reset guide, connection diagnostics import { useState, useEffect, useCallback } from "react"; import type { EthProvider } from "@/hooks/useWallet"; type Lang = "zh" | "en"; interface WalletInfo { id: string; name: string; icon: React.ReactNode; installUrl: string; mobileDeepLink?: string; isInstalled: () => boolean; connect: () => Promise<{ address: string; rawProvider: EthProvider } | null>; network: "evm" | "tron"; } interface WalletSelectorProps { lang: Lang; onAddressDetected: (address: string, network?: "evm" | "tron", rawProvider?: EthProvider) => void; connectedAddress?: string; compact?: boolean; showTron?: boolean; } // ── Wallet Icons ────────────────────────────────────────────────────────────── const MetaMaskIcon = () => ( ); const TrustWalletIcon = () => ( ); const OKXIcon = () => ( ); const CoinbaseIcon = () => ( ); const TokenPocketIcon = () => ( ); const BitgetIcon = () => ( ); const TronLinkIcon = () => ( ); // ── Mobile detection ────────────────────────────────────────────────────────── function isMobileBrowser(): boolean { if (typeof window === "undefined") return false; return /Android|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(navigator.userAgent); } function isInWalletBrowser(): boolean { if (typeof window === "undefined") return false; const ua = navigator.userAgent.toLowerCase(); const w = window as unknown as Record; const eth = w.ethereum as { isMetaMask?: boolean; isTrust?: boolean; isTrustWallet?: boolean; isOKExWallet?: boolean; isOkxWallet?: boolean } | undefined; const tronLink = w.tronLink as { ready?: boolean } | undefined; return !!( eth?.isMetaMask || eth?.isTrust || eth?.isTrustWallet || eth?.isOKExWallet || eth?.isOkxWallet || tronLink?.ready || ua.includes("metamask") || ua.includes("trust") || ua.includes("okex") || ua.includes("tokenpocket") || ua.includes("bitkeep") || ua.includes("tronlink") ); } // ── Provider detection helpers ──────────────────────────────────────────────── type TronLinkProvider = { ready: boolean; tronWeb?: { defaultAddress?: { base58?: string }; trx?: unknown; }; request: (args: { method: string; params?: unknown[] }) => Promise; }; function getEth(): EthProvider | null { if (typeof window === "undefined") return null; return (window as unknown as { ethereum?: EthProvider }).ethereum ?? null; } function getOKX(): EthProvider | null { if (typeof window === "undefined") return null; return (window as unknown as { okxwallet?: EthProvider }).okxwallet ?? null; } function getBitget(): EthProvider | null { if (typeof window === "undefined") return null; const w = window as unknown as { bitkeep?: { ethereum?: EthProvider } }; return w.bitkeep?.ethereum ?? null; } function getTronLink(): TronLinkProvider | null { if (typeof window === "undefined") return null; const w = window as unknown as { tronLink?: TronLinkProvider; tronWeb?: TronLinkProvider["tronWeb"] }; if (w.tronLink?.ready) return w.tronLink; if (w.tronWeb) { return { ready: true, tronWeb: w.tronWeb, request: async () => null, }; } return null; } function findProvider(predicate: (p: EthProvider) => boolean): EthProvider | null { const eth = getEth(); if (!eth) return null; if (eth.providers && Array.isArray(eth.providers)) { return eth.providers.find(predicate) ?? null; } return predicate(eth) ? eth : null; } // ── Improved requestAccounts with better error classification ───────────────── async function requestAccounts(provider: EthProvider): Promise<{ address: string; rawProvider: EthProvider } | null> { try { // First try eth_accounts (silent) to check if already connected const existingAccounts = await provider.request({ method: "eth_accounts" }) as string[]; if (existingAccounts && existingAccounts.length > 0) { return { address: existingAccounts[0], rawProvider: provider }; } } catch { // Ignore - proceed to eth_requestAccounts } try { const accounts = await provider.request({ method: "eth_requestAccounts" }) as string[]; const address = accounts?.[0] ?? null; if (!address) return null; return { address, rawProvider: provider }; } catch (err: unknown) { const error = err as { code?: number; message?: string }; console.log("[WalletSelector] requestAccounts error:", error.code, error.message); if (error?.code === 4001) throw new Error("user_rejected"); if (error?.code === -32002) throw new Error("wallet_pending"); // Some wallets throw with message instead of code if (error?.message?.toLowerCase().includes("user rejected") || error?.message?.toLowerCase().includes("user denied") || error?.message?.toLowerCase().includes("cancelled")) { throw new Error("user_rejected"); } throw err; } } async function requestTronAccounts(provider: TronLinkProvider): Promise<{ address: string; rawProvider: EthProvider } | null> { try { if (provider.tronWeb?.defaultAddress?.base58) { return { address: provider.tronWeb.defaultAddress.base58, rawProvider: provider as unknown as EthProvider }; } const result = await provider.request({ method: "tron_requestAccounts" }) as { code?: number; message?: string }; if (result?.code === 200) { const w = window as unknown as { tronWeb?: { defaultAddress?: { base58?: string } } }; const address = w.tronWeb?.defaultAddress?.base58 ?? null; if (!address) return null; return { address, rawProvider: provider as unknown as EthProvider }; } if (result?.code === 4001) throw new Error("user_rejected"); return null; } catch (err: unknown) { const error = err as { code?: number; message?: string }; if (error?.code === 4001 || error?.message === "user_rejected") throw new Error("user_rejected"); throw err; } } // ── Wallet definitions ──────────────────────────────────────────────────────── function buildWallets(showTron: boolean): WalletInfo[] { const currentUrl = typeof window !== "undefined" ? window.location.href : "https://pre-sale.newassetchain.io"; const urlWithoutProtocol = currentUrl.replace(/^https?:\/\//, ""); const evmWallets: WalletInfo[] = [ { id: "metamask", name: "MetaMask", icon: , installUrl: "https://metamask.io/download/", mobileDeepLink: `https://metamask.app.link/dapp/${urlWithoutProtocol}`, isInstalled: () => !!findProvider(p => !!p.isMetaMask), connect: async () => { const p = findProvider(p => !!p.isMetaMask) ?? getEth(); return p ? requestAccounts(p) : null; }, network: "evm", }, { id: "trust", name: "Trust Wallet", icon: , installUrl: "https://trustwallet.com/download", mobileDeepLink: `https://link.trustwallet.com/open_url?coin_id=60&url=${encodeURIComponent(currentUrl)}`, isInstalled: () => !!findProvider(p => !!(p.isTrust || p.isTrustWallet)), connect: async () => { const p = findProvider(p => !!(p.isTrust || p.isTrustWallet)) ?? getEth(); return p ? requestAccounts(p) : null; }, network: "evm", }, { id: "okx", name: "OKX Wallet", icon: , installUrl: "https://www.okx.com/web3", mobileDeepLink: `okx://wallet/dapp/url?dappUrl=${encodeURIComponent(currentUrl)}`, isInstalled: () => !!(getOKX() || findProvider(p => !!(p.isOKExWallet || p.isOkxWallet))), connect: async () => { const p = getOKX() ?? findProvider(p => !!(p.isOKExWallet || p.isOkxWallet)); return p ? requestAccounts(p) : null; }, network: "evm", }, { id: "coinbase", name: "Coinbase Wallet", icon: , installUrl: "https://www.coinbase.com/wallet/downloads", isInstalled: () => !!findProvider(p => !!p.isCoinbaseWallet), connect: async () => { const p = findProvider(p => !!p.isCoinbaseWallet) ?? getEth(); return p ? requestAccounts(p) : null; }, network: "evm", }, { id: "tokenpocket", name: "TokenPocket", icon: , installUrl: "https://www.tokenpocket.pro/en/download/app", isInstalled: () => !!findProvider(p => !!p.isTokenPocket), connect: async () => { const p = findProvider(p => !!p.isTokenPocket) ?? getEth(); return p ? requestAccounts(p) : null; }, network: "evm", }, { id: "bitget", name: "Bitget Wallet", icon: , installUrl: "https://web3.bitget.com/en/wallet-download", isInstalled: () => !!(getBitget() || findProvider(p => !!(p.isBitkeep || p.isBitgetWallet))), connect: async () => { const p = getBitget() ?? findProvider(p => !!(p.isBitkeep || p.isBitgetWallet)); return p ? requestAccounts(p) : null; }, network: "evm", }, ]; const tronWallets: WalletInfo[] = [ { id: "tronlink", name: "TronLink", icon: , installUrl: "https://www.tronlink.org/", mobileDeepLink: `tronlinkoutside://pull.activity?param=${encodeURIComponent(JSON.stringify({ url: currentUrl, action: "open", protocol: "tronlink", version: "1.0" }))}`, isInstalled: () => !!getTronLink(), connect: async () => { const tron = getTronLink(); if (!tron) return null; return requestTronAccounts(tron); }, network: "tron", }, ]; return showTron ? [...tronWallets, ...evmWallets] : evmWallets; } // Validate Ethereum address format function isValidEthAddress(addr: string): boolean { return /^0x[0-9a-fA-F]{40}$/.test(addr); } // Validate TRON address format (T + 33 base58 chars = 34 chars total) function isValidTronAddress(addr: string): boolean { return /^T[1-9A-HJ-NP-Za-km-z]{33}$/.test(addr); } // ── Mobile DeepLink Panel ───────────────────────────────────────────────────── function MobileDeepLinkPanel({ lang, showTron }: { lang: Lang; showTron: boolean }) { const currentUrl = typeof window !== "undefined" ? window.location.href : "https://pre-sale.newassetchain.io"; const urlWithoutProtocol = currentUrl.replace(/^https?:\/\//, ""); const [showMore, setShowMore] = useState(false); const metamaskDeepLink = `https://metamask.app.link/dapp/${urlWithoutProtocol}`; const otherWallets = [ ...( showTron ? [{ id: "tronlink", name: "TronLink", icon: , deepLink: `tronlinkoutside://pull.activity?param=${encodeURIComponent(JSON.stringify({ url: currentUrl, action: "open", protocol: "tronlink", version: "1.0" }))}`, badge: "TRON", badgeColor: "#FF0013", }] : []), { id: "trust", name: "Trust Wallet", icon: , deepLink: `https://link.trustwallet.com/open_url?coin_id=60&url=${encodeURIComponent(currentUrl)}`, badge: "EVM", badgeColor: "#3375BB", }, { id: "okx", name: "OKX Wallet", icon: , deepLink: `okx://wallet/dapp/url?dappUrl=${encodeURIComponent(currentUrl)}`, badge: "EVM", badgeColor: "#00F0FF", }, { id: "tokenpocket", name: "TokenPocket", icon: , deepLink: `tpdapp://open?params=${encodeURIComponent(JSON.stringify({ url: currentUrl, chain: "ETH", source: "NAC-Presale" }))}`, badge: "EVM/TRON", badgeColor: "#2980FE", }, ]; return (

MetaMask

{lang === "zh" ? "在 MetaMask 内置浏览器中打开" : "Open in MetaMask browser"}

{showMore && (
{otherWallets.map(wallet => ( {wallet.icon} {wallet.name} {wallet.badge} ))}
)}
); } // ── Error Help Panel ────────────────────────────────────────────────────────── // Shows specific troubleshooting steps based on error type function ErrorHelpPanel({ errorType, walletName, lang, onRetry }: { errorType: "user_rejected" | "wallet_pending" | "not_initialized" | "unknown"; walletName: string; lang: Lang; onRetry: () => void; }) { const isZh = lang === "zh"; if (errorType === "user_rejected") { return (

{isZh ? "连接被拒绝" : "Connection Rejected"}

{isZh ? `${walletName} 拒绝了连接请求。可能原因:` : `${walletName} rejected the connection. Possible reasons:`}

{isZh ? "1. 您在钱包弹窗中点击了「拒绝」" : "1. You clicked \"Reject\" in the wallet popup"}

{isZh ? "2. 该网站之前被您在 MetaMask 中屏蔽(最常见原因)" : "2. This site was previously blocked in your wallet (most common)"}

{isZh ? "3. 钱包弹窗未显示(被浏览器拦截)" : "3. Wallet popup was blocked by browser"}

{walletName === "MetaMask" && (

{isZh ? "🔧 MetaMask 权限重置步骤:" : "🔧 MetaMask Permission Reset:"}

{isZh ? "① 点击 MetaMask 图标打开扩展" : "① Click MetaMask icon to open extension"}

{isZh ? "② 点击右上角菜单(三个点)" : "② Click top-right menu (three dots)"}

{isZh ? "③ 选择「已连接的网站」" : "③ Select \"Connected Sites\""}

{isZh ? "④ 找到本网站并删除" : "④ Find this site and remove it"}

{isZh ? "⑤ 回到此页面重新点击连接" : "⑤ Return here and try connecting again"}

)}
); } if (errorType === "wallet_pending") { return (

{isZh ? "钱包有待处理的请求" : "Wallet Has Pending Request"}

{isZh ? "请查看钱包弹窗并处理待处理的请求,然后重试" : "Please check your wallet popup and handle the pending request, then retry"}

); } if (errorType === "not_initialized") { return (
⚙️

{isZh ? "钱包未完成初始化" : "Wallet Not Initialized"}

{isZh ? "请先完成钱包设置(创建或导入钱包),然后刷新页面重试" : "Please complete wallet setup (create or import wallet), then refresh the page"}

); } // Unknown error return (

{isZh ? "连接失败" : "Connection Failed"}

{isZh ? "请刷新页面后重试,或尝试其他钱包" : "Please refresh the page and try again, or try another wallet"}

); } // ── WalletSelector Component ────────────────────────────────────────────────── export function WalletSelector({ lang, onAddressDetected, connectedAddress, compact = false, showTron = false }: WalletSelectorProps) { const [wallets, setWallets] = useState([]); const [connecting, setConnecting] = useState(null); const [errorType, setErrorType] = useState<"user_rejected" | "wallet_pending" | "not_initialized" | "unknown" | null>(null); const [errorWalletName, setErrorWalletName] = useState(""); const [detecting, setDetecting] = useState(true); const [showManual, setShowManual] = useState(false); const [manualAddress, setManualAddress] = useState(""); const [manualError, setManualError] = useState(null); const [isMobile] = useState(() => isMobileBrowser()); const [inWalletBrowser] = useState(() => isInWalletBrowser()); const [lastConnectedWallet, setLastConnectedWallet] = useState(null); const detectWallets = useCallback(() => { setDetecting(true); setErrorType(null); // Wait for wallet extensions to fully inject (up to 1500ms) const timer = setTimeout(() => { setWallets(buildWallets(showTron)); setDetecting(false); }, 1500); return () => clearTimeout(timer); }, [showTron]); useEffect(() => { const cleanup = detectWallets(); return cleanup; }, [detectWallets]); const handleConnect = async (wallet: WalletInfo) => { setConnecting(wallet.id); setErrorType(null); setLastConnectedWallet(wallet); try { const result = await wallet.connect(); if (result) { onAddressDetected(result.address, wallet.network, result.rawProvider); } else { setErrorType("unknown"); setErrorWalletName(wallet.name); } } catch (err: unknown) { const error = err as Error; setErrorWalletName(wallet.name); if (error.message === "user_rejected") { setErrorType("user_rejected"); } else if (error.message === "wallet_pending") { setErrorType("wallet_pending"); } else if (error.message?.includes("not initialized") || error.message?.includes("setup")) { setErrorType("not_initialized"); } else { setErrorType("unknown"); } } finally { setConnecting(null); } }; const handleRetry = useCallback(() => { setErrorType(null); if (lastConnectedWallet) { handleConnect(lastConnectedWallet); } }, [lastConnectedWallet]); // eslint-disable-line react-hooks/exhaustive-deps const handleManualSubmit = () => { const addr = manualAddress.trim(); if (!addr) { setManualError(lang === "zh" ? "请输入钱包地址" : "Please enter wallet address"); return; } if (isValidEthAddress(addr)) { setManualError(null); onAddressDetected(addr, "evm"); return; } if (isValidTronAddress(addr)) { setManualError(null); onAddressDetected(addr, "tron"); return; } setManualError(lang === "zh" ? "地址格式无效,请输入 EVM 地址(0x开头)或 TRON 地址(T开头)" : "Invalid address. Enter an EVM address (0x...) or TRON address (T...)"); }; const installedWallets = wallets.filter(w => w.isInstalled()); const notInstalledWallets = wallets.filter(w => !w.isInstalled()); // If connected address is already set, show compact confirmation if (connectedAddress) { return (

{lang === "zh" ? "钱包已连接" : "Wallet Connected"}

{connectedAddress}

); } // ── Mobile browser (not in wallet app) — show DeepLink guide ────────────── if (isMobile && !inWalletBrowser && !detecting) { return (
{/* Manual address fallback */}
{showManual && (

{lang === "zh" ? "输入 EVM 地址(0x 开头)或 TRON 地址(T 开头)" : "Enter EVM address (0x...) or TRON address (T...)"}

{ setManualAddress(e.target.value); setManualError(null); }} placeholder={lang === "zh" ? "0x... 或 T..." : "0x... or T..."} className="flex-1 px-3 py-2 rounded-lg text-xs font-mono text-white/80 outline-none focus:ring-1" style={{ background: "rgba(255,255,255,0.06)", border: manualError ? "1px solid rgba(255,80,80,0.5)" : "1px solid rgba(255,255,255,0.12)", }} onKeyDown={e => e.key === "Enter" && handleManualSubmit()} />
{manualError && (

{manualError}

)}
)}
); } // ── Loading state ───────────────────────────────────────────────────────── if (detecting) { return (

{lang === "zh" ? "选择钱包连接" : "Select Wallet to Connect"}

{lang === "zh" ? "正在检测钱包..." : "Detecting wallets..."}
); } return (

{lang === "zh" ? "选择钱包连接" : "Select Wallet to Connect"}

{/* Refresh detection button */}
{/* Connecting overlay */} {connecting && (

{lang === "zh" ? "等待钱包授权..." : "Waiting for wallet authorization..."}

{lang === "zh" ? "请查看钱包弹窗并点击「连接」" : "Please check your wallet popup and click \"Connect\""}

)} {/* Error panel */} {!connecting && errorType && ( )} {/* Installed wallets */} {!connecting && !errorType && installedWallets.length > 0 && (
{installedWallets.map(wallet => ( ))}
)} {/* Show installed wallets even when there's an error (for retry) */} {!connecting && errorType && installedWallets.length > 0 && (

{lang === "zh" ? "或选择其他钱包" : "Or try another wallet"}

{installedWallets.map(wallet => ( ))}
)} {/* No wallets installed — desktop */} {!connecting && installedWallets.length === 0 && (

{lang === "zh" ? (showTron ? "未检测到 EVM 或 TRON 钱包" : "未检测到 EVM 钱包") : (showTron ? "No EVM or TRON wallet detected" : "No EVM wallet detected")}

{lang === "zh" ? "请安装以下任一钱包,完成设置后点击上方「刷新」按钮" : "Install any wallet below, then click Refresh above after setup"}

{!showTron && (

{lang === "zh" ? "💡 已安装MetaMask?请先完成钱包初始化(创建或导入钱包),再点击刷新" : "💡 Have MetaMask? Complete wallet setup (create or import) first, then click Refresh"}

)}
)} {/* Not installed wallets */} {!connecting && notInstalledWallets.length > 0 && (

{lang === "zh" ? "未安装" : "Not installed"}

{notInstalledWallets.map(wallet => ( {wallet.icon} {wallet.name} {lang === "zh" ? "点击安装" : "Install"} ))}
)} {/* Manual address input */}
{showManual && (

{lang === "zh" ? "输入 EVM 地址(0x 开头)或 TRON 地址(T 开头)" : "Enter EVM address (0x...) or TRON address (T...)"}

{ setManualAddress(e.target.value); setManualError(null); }} placeholder={lang === "zh" ? "0x... 或 T..." : "0x... or T..."} className="flex-1 px-3 py-2 rounded-lg text-xs font-mono text-white/80 outline-none focus:ring-1" style={{ background: "rgba(255,255,255,0.06)", border: manualError ? "1px solid rgba(255,80,80,0.5)" : "1px solid rgba(255,255,255,0.12)", }} onKeyDown={e => e.key === "Enter" && handleManualSubmit()} />
{manualError && (

{manualError}

)}
)}
); }