diff --git a/client/src/components/WalletSelector.tsx b/client/src/components/WalletSelector.tsx new file mode 100644 index 0000000..7c7dd70 --- /dev/null +++ b/client/src/components/WalletSelector.tsx @@ -0,0 +1,372 @@ +// NAC XIC Presale — WalletSelector Component +// Detects installed EVM wallets and shows connect/install buttons for each + +import { useState, useEffect } from "react"; + +type Lang = "zh" | "en"; + +interface WalletInfo { + id: string; + name: string; + icon: React.ReactNode; + installUrl: string; + isInstalled: () => boolean; + connect: () => Promise; +} + +interface WalletSelectorProps { + lang: Lang; + onAddressDetected: (address: string) => void; + connectedAddress?: string; + compact?: boolean; // compact mode for BSC/ETH panel +} + +// ── Wallet Icons ────────────────────────────────────────────────────────────── + +const MetaMaskIcon = () => ( + + + + + + + + + + +); + +const TrustWalletIcon = () => ( + + + + + +); + +const OKXIcon = () => ( + + + + + + + + +); + +const CoinbaseIcon = () => ( + + + + + +); + +const TokenPocketIcon = () => ( + + + + + +); + +const BitgetIcon = () => ( + + + + + +); + +// ── Provider detection helpers ──────────────────────────────────────────────── + +type EthProvider = { + isMetaMask?: boolean; + isTrust?: boolean; + isTrustWallet?: boolean; + isOKExWallet?: boolean; + isOkxWallet?: boolean; + isCoinbaseWallet?: boolean; + isTokenPocket?: boolean; + isBitkeep?: boolean; + isBitgetWallet?: boolean; + providers?: EthProvider[]; + 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; +} + +function getOKX(): EthProvider | null { + if (typeof window === "undefined") return null; + return (window as unknown as { okxwallet?: EthProvider }).okxwallet ?? null; +} + +function getBitget(): EthProvider | null { + if (typeof window === "undefined") return null; + const w = window as unknown as { bitkeep?: { ethereum?: EthProvider } }; + return w.bitkeep?.ethereum ?? 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; + if (eth.providers && Array.isArray(eth.providers)) { + return eth.providers.find(predicate) ?? null; + } + return predicate(eth) ? eth : null; +} + +async function requestAccounts(provider: EthProvider): Promise { + try { + const accounts = await provider.request({ method: "eth_requestAccounts" }) as string[]; + return accounts?.[0] ?? null; + } catch { + return null; + } +} + +// ── Wallet definitions ──────────────────────────────────────────────────────── + +function buildWallets(): WalletInfo[] { + return [ + { + id: "metamask", + name: "MetaMask", + icon: , + installUrl: "https://metamask.io/download/", + isInstalled: () => !!findProvider(p => !!p.isMetaMask), + connect: async () => { + const p = findProvider(p => !!p.isMetaMask) ?? getEth(); + return p ? requestAccounts(p) : null; + }, + }, + { + id: "trust", + name: "Trust Wallet", + icon: , + installUrl: "https://trustwallet.com/download", + isInstalled: () => !!findProvider(p => !!(p.isTrust || p.isTrustWallet)), + connect: async () => { + const p = findProvider(p => !!(p.isTrust || p.isTrustWallet)) ?? getEth(); + return p ? requestAccounts(p) : null; + }, + }, + { + id: "okx", + name: "OKX Wallet", + icon: , + installUrl: "https://www.okx.com/web3", + isInstalled: () => !!(getOKX() || findProvider(p => !!(p.isOKExWallet || p.isOkxWallet))), + connect: async () => { + const p = getOKX() ?? findProvider(p => !!(p.isOKExWallet || p.isOkxWallet)); + return p ? requestAccounts(p) : null; + }, + }, + { + id: "coinbase", + name: "Coinbase Wallet", + icon: , + installUrl: "https://www.coinbase.com/wallet/downloads", + isInstalled: () => !!findProvider(p => !!p.isCoinbaseWallet), + connect: async () => { + const p = findProvider(p => !!p.isCoinbaseWallet) ?? getEth(); + return p ? requestAccounts(p) : null; + }, + }, + { + id: "tokenpocket", + name: "TokenPocket", + icon: , + installUrl: "https://www.tokenpocket.pro/en/download/app", + isInstalled: () => !!findProvider(p => !!p.isTokenPocket), + connect: async () => { + const p = findProvider(p => !!p.isTokenPocket) ?? getEth(); + return p ? requestAccounts(p) : null; + }, + }, + { + id: "bitget", + name: "Bitget Wallet", + icon: , + installUrl: "https://web3.bitget.com/en/wallet-download", + isInstalled: () => !!(getBitget() || findProvider(p => !!(p.isBitkeep || p.isBitgetWallet))), + connect: async () => { + const p = getBitget() ?? findProvider(p => !!(p.isBitkeep || p.isBitgetWallet)); + return p ? requestAccounts(p) : null; + }, + }, + ]; +} + +// ── WalletSelector Component ────────────────────────────────────────────────── + +export function WalletSelector({ lang, onAddressDetected, connectedAddress, compact = false }: WalletSelectorProps) { + const [wallets, setWallets] = useState([]); + const [connecting, setConnecting] = useState(null); + const [error, setError] = useState(null); + + useEffect(() => { + // Build wallet list after a short delay to allow extensions to inject + const timer = setTimeout(() => { + setWallets(buildWallets()); + }, 400); + return () => clearTimeout(timer); + }, []); + + const handleConnect = async (wallet: WalletInfo) => { + setConnecting(wallet.id); + setError(null); + try { + const address = await wallet.connect(); + if (address) { + onAddressDetected(address); + } else { + setError(lang === "zh" ? "未获取到地址,请重试" : "No address returned, please try again"); + } + } catch { + setError(lang === "zh" ? "连接失败,请重试" : "Connection failed, please try again"); + } finally { + setConnecting(null); + } + }; + + const installedWallets = wallets.filter(w => w.isInstalled()); + const notInstalledWallets = wallets.filter(w => !w.isInstalled()); + + // If connected address is already set, show compact confirmation + if (connectedAddress) { + return ( +
+
+
+

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

