diff --git a/client/src/components/WalletSelector.tsx b/client/src/components/WalletSelector.tsx index 800662f..2dd2ab6 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 -// v2: improved detection timing, refresh button, manual address fallback +// v4: added TronLink support (desktop window.tronLink + mobile DeepLink) import { useState, useEffect, useCallback } from "react"; @@ -11,15 +11,18 @@ interface WalletInfo { name: string; icon: React.ReactNode; installUrl: string; + mobileDeepLink?: string; // DeepLink to open current page in wallet's in-app browser isInstalled: () => boolean; connect: () => Promise; + network: "evm" | "tron"; // wallet network type } interface WalletSelectorProps { lang: Lang; - onAddressDetected: (address: string) => void; + onAddressDetected: (address: string, network?: "evm" | "tron") => void; connectedAddress?: string; compact?: boolean; // compact mode for BSC/ETH panel + showTron?: boolean; // whether to show TRON wallets (for TRC20 panel) } // ── Wallet Icons ────────────────────────────────────────────────────────────── @@ -80,6 +83,46 @@ const BitgetIcon = () => ( ); +// TronLink Icon — official TRON red color +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); +} + +// Check if running inside a wallet's in-app browser +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 EthProvider = { @@ -96,6 +139,15 @@ type EthProvider = { request: (args: { method: string; params?: unknown[] }) => Promise; }; +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; @@ -112,6 +164,22 @@ function getBitget(): EthProvider | null { 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"] }; + // 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, + tronWeb: w.tronWeb, + request: async () => 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(); @@ -128,61 +196,79 @@ async function requestAccounts(provider: EthProvider): Promise { 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 { +async function requestTronAccounts(provider: TronLinkProvider): 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 false; + // TronLink v1: use tronWeb.defaultAddress + if (provider.tronWeb?.defaultAddress?.base58) { + return provider.tronWeb.defaultAddress.base58; + } + // 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 } } }; + return w.tronWeb?.defaultAddress?.base58 ?? null; + } + 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(): WalletInfo[] { - return [ +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", @@ -194,6 +280,7 @@ function buildWallets(): WalletInfo[] { const p = findProvider(p => !!p.isCoinbaseWallet) ?? getEth(); return p ? requestAccounts(p) : null; }, + network: "evm", }, { id: "tokenpocket", @@ -205,6 +292,7 @@ function buildWallets(): WalletInfo[] { const p = findProvider(p => !!p.isTokenPocket) ?? getEth(); return p ? requestAccounts(p) : null; }, + network: "evm", }, { id: "bitget", @@ -216,8 +304,29 @@ function buildWallets(): WalletInfo[] { 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/", + // TronLink mobile DeepLink — opens current URL in TronLink's built-in browser + mobileDeepLink: `tronlinkoutside://pull?param=${encodeURIComponent(JSON.stringify({ url: currentUrl, action: "open", protocol: "tronlink" }))}`, + 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 @@ -225,9 +334,157 @@ 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); +} + +function isValidAddress(addr: string): boolean { + return isValidEthAddress(addr) || isValidTronAddress(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 tronWallets = [ + { + id: "tronlink", + name: "TronLink", + icon: , + deepLink: `tronlinkoutside://pull?param=${encodeURIComponent(JSON.stringify({ url: currentUrl, action: "open", protocol: "tronlink" }))}`, + installUrl: "https://www.tronlink.org/", + badge: "TRON", + badgeColor: "#FF0013", + }, + ]; + + const evmWallets = [ + { + id: "metamask", + name: "MetaMask", + icon: , + deepLink: `https://metamask.app.link/dapp/${urlWithoutProtocol}`, + installUrl: "https://metamask.io/download/", + badge: "EVM", + badgeColor: "#E27625", + }, + { + id: "trust", + name: "Trust Wallet", + icon: , + deepLink: `https://link.trustwallet.com/open_url?coin_id=60&url=${encodeURIComponent(currentUrl)}`, + installUrl: "https://trustwallet.com/download", + badge: "EVM", + badgeColor: "#3375BB", + }, + { + id: "okx", + name: "OKX Wallet", + icon: , + deepLink: `okx://wallet/dapp/url?dappUrl=${encodeURIComponent(currentUrl)}`, + installUrl: "https://www.okx.com/web3", + badge: "EVM", + badgeColor: "#00F0FF", + }, + { + id: "tokenpocket", + name: "TokenPocket", + icon: , + deepLink: `tpoutside://pull?param=${encodeURIComponent(JSON.stringify({ url: currentUrl }))}`, + installUrl: "https://www.tokenpocket.pro/en/download/app", + badge: "EVM/TRON", + badgeColor: "#2980FE", + }, + ]; + + const walletList = showTron ? [...tronWallets, ...evmWallets] : evmWallets; + + return ( +
+ {/* Mobile guidance header */} +
+
+ 📱 +
+

