v12: WalletSelector重写 - 增加错误诊断、MetaMask权限重置引导、连接状态优化

主要改进:
- ErrorHelpPanel: 分类错误处理(user_rejected/wallet_pending/not_initialized/unknown)
- MetaMask 4001错误时显示5步权限重置操作指南
- 连接中状态显示'等待钱包授权...'提示
- 错误后保留重试按钮和其他可用钱包选项
- 增加eth_accounts静默检查(先检查是否已连接)
- Bridge: 确认所有链USDT->XIC路由(BSC/ETH/Polygon/Arbitrum/Avalanche)
This commit is contained in:
Manus 2026-03-10 05:14:24 -04:00
parent 4bdb118cb2
commit 1576303898
2 changed files with 283 additions and 104 deletions

View File

@ -1,6 +1,6 @@
// NAC XIC Presale — Wallet Selector 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
// v4: added TronLink support (desktop window.tronLink + mobile DeepLink) // v5: improved error handling, MetaMask permission reset guide, connection diagnostics
import { useState, useEffect, useCallback } from "react"; import { useState, useEffect, useCallback } from "react";
import type { EthProvider } from "@/hooks/useWallet"; import type { EthProvider } from "@/hooks/useWallet";
@ -12,19 +12,18 @@ interface WalletInfo {
name: string; name: string;
icon: React.ReactNode; icon: React.ReactNode;
installUrl: string; installUrl: string;
mobileDeepLink?: string; // DeepLink to open current page in wallet's in-app browser mobileDeepLink?: string;
isInstalled: () => boolean; isInstalled: () => boolean;
connect: () => Promise<{ address: string; rawProvider: EthProvider } | null>; connect: () => Promise<{ address: string; rawProvider: EthProvider } | null>;
network: "evm" | "tron"; // wallet network type network: "evm" | "tron";
} }
interface WalletSelectorProps { interface WalletSelectorProps {
lang: Lang; lang: Lang;
// rawProvider is passed so parent can call wallet.connectWithProvider() directly
onAddressDetected: (address: string, network?: "evm" | "tron", rawProvider?: EthProvider) => void; onAddressDetected: (address: string, network?: "evm" | "tron", rawProvider?: EthProvider) => void;
connectedAddress?: string; connectedAddress?: string;
compact?: boolean; // compact mode for BSC/ETH panel compact?: boolean;
showTron?: boolean; // whether to show TRON wallets (for TRC20 panel) showTron?: boolean;
} }
// ── Wallet Icons ────────────────────────────────────────────────────────────── // ── Wallet Icons ──────────────────────────────────────────────────────────────
@ -85,7 +84,6 @@ const BitgetIcon = () => (
</svg> </svg>
); );
// TronLink Icon — official TRON red color
const TronLinkIcon = () => ( const TronLinkIcon = () => (
<svg width="24" height="24" viewBox="0 0 24 24" fill="none"> <svg width="24" height="24" viewBox="0 0 24 24" fill="none">
<circle cx="12" cy="12" r="12" fill="#FF0013"/> <circle cx="12" cy="12" r="12" fill="#FF0013"/>
@ -102,7 +100,6 @@ function isMobileBrowser(): boolean {
return /Android|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(navigator.userAgent); return /Android|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(navigator.userAgent);
} }
// Check if running inside a wallet's in-app browser
function isInWalletBrowser(): boolean { function isInWalletBrowser(): boolean {
if (typeof window === "undefined") return false; if (typeof window === "undefined") return false;
const ua = navigator.userAgent.toLowerCase(); const ua = navigator.userAgent.toLowerCase();
@ -126,7 +123,6 @@ function isInWalletBrowser(): boolean {
} }
// ── Provider detection helpers ──────────────────────────────────────────────── // ── Provider detection helpers ────────────────────────────────────────────────
// Note: EthProvider type is imported from useWallet.ts
type TronLinkProvider = { type TronLinkProvider = {
ready: boolean; ready: boolean;
@ -156,9 +152,7 @@ function getBitget(): EthProvider | null {
function getTronLink(): TronLinkProvider | null { function getTronLink(): TronLinkProvider | null {
if (typeof window === "undefined") return null; if (typeof window === "undefined") return null;
const w = window as unknown as { tronLink?: TronLinkProvider; tronWeb?: TronLinkProvider["tronWeb"] }; const w = window as unknown as { tronLink?: TronLinkProvider; tronWeb?: TronLinkProvider["tronWeb"] };
// TronLink injects window.tronLink; tronWeb is also available when connected
if (w.tronLink?.ready) return w.tronLink; if (w.tronLink?.ready) return w.tronLink;
// Some versions only inject tronWeb
if (w.tronWeb) { if (w.tronWeb) {
return { return {
ready: true, ready: true,
@ -169,7 +163,6 @@ function getTronLink(): TronLinkProvider | null {
return null; return null;
} }
// Find a specific provider from the providers array or direct injection
function findProvider(predicate: (p: EthProvider) => boolean): EthProvider | null { function findProvider(predicate: (p: EthProvider) => boolean): EthProvider | null {
const eth = getEth(); const eth = getEth();
if (!eth) return null; if (!eth) return null;
@ -179,7 +172,18 @@ function findProvider(predicate: (p: EthProvider) => boolean): EthProvider | nul
return predicate(eth) ? eth : null; return predicate(eth) ? eth : null;
} }
// ── Improved requestAccounts with better error classification ─────────────────
async function requestAccounts(provider: EthProvider): Promise<{ address: string; rawProvider: EthProvider } | null> { 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 { try {
const accounts = await provider.request({ method: "eth_requestAccounts" }) as string[]; const accounts = await provider.request({ method: "eth_requestAccounts" }) as string[];
const address = accounts?.[0] ?? null; const address = accounts?.[0] ?? null;
@ -187,22 +191,26 @@ async function requestAccounts(provider: EthProvider): Promise<{ address: string
return { address, rawProvider: provider }; return { address, rawProvider: provider };
} catch (err: unknown) { } catch (err: unknown) {
const error = err as { code?: number; message?: string }; 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 === 4001) throw new Error("user_rejected");
if (error?.code === -32002) throw new Error("wallet_pending"); 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; throw err;
} }
} }
async function requestTronAccounts(provider: TronLinkProvider): Promise<{ address: string; rawProvider: EthProvider } | null> { async function requestTronAccounts(provider: TronLinkProvider): Promise<{ address: string; rawProvider: EthProvider } | null> {
try { try {
// TronLink v1: use tronWeb.defaultAddress
if (provider.tronWeb?.defaultAddress?.base58) { if (provider.tronWeb?.defaultAddress?.base58) {
return { address: provider.tronWeb.defaultAddress.base58, rawProvider: provider as unknown as EthProvider }; return { address: provider.tronWeb.defaultAddress.base58, rawProvider: provider as unknown as EthProvider };
} }
// TronLink v2+: use request method
const result = await provider.request({ method: "tron_requestAccounts" }) as { code?: number; message?: string }; const result = await provider.request({ method: "tron_requestAccounts" }) as { code?: number; message?: string };
if (result?.code === 200) { if (result?.code === 200) {
// After approval, tronWeb.defaultAddress should be populated
const w = window as unknown as { tronWeb?: { defaultAddress?: { base58?: string } } }; const w = window as unknown as { tronWeb?: { defaultAddress?: { base58?: string } } };
const address = w.tronWeb?.defaultAddress?.base58 ?? null; const address = w.tronWeb?.defaultAddress?.base58 ?? null;
if (!address) return null; if (!address) return null;
@ -307,7 +315,6 @@ function buildWallets(showTron: boolean): WalletInfo[] {
name: "TronLink", name: "TronLink",
icon: <TronLinkIcon />, icon: <TronLinkIcon />,
installUrl: "https://www.tronlink.org/", installUrl: "https://www.tronlink.org/",
// TronLink mobile DeepLink — opens current URL in TronLink's built-in browser
mobileDeepLink: `tronlinkoutside://pull.activity?param=${encodeURIComponent(JSON.stringify({ url: currentUrl, action: "open", protocol: "tronlink", version: "1.0" }))}`, mobileDeepLink: `tronlinkoutside://pull.activity?param=${encodeURIComponent(JSON.stringify({ url: currentUrl, action: "open", protocol: "tronlink", version: "1.0" }))}`,
isInstalled: () => !!getTronLink(), isInstalled: () => !!getTronLink(),
connect: async () => { connect: async () => {
@ -332,10 +339,6 @@ function isValidTronAddress(addr: string): boolean {
return /^T[1-9A-HJ-NP-Za-km-z]{33}$/.test(addr); return /^T[1-9A-HJ-NP-Za-km-z]{33}$/.test(addr);
} }
function isValidAddress(addr: string): boolean {
return isValidEthAddress(addr) || isValidTronAddress(addr);
}
// ── Mobile DeepLink Panel ───────────────────────────────────────────────────── // ── Mobile DeepLink Panel ─────────────────────────────────────────────────────
function MobileDeepLinkPanel({ lang, showTron }: { lang: Lang; showTron: boolean }) { function MobileDeepLinkPanel({ lang, showTron }: { lang: Lang; showTron: boolean }) {
@ -343,7 +346,6 @@ function MobileDeepLinkPanel({ lang, showTron }: { lang: Lang; showTron: boolean
const urlWithoutProtocol = currentUrl.replace(/^https?:\/\//, ""); const urlWithoutProtocol = currentUrl.replace(/^https?:\/\//, "");
const [showMore, setShowMore] = useState(false); const [showMore, setShowMore] = useState(false);
// MetaMask is always the primary wallet (most widely used)
const metamaskDeepLink = `https://metamask.app.link/dapp/${urlWithoutProtocol}`; const metamaskDeepLink = `https://metamask.app.link/dapp/${urlWithoutProtocol}`;
const otherWallets = [ const otherWallets = [
@ -383,7 +385,6 @@ function MobileDeepLinkPanel({ lang, showTron }: { lang: Lang; showTron: boolean
return ( return (
<div className="space-y-3"> <div className="space-y-3">
{/* Primary: MetaMask — large prominent button */}
<a <a
href={metamaskDeepLink} href={metamaskDeepLink}
className="w-full flex items-center gap-3 px-4 py-4 rounded-2xl transition-all hover:opacity-90 active:scale-[0.98] block" className="w-full flex items-center gap-3 px-4 py-4 rounded-2xl transition-all hover:opacity-90 active:scale-[0.98] block"
@ -407,7 +408,6 @@ function MobileDeepLinkPanel({ lang, showTron }: { lang: Lang; showTron: boolean
</svg> </svg>
</a> </a>
{/* Other wallets — collapsed by default */}
<button <button
onClick={() => setShowMore(v => !v)} onClick={() => setShowMore(v => !v)}
className="w-full text-xs text-white/35 hover:text-white/55 transition-colors py-1 flex items-center justify-center gap-1" className="w-full text-xs text-white/35 hover:text-white/55 transition-colors py-1 flex items-center justify-center gap-1"
@ -454,22 +454,182 @@ function MobileDeepLinkPanel({ lang, showTron }: { lang: Lang; showTron: boolean
); );
} }
// ── 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 (
<div
className="rounded-xl p-4 space-y-3"
style={{ background: "rgba(255,80,80,0.06)", border: "1px solid rgba(255,80,80,0.2)" }}
>
<div className="flex items-start gap-2">
<span className="text-red-400 text-base flex-shrink-0"></span>
<div>
<p className="text-sm font-semibold text-red-400">
{isZh ? "连接被拒绝" : "Connection Rejected"}
</p>
<p className="text-xs text-white/50 mt-1">
{isZh
? `${walletName} 拒绝了连接请求。可能原因:`
: `${walletName} rejected the connection. Possible reasons:`}
</p>
</div>
</div>
<div className="space-y-1.5 text-xs text-white/40 pl-4">
<p>
{isZh
? "1. 您在钱包弹窗中点击了「拒绝」"
: "1. You clicked \"Reject\" in the wallet popup"}
</p>
<p>
{isZh
? "2. 该网站之前被您在 MetaMask 中屏蔽(最常见原因)"
: "2. This site was previously blocked in your wallet (most common)"}
</p>
<p>
{isZh
? "3. 钱包弹窗未显示(被浏览器拦截)"
: "3. Wallet popup was blocked by browser"}
</p>
</div>
{walletName === "MetaMask" && (
<div
className="rounded-lg p-3 space-y-1.5"
style={{ background: "rgba(240,180,41,0.06)", border: "1px solid rgba(240,180,41,0.15)" }}
>
<p className="text-xs font-semibold text-amber-400">
{isZh ? "🔧 MetaMask 权限重置步骤:" : "🔧 MetaMask Permission Reset:"}
</p>
<div className="space-y-1 text-xs text-white/40">
<p>{isZh ? "① 点击 MetaMask 图标打开扩展" : "① Click MetaMask icon to open extension"}</p>
<p>{isZh ? "② 点击右上角菜单(三个点)" : "② Click top-right menu (three dots)"}</p>
<p>{isZh ? "③ 选择「已连接的网站」" : "③ Select \"Connected Sites\""}</p>
<p>{isZh ? "④ 找到本网站并删除" : "④ Find this site and remove it"}</p>
<p>{isZh ? "⑤ 回到此页面重新点击连接" : "⑤ Return here and try connecting again"}</p>
</div>
</div>
)}
<button
onClick={onRetry}
className="w-full py-2 rounded-lg text-xs font-semibold transition-all hover:opacity-90"
style={{ background: "rgba(0,212,255,0.12)", color: "#00d4ff", border: "1px solid rgba(0,212,255,0.25)" }}
>
{isZh ? "重试连接" : "Retry Connection"}
</button>
</div>
);
}
if (errorType === "wallet_pending") {
return (
<div
className="rounded-xl p-4 space-y-3"
style={{ background: "rgba(240,180,41,0.06)", border: "1px solid rgba(240,180,41,0.2)" }}
>
<div className="flex items-start gap-2">
<span className="text-amber-400 text-base flex-shrink-0"></span>
<div>
<p className="text-sm font-semibold text-amber-400">
{isZh ? "钱包有待处理的请求" : "Wallet Has Pending Request"}
</p>
<p className="text-xs text-white/50 mt-1">
{isZh
? "请查看钱包弹窗并处理待处理的请求,然后重试"
: "Please check your wallet popup and handle the pending request, then retry"}
</p>
</div>
</div>
<button
onClick={onRetry}
className="w-full py-2 rounded-lg text-xs font-semibold transition-all hover:opacity-90"
style={{ background: "rgba(240,180,41,0.12)", color: "#f0b429", border: "1px solid rgba(240,180,41,0.25)" }}
>
{isZh ? "重试" : "Retry"}
</button>
</div>
);
}
if (errorType === "not_initialized") {
return (
<div
className="rounded-xl p-4 space-y-3"
style={{ background: "rgba(0,212,255,0.06)", border: "1px solid rgba(0,212,255,0.2)" }}
>
<div className="flex items-start gap-2">
<span className="text-cyan-400 text-base flex-shrink-0"></span>
<div>
<p className="text-sm font-semibold text-cyan-400">
{isZh ? "钱包未完成初始化" : "Wallet Not Initialized"}
</p>
<p className="text-xs text-white/50 mt-1">
{isZh
? "请先完成钱包设置(创建或导入钱包),然后刷新页面重试"
: "Please complete wallet setup (create or import wallet), then refresh the page"}
</p>
</div>
</div>
<button
onClick={() => window.location.reload()}
className="w-full py-2 rounded-lg text-xs font-semibold transition-all hover:opacity-90"
style={{ background: "rgba(0,212,255,0.12)", color: "#00d4ff", border: "1px solid rgba(0,212,255,0.25)" }}
>
{isZh ? "刷新页面" : "Refresh Page"}
</button>
</div>
);
}
// Unknown error
return (
<div
className="rounded-xl p-4 space-y-2"
style={{ background: "rgba(255,80,80,0.06)", border: "1px solid rgba(255,80,80,0.2)" }}
>
<p className="text-sm font-semibold text-red-400">
{isZh ? "连接失败" : "Connection Failed"}
</p>
<p className="text-xs text-white/40">
{isZh ? "请刷新页面后重试,或尝试其他钱包" : "Please refresh the page and try again, or try another wallet"}
</p>
<button
onClick={onRetry}
className="w-full py-2 rounded-lg text-xs font-semibold transition-all hover:opacity-90"
style={{ background: "rgba(0,212,255,0.12)", color: "#00d4ff", border: "1px solid rgba(0,212,255,0.25)" }}
>
{isZh ? "重试" : "Retry"}
</button>
</div>
);
}
// ── WalletSelector Component ────────────────────────────────────────────────── // ── WalletSelector Component ──────────────────────────────────────────────────
export function WalletSelector({ lang, onAddressDetected, connectedAddress, compact = false, showTron = false }: WalletSelectorProps) { export function WalletSelector({ lang, onAddressDetected, connectedAddress, compact = false, showTron = 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 [errorType, setErrorType] = useState<"user_rejected" | "wallet_pending" | "not_initialized" | "unknown" | null>(null);
const [errorWalletName, setErrorWalletName] = useState<string>("");
const [detecting, setDetecting] = useState(true); const [detecting, setDetecting] = useState(true);
const [showManual, setShowManual] = useState(false); const [showManual, setShowManual] = useState(false);
const [manualAddress, setManualAddress] = useState(""); const [manualAddress, setManualAddress] = useState("");
const [manualError, setManualError] = useState<string | null>(null); const [manualError, setManualError] = useState<string | null>(null);
const [isMobile] = useState(() => isMobileBrowser()); const [isMobile] = useState(() => isMobileBrowser());
const [inWalletBrowser] = useState(() => isInWalletBrowser()); const [inWalletBrowser] = useState(() => isInWalletBrowser());
const [lastConnectedWallet, setLastConnectedWallet] = useState<WalletInfo | null>(null);
const detectWallets = useCallback(() => { const detectWallets = useCallback(() => {
setDetecting(true); setDetecting(true);
setError(null); setErrorType(null);
// Wait for wallet extensions to fully inject (up to 1500ms) // Wait for wallet extensions to fully inject (up to 1500ms)
const timer = setTimeout(() => { const timer = setTimeout(() => {
setWallets(buildWallets(showTron)); setWallets(buildWallets(showTron));
@ -485,33 +645,40 @@ export function WalletSelector({ lang, onAddressDetected, connectedAddress, comp
const handleConnect = async (wallet: WalletInfo) => { const handleConnect = async (wallet: WalletInfo) => {
setConnecting(wallet.id); setConnecting(wallet.id);
setError(null); setErrorType(null);
setLastConnectedWallet(wallet);
try { try {
const result = await wallet.connect(); const result = await wallet.connect();
if (result) { if (result) {
// Pass rawProvider so parent can call wallet.connectWithProvider() directly
onAddressDetected(result.address, wallet.network, result.rawProvider); onAddressDetected(result.address, wallet.network, result.rawProvider);
} else { } else {
setError(lang === "zh" ? "未获取到地址,请重试" : "No address returned, please try again"); setErrorType("unknown");
setErrorWalletName(wallet.name);
} }
} catch (err: unknown) { } catch (err: unknown) {
const error = err as Error; const error = err as Error;
setErrorWalletName(wallet.name);
if (error.message === "user_rejected") { if (error.message === "user_rejected") {
setError(lang === "zh" ? "已取消连接" : "Connection cancelled"); setErrorType("user_rejected");
} else if (error.message === "wallet_pending") { } else if (error.message === "wallet_pending") {
setError(lang === "zh" ? "钱包请求处理中,请检查钱包弹窗" : "Wallet request pending, please check your wallet popup"); setErrorType("wallet_pending");
} else if (error.message?.includes("not initialized") || error.message?.includes("setup")) { } else if (error.message?.includes("not initialized") || error.message?.includes("setup")) {
setError(lang === "zh" setErrorType("not_initialized");
? "请先完成钱包初始化设置,然后刷新页面重试"
: "Please complete wallet setup first, then refresh the page");
} else { } else {
setError(lang === "zh" ? "连接失败,请重试" : "Connection failed, please try again"); setErrorType("unknown");
} }
} finally { } finally {
setConnecting(null); setConnecting(null);
} }
}; };
const handleRetry = useCallback(() => {
setErrorType(null);
if (lastConnectedWallet) {
handleConnect(lastConnectedWallet);
}
}, [lastConnectedWallet]); // eslint-disable-line react-hooks/exhaustive-deps
const handleManualSubmit = () => { const handleManualSubmit = () => {
const addr = manualAddress.trim(); const addr = manualAddress.trim();
if (!addr) { if (!addr) {
@ -555,8 +722,6 @@ export function WalletSelector({ lang, onAddressDetected, connectedAddress, comp
} }
// ── Mobile browser (not in wallet app) — show DeepLink guide ────────────── // ── Mobile browser (not in wallet app) — show DeepLink guide ──────────────
// On mobile browsers, ALWAYS show DeepLink guide regardless of whether wallets are "detected"
// because window.ethereum/okxwallet is NOT available in mobile browsers (only in wallet app's built-in browser)
if (isMobile && !inWalletBrowser && !detecting) { if (isMobile && !inWalletBrowser && !detecting) {
return ( return (
<div className="space-y-3"> <div className="space-y-3">
@ -613,7 +778,6 @@ export function WalletSelector({ lang, onAddressDetected, connectedAddress, comp
</div> </div>
</div> </div>
); );
} }
// ── Loading state ───────────────────────────────────────────────────────── // ── Loading state ─────────────────────────────────────────────────────────
@ -622,7 +786,7 @@ export function WalletSelector({ lang, onAddressDetected, connectedAddress, comp
<div className="space-y-3"> <div className="space-y-3">
<div className="flex items-center justify-between"> <div className="flex items-center justify-between">
<p className="text-xs font-semibold text-white/60 uppercase tracking-wider"> <p className="text-xs font-semibold text-white/60 uppercase tracking-wider">
{lang === "zh" ? "选择钱包自动填充地址" : "Select wallet to auto-fill address"} {lang === "zh" ? "选择钱包连接" : "Select Wallet to Connect"}
</p> </p>
</div> </div>
<div className="flex items-center justify-center py-4 gap-2"> <div className="flex items-center justify-center py-4 gap-2">
@ -642,7 +806,7 @@ export function WalletSelector({ lang, onAddressDetected, connectedAddress, comp
<div className="space-y-3"> <div className="space-y-3">
<div className="flex items-center justify-between"> <div className="flex items-center justify-between">
<p className="text-xs font-semibold text-white/60 uppercase tracking-wider"> <p className="text-xs font-semibold text-white/60 uppercase tracking-wider">
{lang === "zh" ? "选择钱包自动填充地址" : "Select wallet to auto-fill address"} {lang === "zh" ? "选择钱包连接" : "Select Wallet to Connect"}
</p> </p>
{/* Refresh detection button */} {/* Refresh detection button */}
<button <button
@ -665,7 +829,7 @@ export function WalletSelector({ lang, onAddressDetected, connectedAddress, comp
</button> </button>
</div> </div>
{/* Connecting overlay — shown when any wallet is connecting */} {/* Connecting overlay */}
{connecting && ( {connecting && (
<div <div
className="rounded-xl p-4 flex items-center gap-3" className="rounded-xl p-4 flex items-center gap-3"
@ -677,16 +841,27 @@ export function WalletSelector({ lang, onAddressDetected, connectedAddress, comp
</svg> </svg>
<div> <div>
<p className="text-sm font-semibold text-white/80"> <p className="text-sm font-semibold text-white/80">
{lang === "zh" ? "正在连接钱包..." : "Connecting wallet..."} {lang === "zh" ? "等待钱包授权..." : "Waiting for wallet authorization..."}
</p> </p>
<p className="text-xs text-white/40 mt-0.5"> <p className="text-xs text-white/40 mt-0.5">
{lang === "zh" ? "请在钱包弹窗中确认连接请求" : "Please confirm the connection request in your wallet popup"} {lang === "zh" ? "请查看钱包弹窗并点击「连接」" : "Please check your wallet popup and click \"Connect\""}
</p> </p>
</div> </div>
</div> </div>
)} )}
{/* Installed wallets — hidden while connecting to prevent accidental double-clicks */}
{!connecting && installedWallets.length > 0 && ( {/* Error panel */}
{!connecting && errorType && (
<ErrorHelpPanel
errorType={errorType}
walletName={errorWalletName}
lang={lang}
onRetry={handleRetry}
/>
)}
{/* Installed wallets */}
{!connecting && !errorType && installedWallets.length > 0 && (
<div className="space-y-2"> <div className="space-y-2">
{installedWallets.map(wallet => ( {installedWallets.map(wallet => (
<button <button
@ -709,16 +884,36 @@ export function WalletSelector({ lang, onAddressDetected, connectedAddress, comp
> >
{lang === "zh" ? "已安装" : "Installed"} {lang === "zh" ? "已安装" : "Installed"}
</span> </span>
{connecting === wallet.id ? ( <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="rgba(0,212,255,0.7)" strokeWidth="2" className="flex-shrink-0">
<svg className="animate-spin w-4 h-4 text-white/60 flex-shrink-0" fill="none" viewBox="0 0 24 24"> <path d="M5 12h14M12 5l7 7-7 7"/>
<circle className="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="4"/> </svg>
<path className="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4z"/> </button>
</svg> ))}
) : ( </div>
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="rgba(0,212,255,0.7)" strokeWidth="2" className="flex-shrink-0"> )}
<path d="M5 12h14M12 5l7 7-7 7"/>
</svg> {/* Show installed wallets even when there's an error (for retry) */}
)} {!connecting && errorType && installedWallets.length > 0 && (
<div className="space-y-2">
<p className="text-xs text-white/30 text-center">
{lang === "zh" ? "或选择其他钱包" : "Or try another wallet"}
</p>
{installedWallets.map(wallet => (
<button
key={wallet.id}
onClick={() => handleConnect(wallet)}
disabled={!!connecting}
className="w-full flex items-center gap-3 px-4 py-3 rounded-xl transition-all hover:opacity-90 active:scale-[0.98]"
style={{
background: "rgba(255,255,255,0.04)",
border: "1px solid rgba(255,255,255,0.1)",
}}
>
<span className="flex-shrink-0">{wallet.icon}</span>
<span className="flex-1 text-left text-sm font-semibold text-white/70">{wallet.name}</span>
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="rgba(255,255,255,0.3)" strokeWidth="2" className="flex-shrink-0">
<path d="M5 12h14M12 5l7 7-7 7"/>
</svg>
</button> </button>
))} ))}
</div> </div>
@ -740,13 +935,6 @@ export function WalletSelector({ lang, onAddressDetected, connectedAddress, comp
? "请安装以下任一钱包,完成设置后点击上方「刷新」按钮" ? "请安装以下任一钱包,完成设置后点击上方「刷新」按钮"
: "Install any wallet below, then click Refresh above after setup"} : "Install any wallet below, then click Refresh above after setup"}
</p> </p>
{showTron && (
<p className="text-xs text-amber-400/70">
{lang === "zh"
? "💡 TRC20 支付请安装 TronLinkBSC/ETH 支付请安装 MetaMask"
: "💡 For TRC20 install TronLink; for BSC/ETH install MetaMask"}
</p>
)}
{!showTron && ( {!showTron && (
<p className="text-xs text-amber-400/70"> <p className="text-xs text-amber-400/70">
{lang === "zh" {lang === "zh"
@ -757,62 +945,43 @@ export function WalletSelector({ lang, onAddressDetected, connectedAddress, comp
</div> </div>
)} )}
{/* Not-installed wallets — only show when NO wallet is installed and not connecting */} {/* Not installed wallets */}
{!connecting && !compact && notInstalledWallets.length > 0 && installedWallets.length === 0 && ( {!connecting && notInstalledWallets.length > 0 && (
<div className="space-y-1"> <div className="space-y-1.5">
<p className="text-xs text-white/30 mt-2"> <p className="text-xs text-white/25 uppercase tracking-wider">
{lang === "zh" ? "未安装(点击安装)" : "Not installed (click to install)"} {lang === "zh" ? "未安装" : "Not installed"}
</p> </p>
<div className="grid grid-cols-3 gap-2"> {notInstalledWallets.map(wallet => (
{notInstalledWallets.map(wallet => (
<a
key={wallet.id}
href={wallet.installUrl}
target="_blank"
rel="noopener noreferrer"
className="flex flex-col items-center gap-1.5 p-2.5 rounded-xl transition-all hover:opacity-80"
style={{
background: "rgba(255,255,255,0.04)",
border: "1px solid rgba(255,255,255,0.08)",
}}
>
<span className="opacity-40">{wallet.icon}</span>
<span className="text-xs text-white/30 text-center leading-tight">{wallet.name}</span>
</a>
))}
</div>
</div>
)}
{/* In compact mode, show install links inline */}
{!connecting && compact && notInstalledWallets.length > 0 && installedWallets.length === 0 && (
<div className="flex flex-wrap gap-2">
{notInstalledWallets.slice(0, 4).map(wallet => (
<a <a
key={wallet.id} key={wallet.id}
href={wallet.installUrl} href={wallet.installUrl}
target="_blank" target="_blank"
rel="noopener noreferrer" rel="noopener noreferrer"
className="flex items-center gap-1.5 px-3 py-1.5 rounded-lg text-xs transition-all hover:opacity-80" className="w-full flex items-center gap-3 px-4 py-2.5 rounded-xl transition-all hover:opacity-80"
style={{ style={{
background: "rgba(255,255,255,0.05)", background: "rgba(255,255,255,0.02)",
border: "1px solid rgba(255,255,255,0.1)", border: "1px dashed rgba(255,255,255,0.1)",
color: "rgba(255,255,255,0.4)",
}} }}
> >
<span className="opacity-50">{wallet.icon}</span> <span className="flex-shrink-0 opacity-50">{wallet.icon}</span>
{lang === "zh" ? `安装 ${wallet.name}` : `Install ${wallet.name}`} <span className="flex-1 text-left text-sm text-white/40">{wallet.name}</span>
<span
className="text-xs px-2 py-0.5 rounded-full font-medium"
style={{ background: "rgba(255,255,255,0.05)", color: "rgba(255,255,255,0.3)" }}
>
{lang === "zh" ? "点击安装" : "Install"}
</span>
<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="rgba(255,255,255,0.2)" strokeWidth="2" className="flex-shrink-0">
<path d="M18 13v6a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h6"/>
<polyline points="15 3 21 3 21 9"/>
<line x1="10" y1="14" x2="21" y2="3"/>
</svg>
</a> </a>
))} ))}
</div> </div>
)} )}
{error && !connecting && ( {/* Manual address input */}
<p className="text-xs text-red-400 text-center">{error}</p>
)}
{/* Manual address input — divider — hidden while connecting */}
{!connecting && (
<div className="pt-1"> <div className="pt-1">
<button <button
onClick={() => { setShowManual(!showManual); setManualError(null); }} onClick={() => { setShowManual(!showManual); setManualError(null); }}
@ -861,7 +1030,6 @@ export function WalletSelector({ lang, onAddressDetected, connectedAddress, comp
</div> </div>
)} )}
</div> </div>
)}
</div> </div>
); );
} }

11
todo.md
View File

@ -141,3 +141,14 @@
- [ ] 修复用户取消钱包弹窗后状态不重置error code 4001/4100处理 - [ ] 修复用户取消钱包弹窗后状态不重置error code 4001/4100处理
- [ ] 修复连接成功后回调不触发accounts事件监听改为直接返回值处理 - [ ] 修复连接成功后回调不触发accounts事件监听改为直接返回值处理
- [ ] 确保每次点击钱包按钮都能重新触发钱包弹窗 - [ ] 确保每次点击钱包按钮都能重新触发钱包弹窗
## v12 Bridge跨链桥完善 + 钱包连接深度修复
- [ ] WalletSelector v5ErrorHelpPanel组件分类错误处理+MetaMask权限重置5步指南
- [ ] WalletSelector v5连接中状态改为"等待钱包授权"提示
- [ ] WalletSelector v5错误后显示"重试"按钮和其他可用钱包
- [ ] Bridge页面确认所有链(BSC/ETH/Polygon/Arbitrum/Avalanche)USDT→XIC路由逻辑
- [ ] Bridge页面Gas费说明每条链原生代币BNB/ETH/MATIC/ETH/AVAX
- [ ] 构建v12并部署到AI服务器(43.224.155.27)
- [ ] 同步代码到备份Git库(git.newassetchain.io)
- [ ] 记录部署日志