+

{connectedAddress}

+
+
+ ); + } + + return ( +
+

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

+ + {/* Installed wallets */} + {installedWallets.length > 0 && ( +
+ {installedWallets.map(wallet => ( + + ))} +
+ )} + + {/* No wallets installed */} + {installedWallets.length === 0 && ( +
+

+ {lang === "zh" ? "未检测到 EVM 钱包" : "No EVM wallet detected"} +

+

+ {lang === "zh" ? "请安装以下任一钱包后刷新页面" : "Install any wallet below and refresh the page"} +

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

+ {lang === "zh" ? "未安装(点击安装)" : "Not installed (click to install)"} +

+
+ {notInstalledWallets.map(wallet => ( + + {wallet.icon} + {wallet.name} + + ))} +
+
+ )} + + {/* In compact mode, show install links inline */} + {compact && notInstalledWallets.length > 0 && installedWallets.length === 0 && ( +
+ {notInstalledWallets.slice(0, 4).map(wallet => ( + + {wallet.icon} + {lang === "zh" ? `安装 ${wallet.name}` : `Install ${wallet.name}`} + + ))} +
+ )} + + {error && ( +

{error}

+ )} +
+ ); +} diff --git a/client/src/hooks/useWallet.ts b/client/src/hooks/useWallet.ts index 55abbc0..b352ae8 100644 --- a/client/src/hooks/useWallet.ts +++ b/client/src/hooks/useWallet.ts @@ -1,7 +1,8 @@ // NAC XIC Presale — Wallet Connection Hook -// Supports MetaMask / any EVM-compatible wallet (BSC + ETH) +// Supports MetaMask, Trust Wallet, OKX Wallet, Coinbase Wallet, and all EVM-compatible wallets +// Robust auto-detect with retry, multi-provider support, and graceful fallback -import { useState, useEffect, useCallback } from "react"; +import { useState, useEffect, useCallback, useRef } from "react"; import { BrowserProvider, JsonRpcSigner, Eip1193Provider } from "ethers"; import { shortenAddress, switchToNetwork } from "@/lib/contracts"; @@ -29,118 +30,246 @@ const INITIAL_STATE: WalletState = { error: null, }; +// Detect the best available EVM provider across all major wallets +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; + + if (!eth) { + // Fallback: check wallet-specific globals + if (w.okxwallet) return w.okxwallet as Eip1193Provider; + if (w.coinbaseWalletExtension) return w.coinbaseWalletExtension as Eip1193Provider; + return 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]; + } + + return eth; +} + +// Build wallet state from a provider and accounts +async function buildWalletState( + rawProvider: Eip1193Provider, + address: string +): Promise> { + const provider = new BrowserProvider(rawProvider); + let chainId: number | null = null; + let signer: JsonRpcSigner | null = null; + + try { + 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); + } catch { + chainId = null; + } + } + + try { + signer = await provider.getSigner(); + } catch { + // getSigner may fail on some wallets before full connection — that's OK + signer = null; + } + + return { + address, + shortAddress: shortenAddress(address), + isConnected: true, + chainId, + provider, + signer, + isConnecting: false, + error: null, + }; +} + export function useWallet() { const [state, setState] = useState(INITIAL_STATE); + const retryRef = useRef | null>(null); + const mountedRef = useRef(true); + useEffect(() => { + mountedRef.current = true; + return () => { + mountedRef.current = false; + if (retryRef.current) clearTimeout(retryRef.current); + }; + }, []); + + // ── Connect (explicit user action) ───────────────────────────────────────── const connect = useCallback(async () => { - if (!window.ethereum) { - setState(s => ({ ...s, error: "Please install MetaMask or a compatible wallet." })); + const rawProvider = detectProvider(); + if (!rawProvider) { + setState(s => ({ ...s, error: "请安装 MetaMask 或其他 EVM 兼容钱包 / Please install MetaMask or a compatible wallet." })); return; } setState(s => ({ ...s, isConnecting: true, error: null })); try { - const provider = new BrowserProvider(window.ethereum as Eip1193Provider); - const accounts = await provider.send("eth_requestAccounts", []); - const network = await provider.getNetwork(); - const signer = await provider.getSigner(); - const address = accounts[0] as string; - setState({ - address, - shortAddress: shortenAddress(address), - isConnected: true, - chainId: Number(network.chainId), - provider, - signer, - isConnecting: false, - error: null, + // Request accounts — this triggers the wallet popup + 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"); + const partial = await buildWalletState(rawProvider, accounts[0]); + if (mountedRef.current) setState({ ...INITIAL_STATE, ...partial }); } catch (err: unknown) { - setState(s => ({ - ...s, - isConnecting: false, - error: (err as Error).message || "Failed to connect wallet", - })); + const msg = (err as Error).message || "Failed to connect wallet"; + if (mountedRef.current) setState(s => ({ ...s, isConnecting: false, error: msg })); } }, []); + // ── Disconnect ────────────────────────────────────────────────────────────── const disconnect = useCallback(() => { setState(INITIAL_STATE); }, []); + // ── Switch Network ────────────────────────────────────────────────────────── const switchNetwork = useCallback(async (chainId: number) => { try { await switchToNetwork(chainId); - if (window.ethereum) { - const provider = new BrowserProvider(window.ethereum as Eip1193Provider); + const rawProvider = detectProvider(); + if (rawProvider) { + const provider = new BrowserProvider(rawProvider); const network = await provider.getNetwork(); - const signer = await provider.getSigner(); - setState(s => ({ - ...s, - chainId: Number(network.chainId), - provider, - signer, - error: null, - })); - } - } catch (err: unknown) { - setState(s => ({ ...s, error: (err as Error).message })); - } - }, []); - - // Auto-detect already-connected wallet on page load (silent, no popup) - useEffect(() => { - const autoDetect = async () => { - if (!window.ethereum) return; - try { - const provider = new BrowserProvider(window.ethereum as Eip1193Provider); - // Use eth_accounts (not eth_requestAccounts) — silent, no popup - const accounts = await provider.send("eth_accounts", []); - if (accounts && accounts.length > 0) { - const network = await provider.getNetwork(); - const signer = await provider.getSigner(); - const address = accounts[0] as string; - setState({ - address, - shortAddress: shortenAddress(address), - isConnected: true, + let signer: JsonRpcSigner | null = null; + try { signer = await provider.getSigner(); } catch { /* ignore */ } + if (mountedRef.current) { + setState(s => ({ + ...s, chainId: Number(network.chainId), provider, signer, - isConnecting: false, error: null, - }); + })); + } + } + } catch (err: unknown) { + if (mountedRef.current) setState(s => ({ ...s, error: (err as Error).message })); + } + }, []); + + // ── Auto-detect on page load (silent, no popup) ───────────────────────────── + useEffect(() => { + let cancelled = false; + + const tryAutoDetect = async (attempt: number) => { + if (cancelled) return; + + 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); + } + return; + } + + try { + const accounts = await (rawProvider as { request: (args: { method: string }) => Promise }).request({ + method: "eth_accounts", // Silent — no popup + }); + if (cancelled) return; + if (accounts && accounts.length > 0) { + const partial = await buildWalletState(rawProvider, accounts[0]); + if (!cancelled && mountedRef.current) { + 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 { // Silently ignore — user hasn't connected yet } }; - autoDetect(); + + // Small initial delay to let wallet extensions inject themselves + retryRef.current = setTimeout(() => tryAutoDetect(1), 300); + + return () => { + cancelled = true; + if (retryRef.current) clearTimeout(retryRef.current); + }; }, []); - // Listen for account/chain changes + // ── Listen for account / chain changes ───────────────────────────────────── useEffect(() => { - if (!window.ethereum) return; - const handleAccountsChanged = (accounts: unknown) => { + const rawProvider = detectProvider(); + if (!rawProvider) return; + + const eth = rawProvider as { + on?: (event: string, handler: (data: unknown) => void) => void; + removeListener?: (event: string, handler: (data: unknown) => void) => void; + }; + if (!eth.on) return; + + const handleAccountsChanged = async (accounts: unknown) => { const accs = accounts as string[]; - if (accs.length === 0) { + if (!mountedRef.current) return; + if (!accs || accs.length === 0) { setState(INITIAL_STATE); } else { - setState(s => ({ - ...s, - address: accs[0], - shortAddress: shortenAddress(accs[0]), - isConnected: true, - })); + // Re-build full state with new address + try { + const partial = await buildWalletState(rawProvider, accs[0]); + if (mountedRef.current) setState({ ...INITIAL_STATE, ...partial }); + } catch { + if (mountedRef.current) { + setState(s => ({ + ...s, + address: accs[0], + shortAddress: shortenAddress(accs[0]), + isConnected: true, + })); + } + } } }; - const handleChainChanged = () => { - window.location.reload(); + + 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(); + let signer: JsonRpcSigner | null = null; + try { signer = await provider.getSigner(); } catch { /* ignore */ } + if (mountedRef.current) { + setState(s => ({ + ...s, + chainId: Number(network.chainId), + provider, + signer, + })); + } + } catch { + // If we can't get network info, reload as last resort + window.location.reload(); + } }; - window.ethereum.on("accountsChanged", handleAccountsChanged); - window.ethereum.on("chainChanged", handleChainChanged); + + eth.on("accountsChanged", handleAccountsChanged); + eth.on("chainChanged", handleChainChanged); + return () => { - window.ethereum?.removeListener("accountsChanged", handleAccountsChanged); - window.ethereum?.removeListener("chainChanged", handleChainChanged); + if (eth.removeListener) { + eth.removeListener("accountsChanged", handleAccountsChanged); + eth.removeListener("chainChanged", handleChainChanged); + } }; }, []); diff --git a/client/src/pages/Admin.tsx b/client/src/pages/Admin.tsx index 5772cd8..aa190ec 100644 --- a/client/src/pages/Admin.tsx +++ b/client/src/pages/Admin.tsx @@ -130,19 +130,31 @@ function SettingsPanel({ token }: { token: string }) { const [telegramStatus, setTelegramStatus] = useState<"idle" | "testing" | "success" | "error">("idle"); const [telegramError, setTelegramError] = useState(""); + // ── Presale Active/Paused Toggle ────────────────────────────────────────── + const isPresaleLive = (configData?.find(c => c.key === "presaleStatus")?.value ?? "live") === "live"; + const [togglingPresale, setTogglingPresale] = useState(false); + const setConfigMutation = trpc.admin.setConfig.useMutation({ onSuccess: (_, vars) => { setSavedKeys(prev => { const s = new Set(Array.from(prev)); s.add(vars.key); return s; }); setSavingKey(null); + setTogglingPresale(false); refetchConfig(); setTimeout(() => setSavedKeys(prev => { const n = new Set(Array.from(prev)); n.delete(vars.key); return n; }), 2000); }, onError: (err) => { setSavingKey(null); + setTogglingPresale(false); alert(`Save failed: ${err.message}`); }, }); + const handleTogglePresale = () => { + const newStatus = isPresaleLive ? "paused" : "live"; + setTogglingPresale(true); + setConfigMutation.mutate({ token, key: "presaleStatus", value: newStatus }); + }; + const testTelegramMutation = trpc.admin.testTelegram.useMutation({ onSuccess: () => { setTelegramStatus("success"); @@ -269,6 +281,66 @@ function SettingsPanel({ token }: { token: string }) { return (
+ {/* ── Presale Active Toggle ── */} +
+
+
+
+
+

