Fix: WalletSelector v2 - 改进钱包检测时序、添加刷新按钮、手动地址输入回退、错误码精准处理(user_rejected/wallet_pending);三个域名(pre-sale/ico/trc-ico.newassetchain.io)全部部署到AI服务器43.224.155.27,DNS解析已生效
This commit is contained in:
parent
81c9b2544f
commit
158822556f
|
|
@ -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
|
// 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";
|
type Lang = "zh" | "en";
|
||||||
|
|
||||||
|
|
@ -125,8 +126,24 @@ async function requestAccounts(provider: EthProvider): Promise<string | null> {
|
||||||
try {
|
try {
|
||||||
const accounts = await provider.request({ method: "eth_requestAccounts" }) as string[];
|
const accounts = await provider.request({ method: "eth_requestAccounts" }) as string[];
|
||||||
return accounts?.[0] ?? null;
|
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<boolean> {
|
||||||
|
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 {
|
} 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 ──────────────────────────────────────────────────
|
// ── WalletSelector Component ──────────────────────────────────────────────────
|
||||||
|
|
||||||
export function WalletSelector({ lang, onAddressDetected, connectedAddress, compact = false }: WalletSelectorProps) {
|
export function WalletSelector({ lang, onAddressDetected, connectedAddress, compact = false }: WalletSelectorProps) {
|
||||||
const [wallets, setWallets] = useState<WalletInfo[]>([]);
|
const [wallets, setWallets] = useState<WalletInfo[]>([]);
|
||||||
const [connecting, setConnecting] = useState<string | null>(null);
|
const [connecting, setConnecting] = useState<string | null>(null);
|
||||||
const [error, setError] = useState<string | null>(null);
|
const [error, setError] = useState<string | null>(null);
|
||||||
|
const [detecting, setDetecting] = useState(true);
|
||||||
|
const [showManual, setShowManual] = useState(false);
|
||||||
|
const [manualAddress, setManualAddress] = useState("");
|
||||||
|
const [manualError, setManualError] = useState<string | null>(null);
|
||||||
|
|
||||||
useEffect(() => {
|
const detectWallets = useCallback(() => {
|
||||||
// Build wallet list after a short delay to allow extensions to inject
|
setDetecting(true);
|
||||||
|
setError(null);
|
||||||
|
// Wait for wallet extensions to fully inject (up to 1500ms)
|
||||||
const timer = setTimeout(() => {
|
const timer = setTimeout(() => {
|
||||||
setWallets(buildWallets());
|
setWallets(buildWallets());
|
||||||
}, 400);
|
setDetecting(false);
|
||||||
|
}, 1500);
|
||||||
return () => clearTimeout(timer);
|
return () => clearTimeout(timer);
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const cleanup = detectWallets();
|
||||||
|
return cleanup;
|
||||||
|
}, [detectWallets]);
|
||||||
|
|
||||||
const handleConnect = async (wallet: WalletInfo) => {
|
const handleConnect = async (wallet: WalletInfo) => {
|
||||||
setConnecting(wallet.id);
|
setConnecting(wallet.id);
|
||||||
setError(null);
|
setError(null);
|
||||||
|
|
@ -228,13 +262,38 @@ export function WalletSelector({ lang, onAddressDetected, connectedAddress, comp
|
||||||
} else {
|
} else {
|
||||||
setError(lang === "zh" ? "未获取到地址,请重试" : "No address returned, please try again");
|
setError(lang === "zh" ? "未获取到地址,请重试" : "No address returned, please try again");
|
||||||
}
|
}
|
||||||
} catch {
|
} catch (err: unknown) {
|
||||||
setError(lang === "zh" ? "连接失败,请重试" : "Connection failed, please try again");
|
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 {
|
} finally {
|
||||||
setConnecting(null);
|
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 installedWallets = wallets.filter(w => w.isInstalled());
|
||||||
const notInstalledWallets = wallets.filter(w => !w.isInstalled());
|
const notInstalledWallets = wallets.filter(w => !w.isInstalled());
|
||||||
|
|
||||||
|
|
@ -258,12 +317,46 @@ export function WalletSelector({ lang, onAddressDetected, connectedAddress, comp
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="space-y-3">
|
<div className="space-y-3">
|
||||||
<p className="text-xs font-semibold text-white/60 uppercase tracking-wider">
|
<div className="flex items-center justify-between">
|
||||||
{lang === "zh" ? "选择钱包自动填充地址" : "Select wallet to auto-fill address"}
|
<p className="text-xs font-semibold text-white/60 uppercase tracking-wider">
|
||||||
</p>
|
{lang === "zh" ? "选择钱包自动填充地址" : "Select wallet to auto-fill address"}
|
||||||
|
</p>
|
||||||
|
{/* Refresh detection button */}
|
||||||
|
<button
|
||||||
|
onClick={detectWallets}
|
||||||
|
disabled={detecting}
|
||||||
|
className="flex items-center gap-1 text-xs px-2 py-1 rounded-lg transition-all hover:opacity-80"
|
||||||
|
style={{ background: "rgba(0,212,255,0.1)", color: "rgba(0,212,255,0.7)", border: "1px solid rgba(0,212,255,0.2)" }}
|
||||||
|
title={lang === "zh" ? "重新检测钱包" : "Re-detect wallets"}
|
||||||
|
>
|
||||||
|
<svg
|
||||||
|
width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2"
|
||||||
|
className={detecting ? "animate-spin" : ""}
|
||||||
|
>
|
||||||
|
<path d="M23 4v6h-6M1 20v-6h6"/>
|
||||||
|
<path d="M3.51 9a9 9 0 0 1 14.85-3.36L23 10M1 14l4.64 4.36A9 9 0 0 0 20.49 15"/>
|
||||||
|
</svg>
|
||||||
|
{detecting
|
||||||
|
? (lang === "zh" ? "检测中..." : "Detecting...")
|
||||||
|
: (lang === "zh" ? "刷新" : "Refresh")}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Loading state */}
|
||||||
|
{detecting && (
|
||||||
|
<div className="flex items-center justify-center py-4 gap-2">
|
||||||
|
<svg className="animate-spin w-4 h-4 text-white/40" fill="none" viewBox="0 0 24 24">
|
||||||
|
<circle className="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="4"/>
|
||||||
|
<path className="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4z"/>
|
||||||
|
</svg>
|
||||||
|
<span className="text-xs text-white/40">
|
||||||
|
{lang === "zh" ? "正在检测钱包..." : "Detecting wallets..."}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
{/* Installed wallets */}
|
{/* Installed wallets */}
|
||||||
{installedWallets.length > 0 && (
|
{!detecting && installedWallets.length > 0 && (
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
{installedWallets.map(wallet => (
|
{installedWallets.map(wallet => (
|
||||||
<button
|
<button
|
||||||
|
|
@ -300,7 +393,7 @@ export function WalletSelector({ lang, onAddressDetected, connectedAddress, comp
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* No wallets installed */}
|
{/* No wallets installed */}
|
||||||
{installedWallets.length === 0 && (
|
{!detecting && installedWallets.length === 0 && (
|
||||||
<div
|
<div
|
||||||
className="rounded-xl p-4 text-center"
|
className="rounded-xl p-4 text-center"
|
||||||
style={{ background: "rgba(255,255,255,0.04)", border: "1px dashed rgba(255,255,255,0.15)" }}
|
style={{ background: "rgba(255,255,255,0.04)", border: "1px dashed rgba(255,255,255,0.15)" }}
|
||||||
|
|
@ -308,14 +401,21 @@ export function WalletSelector({ lang, onAddressDetected, connectedAddress, comp
|
||||||
<p className="text-sm text-white/50 mb-1">
|
<p className="text-sm text-white/50 mb-1">
|
||||||
{lang === "zh" ? "未检测到 EVM 钱包" : "No EVM wallet detected"}
|
{lang === "zh" ? "未检测到 EVM 钱包" : "No EVM wallet detected"}
|
||||||
</p>
|
</p>
|
||||||
<p className="text-xs text-white/30">
|
<p className="text-xs text-white/30 mb-3">
|
||||||
{lang === "zh" ? "请安装以下任一钱包后刷新页面" : "Install any wallet below and refresh the page"}
|
{lang === "zh"
|
||||||
|
? "请安装以下任一钱包,完成设置后点击上方「刷新」按钮"
|
||||||
|
: "Install any wallet below, then click Refresh above after setup"}
|
||||||
|
</p>
|
||||||
|
<p className="text-xs text-amber-400/70">
|
||||||
|
{lang === "zh"
|
||||||
|
? "💡 已安装MetaMask?请先完成钱包初始化(创建或导入钱包),再点击刷新"
|
||||||
|
: "💡 Have MetaMask? Complete wallet setup (create or import) first, then click Refresh"}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Not-installed wallets — show install links */}
|
{/* Not-installed wallets — show install links */}
|
||||||
{!compact && notInstalledWallets.length > 0 && (
|
{!detecting && !compact && notInstalledWallets.length > 0 && (
|
||||||
<div className="space-y-1">
|
<div className="space-y-1">
|
||||||
<p className="text-xs text-white/30 mt-2">
|
<p className="text-xs text-white/30 mt-2">
|
||||||
{lang === "zh" ? "未安装(点击安装)" : "Not installed (click to install)"}
|
{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 */}
|
{/* In compact mode, show install links inline */}
|
||||||
{compact && notInstalledWallets.length > 0 && installedWallets.length === 0 && (
|
{!detecting && compact && notInstalledWallets.length > 0 && installedWallets.length === 0 && (
|
||||||
<div className="flex flex-wrap gap-2">
|
<div className="flex flex-wrap gap-2">
|
||||||
{notInstalledWallets.slice(0, 4).map(wallet => (
|
{notInstalledWallets.slice(0, 4).map(wallet => (
|
||||||
<a
|
<a
|
||||||
|
|
@ -367,6 +467,56 @@ export function WalletSelector({ lang, onAddressDetected, connectedAddress, comp
|
||||||
{error && (
|
{error && (
|
||||||
<p className="text-xs text-red-400 text-center">{error}</p>
|
<p className="text-xs text-red-400 text-center">{error}</p>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
{/* Manual address input — divider */}
|
||||||
|
<div className="pt-1">
|
||||||
|
<button
|
||||||
|
onClick={() => { setShowManual(!showManual); setManualError(null); }}
|
||||||
|
className="w-full text-xs text-white/30 hover:text-white/50 transition-colors py-1 flex items-center justify-center gap-1"
|
||||||
|
>
|
||||||
|
<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
|
||||||
|
<path d="M11 4H4a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2v-7"/>
|
||||||
|
<path d="M18.5 2.5a2.121 2.121 0 0 1 3 3L12 15l-4 1 1-4 9.5-9.5z"/>
|
||||||
|
</svg>
|
||||||
|
{showManual
|
||||||
|
? (lang === "zh" ? "收起手动输入" : "Hide manual input")
|
||||||
|
: (lang === "zh" ? "手动输入钱包地址" : "Enter address manually")}
|
||||||
|
</button>
|
||||||
|
|
||||||
|
{showManual && (
|
||||||
|
<div className="mt-2 space-y-2">
|
||||||
|
<p className="text-xs text-white/40 text-center">
|
||||||
|
{lang === "zh"
|
||||||
|
? "直接输入您的 EVM 钱包地址(0x 开头)"
|
||||||
|
: "Enter your EVM wallet address (starts with 0x)"}
|
||||||
|
</p>
|
||||||
|
<div className="flex gap-2">
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
value={manualAddress}
|
||||||
|
onChange={e => { 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()}
|
||||||
|
/>
|
||||||
|
<button
|
||||||
|
onClick={handleManualSubmit}
|
||||||
|
className="px-3 py-2 rounded-lg text-xs font-semibold transition-all hover:opacity-90 active:scale-95 whitespace-nowrap"
|
||||||
|
style={{ background: "rgba(0,212,255,0.15)", color: "#00d4ff", border: "1px solid rgba(0,212,255,0.3)" }}
|
||||||
|
>
|
||||||
|
{lang === "zh" ? "确认" : "Confirm"}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
{manualError && (
|
||||||
|
<p className="text-xs text-red-400">{manualError}</p>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue