From 158822556fa6bef646ed7e31374b2a73e39767ee Mon Sep 17 00:00:00 2001 From: Manus Date: Mon, 9 Mar 2026 05:43:49 -0400 Subject: [PATCH] =?UTF-8?q?Fix:=20WalletSelector=20v2=20-=20=E6=94=B9?= =?UTF-8?q?=E8=BF=9B=E9=92=B1=E5=8C=85=E6=A3=80=E6=B5=8B=E6=97=B6=E5=BA=8F?= =?UTF-8?q?=E3=80=81=E6=B7=BB=E5=8A=A0=E5=88=B7=E6=96=B0=E6=8C=89=E9=92=AE?= =?UTF-8?q?=E3=80=81=E6=89=8B=E5=8A=A8=E5=9C=B0=E5=9D=80=E8=BE=93=E5=85=A5?= =?UTF-8?q?=E5=9B=9E=E9=80=80=E3=80=81=E9=94=99=E8=AF=AF=E7=A0=81=E7=B2=BE?= =?UTF-8?q?=E5=87=86=E5=A4=84=E7=90=86=EF=BC=88user=5Frejected/wallet=5Fpe?= =?UTF-8?q?nding=EF=BC=89=EF=BC=9B=E4=B8=89=E4=B8=AA=E5=9F=9F=E5=90=8D?= =?UTF-8?q?=EF=BC=88pre-sale/ico/trc-ico.newassetchain.io=EF=BC=89?= =?UTF-8?q?=E5=85=A8=E9=83=A8=E9=83=A8=E7=BD=B2=E5=88=B0AI=E6=9C=8D?= =?UTF-8?q?=E5=8A=A1=E5=99=A843.224.155.27=EF=BC=8CDNS=E8=A7=A3=E6=9E=90?= =?UTF-8?q?=E5=B7=B2=E7=94=9F=E6=95=88?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- client/src/components/WalletSelector.tsx | 184 ++++++++++++++++++++--- 1 file changed, 167 insertions(+), 17 deletions(-) diff --git a/client/src/components/WalletSelector.tsx b/client/src/components/WalletSelector.tsx index 7c7dd70..800662f 100644 --- a/client/src/components/WalletSelector.tsx +++ b/client/src/components/WalletSelector.tsx @@ -1,7 +1,8 @@ -// NAC XIC Presale — WalletSelector Component +// NAC XIC Presale — Wallet Selector Component // Detects installed EVM wallets and shows connect/install buttons for each +// v2: improved detection timing, refresh button, manual address fallback -import { useState, useEffect } from "react"; +import { useState, useEffect, useCallback } from "react"; type Lang = "zh" | "en"; @@ -125,8 +126,24 @@ async function requestAccounts(provider: EthProvider): Promise { try { const accounts = await provider.request({ method: "eth_requestAccounts" }) as string[]; return accounts?.[0] ?? null; + } catch (err: unknown) { + const error = err as { code?: number; message?: string }; + // User rejected + if (error?.code === 4001) throw new Error("user_rejected"); + // MetaMask not initialized / locked + if (error?.code === -32002) throw new Error("wallet_pending"); + throw err; + } +} + +// Check if MetaMask is installed but not yet initialized (no accounts, no unlock) +async function isWalletInitialized(provider: EthProvider): Promise { + try { + const accounts = await provider.request({ method: "eth_accounts" }) as string[]; + // If we can get accounts (even empty array), wallet is initialized + return true; } catch { - return null; + return false; } } @@ -203,21 +220,38 @@ function buildWallets(): WalletInfo[] { ]; } +// Validate Ethereum address format +function isValidEthAddress(addr: string): boolean { + return /^0x[0-9a-fA-F]{40}$/.test(addr); +} + // ── WalletSelector Component ────────────────────────────────────────────────── export function WalletSelector({ lang, onAddressDetected, connectedAddress, compact = false }: WalletSelectorProps) { const [wallets, setWallets] = useState([]); const [connecting, setConnecting] = useState(null); const [error, setError] = useState(null); + const [detecting, setDetecting] = useState(true); + const [showManual, setShowManual] = useState(false); + const [manualAddress, setManualAddress] = useState(""); + const [manualError, setManualError] = useState(null); - useEffect(() => { - // Build wallet list after a short delay to allow extensions to inject + const detectWallets = useCallback(() => { + setDetecting(true); + setError(null); + // Wait for wallet extensions to fully inject (up to 1500ms) const timer = setTimeout(() => { setWallets(buildWallets()); - }, 400); + setDetecting(false); + }, 1500); return () => clearTimeout(timer); }, []); + useEffect(() => { + const cleanup = detectWallets(); + return cleanup; + }, [detectWallets]); + const handleConnect = async (wallet: WalletInfo) => { setConnecting(wallet.id); setError(null); @@ -228,13 +262,38 @@ export function WalletSelector({ lang, onAddressDetected, connectedAddress, comp } else { setError(lang === "zh" ? "未获取到地址,请重试" : "No address returned, please try again"); } - } catch { - setError(lang === "zh" ? "连接失败,请重试" : "Connection failed, please try again"); + } catch (err: unknown) { + const error = err as Error; + if (error.message === "user_rejected") { + setError(lang === "zh" ? "已取消连接" : "Connection cancelled"); + } else if (error.message === "wallet_pending") { + setError(lang === "zh" ? "钱包请求处理中,请检查钱包弹窗" : "Wallet request pending, please check your wallet popup"); + } else if (error.message?.includes("not initialized") || error.message?.includes("setup")) { + setError(lang === "zh" + ? "请先完成钱包初始化设置,然后刷新页面重试" + : "Please complete wallet setup first, then refresh the page"); + } else { + setError(lang === "zh" ? "连接失败,请重试" : "Connection failed, please try again"); + } } finally { setConnecting(null); } }; + const handleManualSubmit = () => { + const addr = manualAddress.trim(); + if (!addr) { + setManualError(lang === "zh" ? "请输入钱包地址" : "Please enter wallet address"); + return; + } + if (!isValidEthAddress(addr)) { + setManualError(lang === "zh" ? "地址格式无效,请输入正确的以太坊地址(0x开头,42位)" : "Invalid address format. Must be 0x followed by 40 hex characters"); + return; + } + setManualError(null); + onAddressDetected(addr); + }; + const installedWallets = wallets.filter(w => w.isInstalled()); const notInstalledWallets = wallets.filter(w => !w.isInstalled()); @@ -258,12 +317,46 @@ export function WalletSelector({ lang, onAddressDetected, connectedAddress, comp return (
-