+ {isPresaleLive ? "预售进行中 PRESALE LIVE" : "预售已暂停 PRESALE PAUSED"} +

+
+

+ {isPresaleLive + ? "用户当前可正常购买 XIC 代币。点击《暂停预售》可立即封禁所有购买入口。" + : "预售已暂停,首页购买按钮已禁用。点击《开启预售》可重新开放购买。"} +

+
+ +
+
+ {/* Presale Parameters */}

Presale Parameters 预售参数

diff --git a/client/src/pages/Home.tsx b/client/src/pages/Home.tsx index 13425c3..cadafee 100644 --- a/client/src/pages/Home.tsx +++ b/client/src/pages/Home.tsx @@ -11,6 +11,7 @@ import { usePresale } from "@/hooks/usePresale"; import { CONTRACTS, PRESALE_CONFIG, formatNumber, shortenAddress } from "@/lib/contracts"; import { trpc } from "@/lib/trpc"; import { type Lang, useTranslation } from "@/lib/i18n"; +import { WalletSelector } from "@/components/WalletSelector"; // ─── Network Tab Types ──────────────────────────────────────────────────────── type NetworkTab = "BSC" | "ETH" | "TRON"; @@ -111,9 +112,6 @@ function TRC20Panel({ usdtAmount, lang, connectedAddress, onConnectWallet }: { u const [evmAddress, setEvmAddress] = useState(connectedAddress || ""); const [evmAddrError, setEvmAddrError] = useState(""); const [submitted, setSubmitted] = useState(false); - const [isAutoConnecting, setIsAutoConnecting] = useState(false); - const hasEthereum = typeof window !== "undefined" && !!window.ethereum; - // TronLink detection state const [tronAddress, setTronAddress] = useState(null); const [isTronConnecting, setIsTronConnecting] = useState(false); @@ -164,30 +162,10 @@ function TRC20Panel({ usdtAmount, lang, connectedAddress, onConnectWallet }: { u } }, [connectedAddress, submitted]); - // Auto-connect EVM wallet from within TRC20 panel - const handleAutoConnectEVM = async () => { - if (!window.ethereum) return; - setIsAutoConnecting(true); - try { - const accounts = await window.ethereum.request({ method: "eth_requestAccounts" }) as string[]; - if (accounts && accounts.length > 0 && !submitted) { - setEvmAddress(accounts[0]); - setEvmAddrError(""); - toast.success(lang === "zh" ? "EVM地址已自动填充!" : "EVM address auto-filled!"); - } - // Also notify parent to update wallet state - if (onConnectWallet) onConnectWallet(); - } catch { - // User rejected or error — silently ignore - } finally { - setIsAutoConnecting(false); - } - }; - const submitTrc20Mutation = trpc.presale.registerTrc20Intent.useMutation({ onSuccess: () => { setSubmitted(true); - toast.success(lang === "zh" ? "EVM地址已保存!" : "EVM address saved!"); + toast.success(lang === "zh" ? "XIC接收地址已保存!" : "XIC receiving address saved!"); }, onError: (err: { message: string }) => { toast.error(err.message); @@ -202,8 +180,8 @@ function TRC20Panel({ usdtAmount, lang, connectedAddress, onConnectWallet }: { u }; const validateEvmAddress = (addr: string) => { - if (!addr) return lang === "zh" ? "请输入您的EVM地址" : "Please enter your EVM address"; - if (!/^0x[0-9a-fA-F]{40}$/.test(addr)) return lang === "zh" ? "无效的EVM地址格式(应以0x开头,共42位)" : "Invalid EVM address format (must start with 0x, 42 chars)"; + if (!addr) return lang === "zh" ? "请输入您的XIC接收地址" : "Please enter your XIC receiving address"; + if (!/^0x[0-9a-fA-F]{40}$/.test(addr)) return lang === "zh" ? "无效的XIC接收地址格式(应以0x开头,入42位)" : "Invalid XIC receiving address format (must start with 0x, 42 chars)"; return ""; }; @@ -221,42 +199,33 @@ function TRC20Panel({ usdtAmount, lang, connectedAddress, onConnectWallet }: { u
⚠️