+ {lang === "zh" ? "手机端连接钱包" : "Connect Wallet on Mobile"} +

+

+ {lang === "zh" + ? "手机浏览器不支持钱包扩展。请选择以下任一钱包 App,在其内置浏览器中打开本页面即可连接钱包。" + : "Mobile browsers don't support wallet extensions. Open this page in a wallet app's built-in browser to connect."} +

+
+
+
+ + {/* Wallet DeepLink buttons */} +
+

+ {lang === "zh" ? "选择钱包 App 打开本页面" : "Choose a wallet app to open this page"} +

+ {walletList.map(wallet => ( + + {wallet.icon} + {wallet.name} + + {wallet.badge} + + + {lang === "zh" ? "在 App 中打开" : "Open in App"} + + + + + + + + ))} +
+ + {/* Step guide */} +
+

+ {lang === "zh" ? "操作步骤" : "How it works"} +

+ {[ + lang === "zh" ? "1. 点击上方任一钱包 App 按钮" : "1. Tap any wallet app button above", + lang === "zh" ? "2. 在钱包 App 的内置浏览器中打开本页面" : "2. Page opens in the wallet app's browser", + lang === "zh" ? "3. 点击「连接钱包」即可自动连接" : "3. Tap 'Connect Wallet' to connect automatically", + ].map((step, i) => ( +

{step}

+ ))} +
+
+ ); +} + // ── WalletSelector Component ────────────────────────────────────────────────── -export function WalletSelector({ lang, onAddressDetected, connectedAddress, compact = false }: WalletSelectorProps) { +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); @@ -235,17 +492,19 @@ export function WalletSelector({ lang, onAddressDetected, connectedAddress, comp const [showManual, setShowManual] = useState(false); const [manualAddress, setManualAddress] = useState(""); const [manualError, setManualError] = useState(null); + const [isMobile] = useState(() => isMobileBrowser()); + const [inWalletBrowser] = useState(() => isInWalletBrowser()); const detectWallets = useCallback(() => { setDetecting(true); setError(null); // Wait for wallet extensions to fully inject (up to 1500ms) const timer = setTimeout(() => { - setWallets(buildWallets()); + setWallets(buildWallets(showTron)); setDetecting(false); }, 1500); return () => clearTimeout(timer); - }, []); + }, [showTron]); useEffect(() => { const cleanup = detectWallets(); @@ -258,7 +517,7 @@ export function WalletSelector({ lang, onAddressDetected, connectedAddress, comp try { const address = await wallet.connect(); if (address) { - onAddressDetected(address); + onAddressDetected(address, wallet.network); } else { setError(lang === "zh" ? "未获取到地址,请重试" : "No address returned, please try again"); } @@ -269,8 +528,8 @@ export function WalletSelector({ lang, onAddressDetected, connectedAddress, comp } 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" - ? "请先完成钱包初始化设置,然后刷新页面重试" + setError(lang === "zh" + ? "请先完成钱包初始化设置,然后刷新页面重试" : "Please complete wallet setup first, then refresh the page"); } else { setError(lang === "zh" ? "连接失败,请重试" : "Connection failed, please try again"); @@ -286,12 +545,19 @@ export function WalletSelector({ lang, onAddressDetected, connectedAddress, comp 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"); + if (isValidEthAddress(addr)) { + setManualError(null); + onAddressDetected(addr, "evm"); return; } - setManualError(null); - onAddressDetected(addr); + 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()); @@ -315,6 +581,91 @@ export function WalletSelector({ lang, onAddressDetected, connectedAddress, comp ); } + // ── Mobile browser (not in wallet app) — show DeepLink guide ────────────── + if (isMobile && !inWalletBrowser && !detecting) { + const hasInstalledWallet = installedWallets.length > 0; + + if (!hasInstalledWallet) { + 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 auto-fill address"} +

