From 1576303898a84ddcd0a3036f75933bd85094da80 Mon Sep 17 00:00:00 2001 From: Manus Date: Tue, 10 Mar 2026 05:14:24 -0400 Subject: [PATCH] =?UTF-8?q?v12:=20WalletSelector=E9=87=8D=E5=86=99=20-=20?= =?UTF-8?q?=E5=A2=9E=E5=8A=A0=E9=94=99=E8=AF=AF=E8=AF=8A=E6=96=AD=E3=80=81?= =?UTF-8?q?MetaMask=E6=9D=83=E9=99=90=E9=87=8D=E7=BD=AE=E5=BC=95=E5=AF=BC?= =?UTF-8?q?=E3=80=81=E8=BF=9E=E6=8E=A5=E7=8A=B6=E6=80=81=E4=BC=98=E5=8C=96?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 主要改进: - ErrorHelpPanel: 分类错误处理(user_rejected/wallet_pending/not_initialized/unknown) - MetaMask 4001错误时显示5步权限重置操作指南 - 连接中状态显示'等待钱包授权...'提示 - 错误后保留重试按钮和其他可用钱包选项 - 增加eth_accounts静默检查(先检查是否已连接) - Bridge: 确认所有链USDT->XIC路由(BSC/ETH/Polygon/Arbitrum/Avalanche) --- client/src/components/WalletSelector.tsx | 376 ++++++++++++++++------- todo.md | 11 + 2 files changed, 283 insertions(+), 104 deletions(-) diff --git a/client/src/components/WalletSelector.tsx b/client/src/components/WalletSelector.tsx index 7f65bc0..070c42a 100644 --- a/client/src/components/WalletSelector.tsx +++ b/client/src/components/WalletSelector.tsx @@ -1,6 +1,6 @@ // NAC XIC Presale — Wallet Selector Component // 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 type { EthProvider } from "@/hooks/useWallet"; @@ -12,19 +12,18 @@ interface WalletInfo { name: string; icon: React.ReactNode; installUrl: string; - mobileDeepLink?: string; // DeepLink to open current page in wallet's in-app browser + mobileDeepLink?: string; isInstalled: () => boolean; connect: () => Promise<{ address: string; rawProvider: EthProvider } | null>; - network: "evm" | "tron"; // wallet network type + network: "evm" | "tron"; } interface WalletSelectorProps { lang: Lang; - // rawProvider is passed so parent can call wallet.connectWithProvider() directly onAddressDetected: (address: string, network?: "evm" | "tron", rawProvider?: EthProvider) => void; connectedAddress?: string; - compact?: boolean; // compact mode for BSC/ETH panel - showTron?: boolean; // whether to show TRON wallets (for TRC20 panel) + compact?: boolean; + showTron?: boolean; } // ── Wallet Icons ────────────────────────────────────────────────────────────── @@ -85,7 +84,6 @@ const BitgetIcon = () => ( ); -// TronLink Icon — official TRON red color const TronLinkIcon = () => ( @@ -102,7 +100,6 @@ function isMobileBrowser(): boolean { 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 { if (typeof window === "undefined") return false; const ua = navigator.userAgent.toLowerCase(); @@ -126,7 +123,6 @@ function isInWalletBrowser(): boolean { } // ── Provider detection helpers ──────────────────────────────────────────────── -// Note: EthProvider type is imported from useWallet.ts type TronLinkProvider = { ready: boolean; @@ -156,9 +152,7 @@ function getBitget(): EthProvider | null { function getTronLink(): TronLinkProvider | null { if (typeof window === "undefined") return null; 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; - // Some versions only inject tronWeb if (w.tronWeb) { return { ready: true, @@ -169,7 +163,6 @@ function getTronLink(): TronLinkProvider | null { return null; } -// Find a specific provider from the providers array or direct injection function findProvider(predicate: (p: EthProvider) => boolean): EthProvider | null { const eth = getEth(); if (!eth) return null; @@ -179,7 +172,18 @@ function findProvider(predicate: (p: EthProvider) => boolean): EthProvider | nul 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; @@ -187,22 +191,26 @@ async function requestAccounts(provider: EthProvider): Promise<{ address: string 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 { - // TronLink v1: use tronWeb.defaultAddress if (provider.tronWeb?.defaultAddress?.base58) { 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 }; if (result?.code === 200) { - // After approval, tronWeb.defaultAddress should be populated const w = window as unknown as { tronWeb?: { defaultAddress?: { base58?: string } } }; const address = w.tronWeb?.defaultAddress?.base58 ?? null; if (!address) return null; @@ -307,7 +315,6 @@ function buildWallets(showTron: boolean): WalletInfo[] { name: "TronLink", icon: , 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" }))}`, isInstalled: () => !!getTronLink(), connect: async () => { @@ -332,10 +339,6 @@ function isValidTronAddress(addr: string): boolean { 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 ───────────────────────────────────────────────────── 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 [showMore, setShowMore] = useState(false); - // MetaMask is always the primary wallet (most widely used) const metamaskDeepLink = `https://metamask.app.link/dapp/${urlWithoutProtocol}`; const otherWallets = [ @@ -383,7 +385,6 @@ function MobileDeepLinkPanel({ lang, showTron }: { lang: Lang; showTron: boolean return (
- {/* Primary: MetaMask — large prominent button */} - {/* Other wallets — collapsed by default */} +
+ ); + } + + 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 [error, setError] = 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); - setError(null); + setErrorType(null); // Wait for wallet extensions to fully inject (up to 1500ms) const timer = setTimeout(() => { setWallets(buildWallets(showTron)); @@ -485,33 +645,40 @@ export function WalletSelector({ lang, onAddressDetected, connectedAddress, comp const handleConnect = async (wallet: WalletInfo) => { setConnecting(wallet.id); - setError(null); + setErrorType(null); + setLastConnectedWallet(wallet); try { const result = await wallet.connect(); if (result) { - // Pass rawProvider so parent can call wallet.connectWithProvider() directly onAddressDetected(result.address, wallet.network, result.rawProvider); } else { - setError(lang === "zh" ? "未获取到地址,请重试" : "No address returned, please try again"); + setErrorType("unknown"); + setErrorWalletName(wallet.name); } } catch (err: unknown) { const error = err as Error; + setErrorWalletName(wallet.name); if (error.message === "user_rejected") { - setError(lang === "zh" ? "已取消连接" : "Connection cancelled"); + setErrorType("user_rejected"); } 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")) { - setError(lang === "zh" - ? "请先完成钱包初始化设置,然后刷新页面重试" - : "Please complete wallet setup first, then refresh the page"); + setErrorType("not_initialized"); } else { - setError(lang === "zh" ? "连接失败,请重试" : "Connection failed, please try again"); + 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) { @@ -555,8 +722,6 @@ export function WalletSelector({ lang, onAddressDetected, connectedAddress, comp } // ── 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) { return (
@@ -613,7 +778,6 @@ export function WalletSelector({ lang, onAddressDetected, connectedAddress, comp
); - } // ── Loading state ───────────────────────────────────────────────────────── @@ -622,7 +786,7 @@ export function WalletSelector({ lang, onAddressDetected, connectedAddress, comp

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