- {lang === "zh" ? "必填:您的EVM钱包地址(用于接收XIC代币)" : "Required: Your EVM Wallet Address (to receive XIC tokens)"} + {lang === "zh" ? "必填:您的XIC接收地址(BSC/ETH钉包地址)" : "Required: Your XIC Receiving Address (BSC/ETH wallet address)"}

{lang === "zh" - ? "XIC代币在BSC网络上发放。请提供您的BSC/ETH地址(0x开头),以便我们将代币发送给您。" - : "XIC tokens are distributed on BSC. Please provide your BSC/ETH address (starts with 0x) so we can send your tokens."} + ? "XIC代币将发放到您的BSC/ETH钉包地址(0x开头)。请确保填写正确的地址,否则无法收到代币。" + : "XIC tokens will be sent to your BSC/ETH wallet address (starts with 0x). Please make sure to enter the correct address."}

- {/* Auto-connect button — shown when MetaMask is available but address not yet filled */} - {hasEthereum && !connectedAddress && !evmAddress && ( - + /> )} { setEvmAddress(e.target.value); setEvmAddrError(""); setSubmitted(false); }} - placeholder={lang === "zh" ? "0x... (您的BSC/ETH地址)" : "0x... (your BSC/ETH address)"} + placeholder={lang === "zh" ? "0x... (您的XIC接收地址)" : "0x... (your XIC receiving address)"} className="w-full px-4 py-3 rounded-xl text-sm font-mono" style={{ background: "rgba(255,255,255,0.05)", @@ -266,7 +235,7 @@ function TRC20Panel({ usdtAmount, lang, connectedAddress, onConnectWallet }: { u }} /> {evmAddrError &&

{evmAddrError}

} - {submitted &&

✓ {lang === "zh" ? "EVM地址已保存" : "EVM address saved"}

} + {submitted &&

✓ {lang === "zh" ? "XIC接收地址已保存" : "XIC receiving address saved"}

}
@@ -308,8 +277,8 @@ function TRC20Panel({ usdtAmount, lang, connectedAddress, onConnectWallet }: { u

{lang === "zh" - ? "您的 TronLink 已连接。请在上方填写 EVM 地址以接收 XIC 代币,然后向下方地址发送 USDT。" - : "TronLink connected. Please fill your EVM address above to receive XIC tokens, then send USDT to the address below."} + ? "您的 TronLink 已连接。请在上方填写 XIC 接收地址,然后向下方地址发送 USDT。" + : "TronLink connected. Please fill your XIC receiving address above, then send USDT to the address below."}

) : ( @@ -378,8 +347,8 @@ function TRC20Panel({ usdtAmount, lang, connectedAddress, onConnectWallet }: { u
-
-
🔗
-

{t("buy_connect_msg")}

- -
+

{t("buy_connect_msg")}

+ { + // Address detected — wallet is now connected, trigger wallet.connect to sync state + wallet.connect(); + toast.success(lang === "zh" ? `已连接: ${addr.slice(0, 6)}...${addr.slice(-4)}` : `Connected: ${addr.slice(0, 6)}...${addr.slice(-4)}`); + }} + compact + />
{t("buy_connect_hint")}
); @@ -963,6 +931,8 @@ export default function Home() { const stats = onChainStats || FALLBACK_STATS; const progressPct = stats.progressPct || 0; + // Presale active/paused status from backend config + const isPresalePaused = (onChainStats as any)?.presaleStatus === "paused"; // 钱包状态提升到顶层,共享给NavWalletButton和EVMPurchasePanel const wallet = useWallet(); @@ -971,6 +941,21 @@ export default function Home() { return (
+ {/* ── Presale Paused Banner ── */} + {isPresalePaused && ( +
+ + {lang === "zh" ? "预售活动已暂停,暂时无法购买。请关注官方渠道获取最新公告。" : "Presale is currently paused. Please follow our official channels for updates."} + +
+ )} {/* ── Navigation ── */}