+
+
+ + + + + + {lang === "zh" ? "正在检测钱包..." : "Detecting wallets..."} + +
+
+ ); + } + return (
@@ -336,27 +687,14 @@ export function WalletSelector({ lang, onAddressDetected, connectedAddress, comp - {detecting - ? (lang === "zh" ? "检测中..." : "Detecting...") + {detecting + ? (lang === "zh" ? "检测中..." : "Detecting...") : (lang === "zh" ? "刷新" : "Refresh")}
- {/* Loading state */} - {detecting && ( -
- - - - - - {lang === "zh" ? "正在检测钱包..." : "Detecting wallets..."} - -
- )} - {/* Installed wallets */} - {!detecting && installedWallets.length > 0 && ( + {installedWallets.length > 0 && (
{installedWallets.map(wallet => (
)} - {/* No wallets installed */} - {!detecting && installedWallets.length === 0 && ( + {/* No wallets installed — desktop */} + {installedWallets.length === 0 && (

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

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

-

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

+ {showTron && ( +

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

+ )} + {!showTron && ( +

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

+ )}
)} {/* Not-installed wallets — show install links */} - {!detecting && !compact && notInstalledWallets.length > 0 && ( + {!compact && notInstalledWallets.length > 0 && (

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

{notInstalledWallets.slice(0, 4).map(wallet => ( - {showManual + {showManual ? (lang === "zh" ? "收起手动输入" : "Hide manual input") : (lang === "zh" ? "手动输入钱包地址" : "Enter address manually")} @@ -486,16 +837,16 @@ export function WalletSelector({ lang, onAddressDetected, connectedAddress, comp {showManual && (

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

{ setManualAddress(e.target.value); setManualError(null); }} - placeholder={lang === "zh" ? "0x..." : "0x..."} + 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)", diff --git a/client/src/hooks/useWallet.ts b/client/src/hooks/useWallet.ts index b352ae8..4a33cac 100644 --- a/client/src/hooks/useWallet.ts +++ b/client/src/hooks/useWallet.ts @@ -1,6 +1,6 @@ // NAC XIC Presale — Wallet Connection Hook // Supports MetaMask, Trust Wallet, OKX Wallet, Coinbase Wallet, and all EVM-compatible wallets -// Robust auto-detect with retry, multi-provider support, and graceful fallback +// v3: improved error handling, MetaMask initialization detection, toast notifications import { useState, useEffect, useCallback, useRef } from "react"; import { BrowserProvider, JsonRpcSigner, Eip1193Provider } from "ethers"; @@ -31,12 +31,17 @@ const INITIAL_STATE: WalletState = { }; // Detect the best available EVM provider across all major wallets -function detectProvider(): Eip1193Provider | null { +export function detectProvider(): Eip1193Provider | null { if (typeof window === "undefined") return null; - // Check for multiple injected providers (e.g., MetaMask + Coinbase both installed) const w = window as unknown as Record; - const eth = w.ethereum as (Eip1193Provider & { providers?: Eip1193Provider[]; isMetaMask?: boolean; isTrust?: boolean; isOKExWallet?: boolean; isCoinbaseWallet?: boolean }) | undefined; + const eth = w.ethereum as (Eip1193Provider & { + providers?: Eip1193Provider[]; + isMetaMask?: boolean; + isTrust?: boolean; + isOKExWallet?: boolean; + isCoinbaseWallet?: boolean; + }) | undefined; if (!eth) { // Fallback: check wallet-specific globals @@ -47,7 +52,6 @@ function detectProvider(): Eip1193Provider | null { // If multiple providers are injected (common when multiple extensions installed) if (eth.providers && Array.isArray(eth.providers) && eth.providers.length > 0) { - // Prefer MetaMask if available, otherwise use first provider const metamask = eth.providers.find((p: Eip1193Provider & { isMetaMask?: boolean }) => p.isMetaMask); return metamask ?? eth.providers[0]; } @@ -55,6 +59,26 @@ function detectProvider(): Eip1193Provider | null { return eth; } +// Check if MetaMask is installed but not yet initialized (no wallet created/imported) +export async function checkWalletReady(rawProvider: Eip1193Provider): Promise<{ ready: boolean; reason?: string }> { + try { + // eth_accounts is silent — if it returns empty array, wallet is installed but locked or not initialized + const accounts = await (rawProvider as { request: (args: { method: string }) => Promise }).request({ + method: "eth_accounts", + }); + // If we get here, the wallet is at least initialized (even if locked / no accounts) + return { ready: true }; + } catch (err: unknown) { + const error = err as { code?: number; message?: string }; + // -32002: Request already pending (MetaMask not initialized or another request pending) + if (error?.code === -32002) { + return { ready: false, reason: "pending" }; + } + // Any other error — treat as not ready + return { ready: false, reason: error?.message || "unknown" }; + } +} + // Build wallet state from a provider and accounts async function buildWalletState( rawProvider: Eip1193Provider, @@ -68,7 +92,6 @@ async function buildWalletState( const network = await provider.getNetwork(); chainId = Number(network.chainId); } catch { - // Some wallets don't support getNetwork immediately — try eth_chainId directly try { const chainHex = await (rawProvider as { request: (args: { method: string }) => Promise }).request({ method: "eth_chainId" }); chainId = parseInt(chainHex, 16); @@ -80,7 +103,6 @@ async function buildWalletState( try { signer = await provider.getSigner(); } catch { - // getSigner may fail on some wallets before full connection — that's OK signer = null; } @@ -110,25 +132,58 @@ export function useWallet() { }, []); // ── Connect (explicit user action) ───────────────────────────────────────── - const connect = useCallback(async () => { + const connect = useCallback(async (): Promise<{ success: boolean; error?: string }> => { const rawProvider = detectProvider(); + if (!rawProvider) { - setState(s => ({ ...s, error: "请安装 MetaMask 或其他 EVM 兼容钱包 / Please install MetaMask or a compatible wallet." })); - return; + const msg = "未检测到钱包插件。请安装 MetaMask 或其他 EVM 兼容钱包后刷新页面。"; + if (mountedRef.current) setState(s => ({ ...s, error: msg })); + return { success: false, error: msg }; } + setState(s => ({ ...s, isConnecting: true, error: null })); + try { // Request accounts — this triggers the wallet popup - const accounts = await (rawProvider as { request: (args: { method: string; params?: unknown[] }) => Promise }).request({ + const accounts = await (rawProvider as { + request: (args: { method: string; params?: unknown[] }) => Promise + }).request({ method: "eth_requestAccounts", params: [], }); - if (!accounts || accounts.length === 0) throw new Error("No accounts returned"); + + if (!accounts || accounts.length === 0) { + throw new Error("no_accounts"); + } + const partial = await buildWalletState(rawProvider, accounts[0]); if (mountedRef.current) setState({ ...INITIAL_STATE, ...partial }); + return { success: true }; + } catch (err: unknown) { - const msg = (err as Error).message || "Failed to connect wallet"; + const error = err as { code?: number; message?: string }; + let msg: string; + + if (error?.code === 4001) { + // User rejected + msg = "已取消连接 / Connection cancelled"; + } else if (error?.code === -32002) { + // MetaMask has a pending request — usually means it's not initialized or popup is already open + msg = "钱包请求处理中,请检查 MetaMask 弹窗。如未弹出,请先完成 MetaMask 初始化设置(创建或导入钱包),然后刷新页面重试。"; + } else if (error?.message === "no_accounts") { + msg = "未获取到账户,请确认钱包已解锁并授权此网站。"; + } else if ( + error?.message?.toLowerCase().includes("not initialized") || + error?.message?.toLowerCase().includes("setup") || + error?.message?.toLowerCase().includes("onboarding") + ) { + msg = "MetaMask 尚未完成初始化。请先打开 MetaMask 扩展,创建或导入钱包,然后刷新页面重试。"; + } else { + msg = `连接失败: ${error?.message || "未知错误"}。请刷新页面重试。`; + } + if (mountedRef.current) setState(s => ({ ...s, isConnecting: false, error: msg })); + return { success: false, error: msg }; } }, []); @@ -171,7 +226,6 @@ export function useWallet() { const rawProvider = detectProvider(); if (!rawProvider) { - // Wallet extension may not be injected yet — retry up to 3 times if (attempt < 3) { retryRef.current = setTimeout(() => tryAutoDetect(attempt + 1), 800 * attempt); } @@ -189,7 +243,6 @@ export function useWallet() { setState({ ...INITIAL_STATE, ...partial }); } } else if (attempt < 3) { - // Accounts empty — wallet might not have finished loading, retry retryRef.current = setTimeout(() => tryAutoDetect(attempt + 1), 1000 * attempt); } } catch { @@ -197,7 +250,6 @@ export function useWallet() { } }; - // Small initial delay to let wallet extensions inject themselves retryRef.current = setTimeout(() => tryAutoDetect(1), 300); return () => { @@ -223,7 +275,6 @@ export function useWallet() { if (!accs || accs.length === 0) { setState(INITIAL_STATE); } else { - // Re-build full state with new address try { const partial = await buildWalletState(rawProvider, accs[0]); if (mountedRef.current) setState({ ...INITIAL_STATE, ...partial }); @@ -242,7 +293,6 @@ export function useWallet() { const handleChainChanged = async () => { if (!mountedRef.current) return; - // Re-fetch network info instead of reloading the page try { const provider = new BrowserProvider(rawProvider); const network = await provider.getNetwork(); @@ -257,7 +307,6 @@ export function useWallet() { })); } } catch { - // If we can't get network info, reload as last resort window.location.reload(); } }; diff --git a/client/src/pages/Home.tsx b/client/src/pages/Home.tsx index cadafee..c2a86f7 100644 --- a/client/src/pages/Home.tsx +++ b/client/src/pages/Home.tsx @@ -112,48 +112,8 @@ function TRC20Panel({ usdtAmount, lang, connectedAddress, onConnectWallet }: { u const [evmAddress, setEvmAddress] = useState(connectedAddress || ""); const [evmAddrError, setEvmAddrError] = useState(""); const [submitted, setSubmitted] = useState(false); - // TronLink detection state + // TronLink detection state — now handled by WalletSelector(showTron=true) const [tronAddress, setTronAddress] = useState(null); - const [isTronConnecting, setIsTronConnecting] = useState(false); - const hasTronLink = typeof window !== "undefined" && (!!(window as unknown as Record).tronWeb || !!(window as unknown as Record).tronLink); - - // Auto-detect TronLink on mount - useEffect(() => { - const detectTron = async () => { - // Wait briefly for TronLink to inject - await new Promise(resolve => setTimeout(resolve, 500)); - const tronWeb = (window as unknown as Record).tronWeb as { defaultAddress?: { base58?: string }; ready?: boolean } | undefined; - if (tronWeb && tronWeb.ready && tronWeb.defaultAddress?.base58) { - setTronAddress(tronWeb.defaultAddress.base58); - } - }; - detectTron(); - }, []); - - // Connect TronLink wallet - const handleConnectTronLink = async () => { - setIsTronConnecting(true); - try { - const tronLink = (window as unknown as Record).tronLink as { request?: (args: { method: string }) => Promise<{ code: number }> } | undefined; - const tronWeb = (window as unknown as Record).tronWeb as { defaultAddress?: { base58?: string }; ready?: boolean } | undefined; - if (tronLink?.request) { - const result = await tronLink.request({ method: 'tron_requestAccounts' }); - if (result?.code === 200 && tronWeb?.defaultAddress?.base58) { - setTronAddress(tronWeb.defaultAddress.base58); - toast.success(lang === "zh" ? "TronLink已连接!" : "TronLink connected!"); - } - } else if (tronWeb?.ready && tronWeb.defaultAddress?.base58) { - setTronAddress(tronWeb.defaultAddress.base58); - toast.success(lang === "zh" ? "TronLink已检测到!" : "TronLink detected!"); - } else { - toast.error(lang === "zh" ? "请安装TronLink钱包扩展" : "Please install TronLink wallet extension"); - } - } catch { - toast.error(lang === "zh" ? "连接TronLink失败" : "Failed to connect TronLink"); - } finally { - setIsTronConnecting(false); - } - }; // Auto-fill EVM address whenever wallet connects or address changes (unless user already submitted) useEffect(() => { @@ -252,7 +212,7 @@ function TRC20Panel({ usdtAmount, lang, connectedAddress, onConnectWallet }: { u
- {/* TronLink Wallet Detection */} + {/* TronLink Wallet Detection — using unified WalletSelector with showTron=true */}
@@ -261,7 +221,7 @@ function TRC20Panel({ usdtAmount, lang, connectedAddress, onConnectWallet }: { u

- {lang === "zh" ? "TronLink 钱包(可选)" : "TronLink Wallet (Optional)"} + {lang === "zh" ? "连接 TronLink 钱包(可选)" : "Connect TronLink Wallet (Optional)"}

{tronAddress ? ( @@ -283,45 +243,28 @@ function TRC20Panel({ usdtAmount, lang, connectedAddress, onConnectWallet }: { u
) : (
-

+

{lang === "zh" - ? "如果您使用 TronLink 钱包,可以连接后自动验证您的 TRON 地址。" - : "If you use TronLink wallet, connect to auto-verify your TRON address."} + ? "连接 TronLink 可自动验证您的 TRON 地址。手机用户可通过 TronLink App 内置浏览器打开本页面。" + : "Connect TronLink to auto-verify your TRON address. Mobile users can open this page in TronLink App's built-in browser."}

- {hasTronLink ? ( - - ) : ( -
- {lang === "zh" ? "安装 TronLink 钱包 →" : "Install TronLink Wallet →"} - - )} + { + if (network === "tron") { + setTronAddress(addr); + toast.success(lang === "zh" ? "TronLink 已连接!" : "TronLink connected!"); + } else { + // EVM address detected in TRC20 panel — use as XIC receiving address + setEvmAddress(addr); + setEvmAddrError(""); + toast.success(lang === "zh" ? "XIC接收地址已自动填充!" : "XIC receiving address auto-filled!"); + if (onConnectWallet) onConnectWallet(); + } + }} + />
)}
@@ -421,8 +364,8 @@ function EVMPurchasePanel({ network, lang, wallet }: { network: "BSC" | "ETH"; l lang={lang} connectedAddress={wallet.address ?? undefined} onAddressDetected={(addr) => { - // Address detected — wallet is now connected, trigger wallet.connect to sync state - wallet.connect(); + // WalletSelector already called eth_requestAccounts and got the address + // Just show success toast; wallet state will auto-update via accountsChanged event toast.success(lang === "zh" ? `已连接: ${addr.slice(0, 6)}...${addr.slice(-4)}` : `Connected: ${addr.slice(0, 6)}...${addr.slice(-4)}`); }} compact @@ -814,6 +757,7 @@ type WalletHookReturn = ReturnType; function NavWalletButton({ lang, wallet }: { lang: Lang; wallet: WalletHookReturn }) { const { t } = useTranslation(lang); const [showMenu, setShowMenu] = useState(false); + const [showWalletModal, setShowWalletModal] = useState(false); const menuRef = useRef(null); useEffect(() => { @@ -824,21 +768,105 @@ function NavWalletButton({ lang, wallet }: { lang: Lang; wallet: WalletHookRetur return () => document.removeEventListener("mousedown", handleClick); }, []); + // Detect mobile browser + const isMobile = typeof window !== "undefined" && /Android|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(navigator.userAgent); + + // Handle connect button click — show wallet selector modal + const handleConnectClick = async () => { + // On mobile browsers, skip direct connect attempt and show modal immediately + // (mobile browsers don't support wallet extensions) + if (isMobile) { + setShowWalletModal(true); + return; + } + // On desktop: first try direct connect (works if wallet is already set up and locked) + const result = await wallet.connect(); + if (!result.success && result.error) { + // If direct connect failed, show the wallet selector modal for guided setup + setShowWalletModal(true); + toast.error(result.error, { duration: 6000 }); + } + }; + if (!wallet.isConnected) { return ( - + <> + + + {/* Wallet Connection Modal */} + {showWalletModal && ( +
{ if (e.target === e.currentTarget) setShowWalletModal(false); }} + > +
+ {/* Close button */} + + +

+ {lang === "zh" ? "连接钱包" : "Connect Wallet"} +

+

+ {lang === "zh" + ? "选择您的钱包进行连接,或手动输入地址" + : "Select your wallet to connect, or enter address manually"} +

+ + {/* MetaMask initialization guide */} +
+

+ {lang === "zh" + ? "💡 首次使用 MetaMask?请先打开 MetaMask 扩展完成初始化(创建或导入钱包),完成后点击下方「刷新」按钮重新检测。" + : "💡 First time using MetaMask? Open the MetaMask extension and complete setup (create or import a wallet), then click Refresh below to re-detect."} +

+
+ + { + // After address detected from WalletSelector, sync wallet state + const result = await wallet.connect(); + if (result.success) { + setShowWalletModal(false); + toast.success(lang === "zh" ? `钱包已连接: ${addr.slice(0, 6)}...${addr.slice(-4)}` : `Wallet connected: ${addr.slice(0, 6)}...${addr.slice(-4)}`); + } else { + // Even if connect() failed, we have the address — close modal + setShowWalletModal(false); + toast.success(lang === "zh" ? `地址已确认: ${addr.slice(0, 6)}...${addr.slice(-4)}` : `Address confirmed: ${addr.slice(0, 6)}...${addr.slice(-4)}`); + } + }} + /> +
+
+ )} + ); }