@@ -642,7 +806,7 @@ export function WalletSelector({ lang, onAddressDetected, connectedAddress, comp

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

{/* Refresh detection button */}
- {/* Connecting overlay — shown when any wallet is connecting */} + {/* Connecting overlay */} {connecting && (

- {lang === "zh" ? "正在连接钱包..." : "Connecting wallet..."} + {lang === "zh" ? "等待钱包授权..." : "Waiting for wallet authorization..."}

- {lang === "zh" ? "请在钱包弹窗中确认连接请求" : "Please confirm the connection request in your wallet popup"} + {lang === "zh" ? "请查看钱包弹窗并点击「连接」" : "Please check your wallet popup and click \"Connect\""}

)} - {/* Installed wallets — hidden while connecting to prevent accidental double-clicks */} - {!connecting && installedWallets.length > 0 && ( + + {/* 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 => ( + ))}
@@ -740,13 +935,6 @@ export function WalletSelector({ lang, onAddressDetected, connectedAddress, comp ? "请安装以下任一钱包,完成设置后点击上方「刷新」按钮" : "Install any wallet below, then click Refresh above after setup"}

- {showTron && ( -

- {lang === "zh" - ? "💡 TRC20 支付请安装 TronLink;BSC/ETH 支付请安装 MetaMask" - : "💡 For TRC20 install TronLink; for BSC/ETH install MetaMask"} -

- )} {!showTron && (

{lang === "zh" @@ -757,62 +945,43 @@ export function WalletSelector({ lang, onAddressDetected, connectedAddress, comp

)} - {/* Not-installed wallets — only show when NO wallet is installed and not connecting */} - {!connecting && !compact && notInstalledWallets.length > 0 && installedWallets.length === 0 && ( -
-

- {lang === "zh" ? "未安装(点击安装)" : "Not installed (click to install)"} + {/* Not installed wallets */} + {!connecting && notInstalledWallets.length > 0 && ( +

+

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

-
- {notInstalledWallets.map(wallet => ( - - {wallet.icon} - {wallet.name} - - ))} -
-
- )} - - {/* In compact mode, show install links inline */} - {!connecting && compact && notInstalledWallets.length > 0 && installedWallets.length === 0 && ( - )} - {error && !connecting && ( -

{error}

- )} - - {/* Manual address input — divider — hidden while connecting */} - {!connecting && ( + {/* Manual address input */}
)}
- )}
); } diff --git a/todo.md b/todo.md index 2b9c3cf..4ad51ee 100644 --- a/todo.md +++ b/todo.md @@ -141,3 +141,14 @@ - [ ] 修复用户取消钱包弹窗后状态不重置(error code 4001/4100处理) - [ ] 修复连接成功后回调不触发(accounts事件监听改为直接返回值处理) - [ ] 确保每次点击钱包按钮都能重新触发钱包弹窗 + +## v12 Bridge跨链桥完善 + 钱包连接深度修复 + +- [ ] WalletSelector v5:ErrorHelpPanel组件(分类错误处理+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) +- [ ] 记录部署日志