- {lang === "zh" ? "选择钱包自动填充地址" : "Select wallet to auto-fill address"} -

+
+

+ {lang === "zh" ? "选择钱包自动填充地址" : "Select wallet to auto-fill address"} +

+ {/* Refresh detection button */} + +
+ + {/* Loading state */} + {detecting && ( +
+ + + + + + {lang === "zh" ? "正在检测钱包..." : "Detecting wallets..."} + +
+ )} {/* Installed wallets */} - {installedWallets.length > 0 && ( + {!detecting && installedWallets.length > 0 && (
{installedWallets.map(wallet => (
)} {/* Not-installed wallets — show install links */} - {!compact && notInstalledWallets.length > 0 && ( + {!detecting && !compact && notInstalledWallets.length > 0 && (

{lang === "zh" ? "未安装(点击安装)" : "Not installed (click to install)"} @@ -342,7 +442,7 @@ export function WalletSelector({ lang, onAddressDetected, connectedAddress, comp )} {/* In compact mode, show install links inline */} - {compact && notInstalledWallets.length > 0 && installedWallets.length === 0 && ( + {!detecting && compact && notInstalledWallets.length > 0 && installedWallets.length === 0 && (

{notInstalledWallets.slice(0, 4).map(wallet => ( {error}

)} + + {/* Manual address input — divider */} +
+ + + {showManual && ( +
+

+ {lang === "zh" + ? "直接输入您的 EVM 钱包地址(0x 开头)" + : "Enter your EVM wallet address (starts with 0x)"} +

+
+ { setManualAddress(e.target.value); setManualError(null); }} + placeholder={lang === "zh" ? "0x..." : "0x..."} + 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}

+ )} +
+ )} +
); }