From a7aa132b71781d3f317e686e0907f44cea1854ee Mon Sep 17 00:00:00 2001 From: Manus Date: Tue, 10 Mar 2026 01:48:13 -0400 Subject: [PATCH] =?UTF-8?q?Checkpoint:=20=E4=BF=AE=E5=A4=8D=E6=89=8B?= =?UTF-8?q?=E6=9C=BA=E7=AB=AF=E9=92=B1=E5=8C=85=E8=BF=9E=E6=8E=A5=E5=BC=B9?= =?UTF-8?q?=E5=87=BA=E6=A1=86=E9=97=AE=E9=A2=98=EF=BC=9A=201.=20Modal=20?= =?UTF-8?q?=E6=94=B9=E4=B8=BA=E6=89=8B=E6=9C=BA=E7=AB=AF=E5=BA=95=E9=83=A8?= =?UTF-8?q?=E5=BC=B9=E5=87=BA=EF=BC=88items-end=EF=BC=89=EF=BC=8C=E5=8A=A0?= =?UTF-8?q?=20maxHeight=2085vh=20+=20overflow-y-auto=EF=BC=8C=E9=98=B2?= =?UTF-8?q?=E6=AD=A2=E5=86=85=E5=AE=B9=E8=B6=85=E5=87=BA=E5=B1=8F=E5=B9=95?= =?UTF-8?q?=202.=20MobileDeepLinkPanel=20=E7=B2=BE=E7=AE=80=EF=BC=9AMetaMa?= =?UTF-8?q?sk=20=E5=8D=95=E7=8B=AC=E7=AA=81=E5=87=BA=E6=98=BE=E7=A4=BA?= =?UTF-8?q?=E5=9C=A8=E6=9C=80=E4=B8=8A=E6=96=B9=EF=BC=8C=E5=85=B6=E4=BB=96?= =?UTF-8?q?=E9=92=B1=E5=8C=85=EF=BC=88Trust/OKX/TokenPocket=EF=BC=89?= =?UTF-8?q?=E9=BB=98=E8=AE=A4=E6=8A=98=E5=8F=A0=203.=20=E6=89=8B=E6=9C=BA?= =?UTF-8?q?=E7=AB=AF=E5=9C=A8=E9=92=B1=E5=8C=85=E5=86=85=E7=BD=AE=E6=B5=8F?= =?UTF-8?q?=E8=A7=88=E5=99=A8=E4=B8=AD=EF=BC=88window.ethereum=20=E5=B7=B2?= =?UTF-8?q?=E6=B3=A8=E5=85=A5=EF=BC=89=E6=97=B6=EF=BC=8C=E7=9B=B4=E6=8E=A5?= =?UTF-8?q?=E8=B0=83=E8=B5=B7=E8=BF=9E=E6=8E=A5=EF=BC=8C=E4=B8=8D=E5=BC=B9?= =?UTF-8?q?=E9=80=89=E6=8B=A9=E5=88=97=E8=A1=A8=204.=20=E6=89=8B=E6=9C=BA?= =?UTF-8?q?=E7=AB=AF=E9=9A=90=E8=97=8F=E6=A1=8C=E9=9D=A2=E4=B8=93=E7=94=A8?= =?UTF-8?q?=E7=9A=84=20MetaMask=20=E6=89=A9=E5=B1=95=E5=88=9D=E5=A7=8B?= =?UTF-8?q?=E5=8C=96=E6=8F=90=E7=A4=BA=20=E5=8F=A6=E5=A4=96=EF=BC=9A?= =?UTF-8?q?=E5=81=9C=E6=AD=A2=E4=BA=86=E6=97=A7=E9=A2=84=E5=94=AE=E5=90=88?= =?UTF-8?q?=E7=BA=A6=200xc65e7a2738ed884db8d26a6eb2fecf7daca2e90c=EF=BC=88?= =?UTF-8?q?=E8=B0=83=E7=94=A8=20endPresale()=EF=BC=8CTX:=200x286e35...?= =?UTF-8?q?=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- client/src/components/WalletSelector.tsx | 316 +++++++++++------------ client/src/hooks/useWallet.ts | 57 ++-- client/src/pages/Home.tsx | 128 +++++---- todo.md | 7 + 4 files changed, 270 insertions(+), 238 deletions(-) diff --git a/client/src/components/WalletSelector.tsx b/client/src/components/WalletSelector.tsx index c26cf91..df0c859 100644 --- a/client/src/components/WalletSelector.tsx +++ b/client/src/components/WalletSelector.tsx @@ -3,6 +3,7 @@ // v4: added TronLink support (desktop window.tronLink + mobile DeepLink) import { useState, useEffect, useCallback } from "react"; +import type { EthProvider } from "@/hooks/useWallet"; type Lang = "zh" | "en"; @@ -13,13 +14,14 @@ interface WalletInfo { installUrl: string; mobileDeepLink?: string; // DeepLink to open current page in wallet's in-app browser isInstalled: () => boolean; - connect: () => Promise; + connect: () => Promise<{ address: string; rawProvider: EthProvider } | null>; network: "evm" | "tron"; // wallet network type } interface WalletSelectorProps { lang: Lang; - onAddressDetected: (address: string, network?: "evm" | "tron") => void; + // 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) @@ -124,20 +126,7 @@ function isInWalletBrowser(): boolean { } // ── 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; -}; +// Note: EthProvider type is imported from useWallet.ts type TronLinkProvider = { ready: boolean; @@ -190,10 +179,12 @@ function findProvider(predicate: (p: EthProvider) => boolean): EthProvider | nul return predicate(eth) ? eth : null; } -async function requestAccounts(provider: EthProvider): Promise { +async function requestAccounts(provider: EthProvider): Promise<{ address: string; rawProvider: EthProvider } | null> { try { const accounts = await provider.request({ method: "eth_requestAccounts" }) as string[]; - return accounts?.[0] ?? null; + const address = accounts?.[0] ?? null; + if (!address) return null; + return { address, rawProvider: provider }; } catch (err: unknown) { const error = err as { code?: number; message?: string }; if (error?.code === 4001) throw new Error("user_rejected"); @@ -202,18 +193,20 @@ async function requestAccounts(provider: EthProvider): Promise { } } -async function requestTronAccounts(provider: TronLinkProvider): Promise { +async function requestTronAccounts(provider: TronLinkProvider): Promise<{ address: string; rawProvider: EthProvider } | null> { try { // TronLink v1: use tronWeb.defaultAddress if (provider.tronWeb?.defaultAddress?.base58) { - return 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 } } }; - return w.tronWeb?.defaultAddress?.base58 ?? null; + const address = w.tronWeb?.defaultAddress?.base58 ?? null; + if (!address) return null; + return { address, rawProvider: provider as unknown as EthProvider }; } if (result?.code === 4001) throw new Error("user_rejected"); return null; @@ -348,35 +341,25 @@ function isValidAddress(addr: string): boolean { 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 [showMore, setShowMore] = useState(false); - const tronWallets = [ - { + // MetaMask is always the primary wallet (most widely used) + const metamaskDeepLink = `https://metamask.app.link/dapp/${urlWithoutProtocol}`; + + const otherWallets = [ + ...( showTron ? [{ id: "tronlink", name: "TronLink", icon: , deepLink: `tronlinkoutside://pull.activity?param=${encodeURIComponent(JSON.stringify({ url: currentUrl, action: "open", protocol: "tronlink", version: "1.0" }))}`, - 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", }, @@ -385,7 +368,6 @@ function MobileDeepLinkPanel({ lang, showTron }: { lang: Lang; showTron: boolean name: "OKX Wallet", icon: , deepLink: `okx://wallet/dapp/url?dappUrl=${encodeURIComponent(currentUrl)}`, - installUrl: "https://www.okx.com/web3", badge: "EVM", badgeColor: "#00F0FF", }, @@ -394,90 +376,80 @@ function MobileDeepLinkPanel({ lang, showTron }: { lang: Lang; showTron: boolean name: "TokenPocket", icon: , deepLink: `tpdapp://open?params=${encodeURIComponent(JSON.stringify({ url: currentUrl, chain: "ETH", source: "NAC-Presale" }))}`, - 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."} -

-
+ +
+

MetaMask

+

+ {lang === "zh" ? "在 MetaMask 内置浏览器中打开" : "Open in MetaMask browser"} +

-
+ + + + + + - {/* 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 */} -
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" > -

- {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}

- ))} -
+ + + + {showMore + ? (lang === "zh" ? "收起其他钱包" : "Hide other wallets") + : (lang === "zh" ? "其他钱包(Trust / OKX / TokenPocket)" : "Other wallets (Trust / OKX / TokenPocket)")} + + + {showMore && ( + + )}
); } @@ -515,9 +487,10 @@ export function WalletSelector({ lang, onAddressDetected, connectedAddress, comp setConnecting(wallet.id); setError(null); try { - const address = await wallet.connect(); - if (address) { - onAddressDetected(address, wallet.network); + 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"); } @@ -582,66 +555,65 @@ 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) { - const hasInstalledWallet = installedWallets.length > 0; + return ( +
+ - if (!hasInstalledWallet) { - return ( -
- + {/* Manual address fallback */} +
+ - {/* 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}

- )} + {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 ───────────────────────────────────────────────────────── @@ -765,8 +737,8 @@ export function WalletSelector({ lang, onAddressDetected, connectedAddress, comp
)} - {/* Not-installed wallets — show install links */} - {!compact && notInstalledWallets.length > 0 && ( + {/* Not-installed wallets — only show when NO wallet is installed (UI fix: don't mix installed+uninstalled) */} + {!compact && notInstalledWallets.length > 0 && installedWallets.length === 0 && (

{lang === "zh" ? "未安装(点击安装)" : "Not installed (click to install)"} diff --git a/client/src/hooks/useWallet.ts b/client/src/hooks/useWallet.ts index 6c84b39..e5ea07e 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, TokenPocket, OKX, Bitget, Trust Wallet, imToken, SafePal, and all EVM wallets -// v4: improved Chinese wallet support (TokenPocket, OKX, Bitget first priority) +// v5: added connectWithProvider() to fix state sync when WalletSelector connects via external provider import { useState, useEffect, useCallback, useRef } from "react"; import { BrowserProvider, JsonRpcSigner, Eip1193Provider } from "ethers"; @@ -30,7 +30,7 @@ const INITIAL_STATE: WalletState = { error: null, }; -type EthProvider = Eip1193Provider & { +export type EthProvider = Eip1193Provider & { isMetaMask?: boolean; isTrust?: boolean; isTrustWallet?: boolean; @@ -45,12 +45,12 @@ type EthProvider = Eip1193Provider & { // Detect the best available EVM provider across all major wallets // Priority: TokenPocket > OKX > Bitget > Trust Wallet > MetaMask > others -export function detectProvider(): Eip1193Provider | null { +export function detectProvider(): EthProvider | null { if (typeof window === "undefined") return null; const w = window as unknown as Record; - // 1. TokenPocket — injects window.ethereum with isTokenPocket flag + // 1. Check window.ethereum (most wallets inject here) const eth = w.ethereum as EthProvider | undefined; if (eth) { // Check providers array first (multiple extensions installed) @@ -74,21 +74,21 @@ export function detectProvider(): Eip1193Provider | null { } // 2. OKX Wallet — sometimes injects window.okxwallet separately - if (w.okxwallet) return w.okxwallet as Eip1193Provider; + if (w.okxwallet) return w.okxwallet as EthProvider; // 3. Bitget Wallet — sometimes injects window.bitkeep.ethereum - const bitkeep = w.bitkeep as { ethereum?: Eip1193Provider } | undefined; + const bitkeep = w.bitkeep as { ethereum?: EthProvider } | undefined; if (bitkeep?.ethereum) return bitkeep.ethereum; // 4. Coinbase Wallet - if (w.coinbaseWalletExtension) return w.coinbaseWalletExtension as Eip1193Provider; + if (w.coinbaseWalletExtension) return w.coinbaseWalletExtension as EthProvider; return null; } // Build wallet state from a provider and accounts async function buildWalletState( - rawProvider: Eip1193Provider, + rawProvider: EthProvider, address: string ): Promise> { const provider = new BrowserProvider(rawProvider); @@ -125,7 +125,14 @@ async function buildWalletState( }; } -export function useWallet() { +export type WalletHookReturn = WalletState & { + connect: () => Promise<{ success: boolean; error?: string }>; + connectWithProvider: (rawProvider: EthProvider, address: string) => Promise; + disconnect: () => void; + switchNetwork: (chainId: number) => Promise; +}; + +export function useWallet(): WalletHookReturn { const [state, setState] = useState(INITIAL_STATE); const retryRef = useRef | null>(null); const mountedRef = useRef(true); @@ -138,7 +145,31 @@ export function useWallet() { }; }, []); - // ── Connect (explicit user action) ───────────────────────────────────────── + // ── Connect via external provider (called from WalletSelector) ───────────── + // KEY FIX: WalletSelector already has the provider and address from the wallet popup. + // We update state directly without calling connect() again (which would use detectProvider() + // and might pick the wrong wallet or fail if the wallet injects to a different window property). + const connectWithProvider = useCallback(async (rawProvider: EthProvider, address: string) => { + if (!mountedRef.current) return; + setState(s => ({ ...s, isConnecting: true, error: null })); + try { + const partial = await buildWalletState(rawProvider, address); + if (mountedRef.current) setState({ ...INITIAL_STATE, ...partial }); + } catch { + // Fallback: at minimum set address and isConnected = true + if (mountedRef.current) { + setState({ + ...INITIAL_STATE, + address, + shortAddress: shortenAddress(address), + isConnected: true, + isConnecting: false, + }); + } + } + }, []); + + // ── Connect (explicit user action via detectProvider) ────────────────────── const connect = useCallback(async (): Promise<{ success: boolean; error?: string }> => { const rawProvider = detectProvider(); @@ -172,10 +203,8 @@ export function useWallet() { let msg: string; if (error?.code === 4001) { - // User rejected msg = "已取消连接 / Connection cancelled"; } else if (error?.code === -32002) { - // Wallet has a pending request msg = "钱包请求处理中,请检查钱包弹窗。如未弹出,请先完成钱包初始化设置,然后刷新页面重试。"; } else if (error?.message === "no_accounts") { msg = "未获取到账户,请确认钱包已解锁并授权此网站。"; @@ -228,7 +257,6 @@ export function useWallet() { const rawProvider = detectProvider(); if (!rawProvider) { if (attempt < 5) { - // Retry more times — some wallets inject later (especially mobile in-app browsers) retryRef.current = setTimeout(() => tryAutoDetect(attempt + 1), 600 * attempt); } return; @@ -248,7 +276,6 @@ export function useWallet() { retryRef.current = setTimeout(() => tryAutoDetect(attempt + 1), 800 * attempt); } } catch { - // Silently ignore — user hasn't connected yet if (attempt < 3) { retryRef.current = setTimeout(() => tryAutoDetect(attempt + 1), 1000); } @@ -327,5 +354,5 @@ export function useWallet() { }; }, []); - return { ...state, connect, disconnect, switchNetwork }; + return { ...state, connect, connectWithProvider, disconnect, switchNetwork }; } diff --git a/client/src/pages/Home.tsx b/client/src/pages/Home.tsx index 819001c..b5e94fc 100644 --- a/client/src/pages/Home.tsx +++ b/client/src/pages/Home.tsx @@ -159,12 +159,12 @@ function TRC20Panel({ usdtAmount, lang, connectedAddress, onConnectWallet }: { u

⚠️

- {lang === "zh" ? "必填:您的XIC接收地址(BSC/ETH钉包地址)" : "Required: Your XIC Receiving Address (BSC/ETH wallet address)"} + {lang === "zh" ? "必填:您的XIC接收地址(BSC/ETH钱包地址)" : "Required: Your XIC Receiving Address (BSC/ETH wallet address)"}

{lang === "zh" - ? "XIC代币将发放到您的BSC/ETH钉包地址(0x开头)。请确保填写正确的地址,否则无法收到代币。" + ? "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."}

@@ -364,9 +364,15 @@ function EVMPurchasePanel({ network, lang, wallet }: { network: "BSC" | "ETH"; l { - // WalletSelector already called eth_requestAccounts and got the address - // Just show success toast; wallet state will auto-update via accountsChanged event + onAddressDetected={async (addr, _network, rawProvider) => { + // KEY FIX: call connectWithProvider to sync wallet state immediately + // Do NOT rely on accountsChanged event — it only fires for window.ethereum listeners + if (rawProvider) { + await wallet.connectWithProvider(rawProvider, addr); + } else { + // Manual address entry — no provider available, set address-only state + await wallet.connectWithProvider({ request: async () => [] } as unknown as import("@/hooks/useWallet").EthProvider, addr); + } toast.success(lang === "zh" ? `已连接: ${addr.slice(0, 6)}...${addr.slice(-4)}` : `Connected: ${addr.slice(0, 6)}...${addr.slice(-4)}`); }} compact @@ -522,27 +528,24 @@ function EVMPurchasePanel({ network, lang, wallet }: { network: "BSC" | "ETH"; l : (lang === "zh" ? "无最低/最高购买限制" : "No minimum or maximum purchase limit")}

- {/* Add XIC to Wallet button — only show on BSC where token address is known */} - {network === "BSC" && CONTRACTS.BSC.token && ( + {/* Add XIC to Wallet button — only show on BSC where token address is known AND wallet is connected */} + {network === "BSC" && CONTRACTS.BSC.token && wallet.isConnected && (