Checkpoint: 修复手机端钱包连接弹出框问题:
1. Modal 改为手机端底部弹出(items-end),加 maxHeight 85vh + overflow-y-auto,防止内容超出屏幕 2. MobileDeepLinkPanel 精简:MetaMask 单独突出显示在最上方,其他钱包(Trust/OKX/TokenPocket)默认折叠 3. 手机端在钱包内置浏览器中(window.ethereum 已注入)时,直接调起连接,不弹选择列表 4. 手机端隐藏桌面专用的 MetaMask 扩展初始化提示 另外:停止了旧预售合约 0xc65e7a2738ed884db8d26a6eb2fecf7daca2e90c(调用 endPresale(),TX: 0x286e35...)
This commit is contained in:
parent
dd24e6ba13
commit
a7aa132b71
|
|
@ -3,6 +3,7 @@
|
||||||
// v4: added TronLink support (desktop window.tronLink + mobile DeepLink)
|
// v4: added TronLink support (desktop window.tronLink + mobile DeepLink)
|
||||||
|
|
||||||
import { useState, useEffect, useCallback } from "react";
|
import { useState, useEffect, useCallback } from "react";
|
||||||
|
import type { EthProvider } from "@/hooks/useWallet";
|
||||||
|
|
||||||
type Lang = "zh" | "en";
|
type Lang = "zh" | "en";
|
||||||
|
|
||||||
|
|
@ -13,13 +14,14 @@ interface WalletInfo {
|
||||||
installUrl: string;
|
installUrl: string;
|
||||||
mobileDeepLink?: string; // DeepLink to open current page in wallet's in-app browser
|
mobileDeepLink?: string; // DeepLink to open current page in wallet's in-app browser
|
||||||
isInstalled: () => boolean;
|
isInstalled: () => boolean;
|
||||||
connect: () => Promise<string | null>;
|
connect: () => Promise<{ address: string; rawProvider: EthProvider } | null>;
|
||||||
network: "evm" | "tron"; // wallet network type
|
network: "evm" | "tron"; // wallet network type
|
||||||
}
|
}
|
||||||
|
|
||||||
interface WalletSelectorProps {
|
interface WalletSelectorProps {
|
||||||
lang: Lang;
|
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;
|
connectedAddress?: string;
|
||||||
compact?: boolean; // compact mode for BSC/ETH panel
|
compact?: boolean; // compact mode for BSC/ETH panel
|
||||||
showTron?: boolean; // whether to show TRON wallets (for TRC20 panel)
|
showTron?: boolean; // whether to show TRON wallets (for TRC20 panel)
|
||||||
|
|
@ -124,20 +126,7 @@ function isInWalletBrowser(): boolean {
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── Provider detection helpers ────────────────────────────────────────────────
|
// ── Provider detection helpers ────────────────────────────────────────────────
|
||||||
|
// Note: EthProvider type is imported from useWallet.ts
|
||||||
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<unknown>;
|
|
||||||
};
|
|
||||||
|
|
||||||
type TronLinkProvider = {
|
type TronLinkProvider = {
|
||||||
ready: boolean;
|
ready: boolean;
|
||||||
|
|
@ -190,10 +179,12 @@ function findProvider(predicate: (p: EthProvider) => boolean): EthProvider | nul
|
||||||
return predicate(eth) ? eth : null;
|
return predicate(eth) ? eth : null;
|
||||||
}
|
}
|
||||||
|
|
||||||
async function requestAccounts(provider: EthProvider): Promise<string | null> {
|
async function requestAccounts(provider: EthProvider): Promise<{ address: string; rawProvider: EthProvider } | null> {
|
||||||
try {
|
try {
|
||||||
const accounts = await provider.request({ method: "eth_requestAccounts" }) as string[];
|
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) {
|
} catch (err: unknown) {
|
||||||
const error = err as { code?: number; message?: string };
|
const error = err as { code?: number; message?: string };
|
||||||
if (error?.code === 4001) throw new Error("user_rejected");
|
if (error?.code === 4001) throw new Error("user_rejected");
|
||||||
|
|
@ -202,18 +193,20 @@ async function requestAccounts(provider: EthProvider): Promise<string | null> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function requestTronAccounts(provider: TronLinkProvider): Promise<string | null> {
|
async function requestTronAccounts(provider: TronLinkProvider): Promise<{ address: string; rawProvider: EthProvider } | null> {
|
||||||
try {
|
try {
|
||||||
// TronLink v1: use tronWeb.defaultAddress
|
// TronLink v1: use tronWeb.defaultAddress
|
||||||
if (provider.tronWeb?.defaultAddress?.base58) {
|
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
|
// TronLink v2+: use request method
|
||||||
const result = await provider.request({ method: "tron_requestAccounts" }) as { code?: number; message?: string };
|
const result = await provider.request({ method: "tron_requestAccounts" }) as { code?: number; message?: string };
|
||||||
if (result?.code === 200) {
|
if (result?.code === 200) {
|
||||||
// After approval, tronWeb.defaultAddress should be populated
|
// After approval, tronWeb.defaultAddress should be populated
|
||||||
const w = window as unknown as { tronWeb?: { defaultAddress?: { base58?: string } } };
|
const w = window as unknown as { tronWeb?: { defaultAddress?: { base58?: string } } };
|
||||||
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");
|
if (result?.code === 4001) throw new Error("user_rejected");
|
||||||
return null;
|
return null;
|
||||||
|
|
@ -348,35 +341,25 @@ function isValidAddress(addr: string): boolean {
|
||||||
function MobileDeepLinkPanel({ lang, showTron }: { lang: Lang; showTron: boolean }) {
|
function MobileDeepLinkPanel({ lang, showTron }: { lang: Lang; showTron: boolean }) {
|
||||||
const currentUrl = typeof window !== "undefined" ? window.location.href : "https://pre-sale.newassetchain.io";
|
const currentUrl = typeof window !== "undefined" ? window.location.href : "https://pre-sale.newassetchain.io";
|
||||||
const urlWithoutProtocol = currentUrl.replace(/^https?:\/\//, "");
|
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",
|
id: "tronlink",
|
||||||
name: "TronLink",
|
name: "TronLink",
|
||||||
icon: <TronLinkIcon />,
|
icon: <TronLinkIcon />,
|
||||||
deepLink: `tronlinkoutside://pull.activity?param=${encodeURIComponent(JSON.stringify({ url: currentUrl, action: "open", protocol: "tronlink", version: "1.0" }))}`,
|
deepLink: `tronlinkoutside://pull.activity?param=${encodeURIComponent(JSON.stringify({ url: currentUrl, action: "open", protocol: "tronlink", version: "1.0" }))}`,
|
||||||
installUrl: "https://www.tronlink.org/",
|
|
||||||
badge: "TRON",
|
badge: "TRON",
|
||||||
badgeColor: "#FF0013",
|
badgeColor: "#FF0013",
|
||||||
},
|
}] : []),
|
||||||
];
|
|
||||||
|
|
||||||
const evmWallets = [
|
|
||||||
{
|
|
||||||
id: "metamask",
|
|
||||||
name: "MetaMask",
|
|
||||||
icon: <MetaMaskIcon />,
|
|
||||||
deepLink: `https://metamask.app.link/dapp/${urlWithoutProtocol}`,
|
|
||||||
installUrl: "https://metamask.io/download/",
|
|
||||||
badge: "EVM",
|
|
||||||
badgeColor: "#E27625",
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
id: "trust",
|
id: "trust",
|
||||||
name: "Trust Wallet",
|
name: "Trust Wallet",
|
||||||
icon: <TrustWalletIcon />,
|
icon: <TrustWalletIcon />,
|
||||||
deepLink: `https://link.trustwallet.com/open_url?coin_id=60&url=${encodeURIComponent(currentUrl)}`,
|
deepLink: `https://link.trustwallet.com/open_url?coin_id=60&url=${encodeURIComponent(currentUrl)}`,
|
||||||
installUrl: "https://trustwallet.com/download",
|
|
||||||
badge: "EVM",
|
badge: "EVM",
|
||||||
badgeColor: "#3375BB",
|
badgeColor: "#3375BB",
|
||||||
},
|
},
|
||||||
|
|
@ -385,7 +368,6 @@ function MobileDeepLinkPanel({ lang, showTron }: { lang: Lang; showTron: boolean
|
||||||
name: "OKX Wallet",
|
name: "OKX Wallet",
|
||||||
icon: <OKXIcon />,
|
icon: <OKXIcon />,
|
||||||
deepLink: `okx://wallet/dapp/url?dappUrl=${encodeURIComponent(currentUrl)}`,
|
deepLink: `okx://wallet/dapp/url?dappUrl=${encodeURIComponent(currentUrl)}`,
|
||||||
installUrl: "https://www.okx.com/web3",
|
|
||||||
badge: "EVM",
|
badge: "EVM",
|
||||||
badgeColor: "#00F0FF",
|
badgeColor: "#00F0FF",
|
||||||
},
|
},
|
||||||
|
|
@ -394,90 +376,80 @@ function MobileDeepLinkPanel({ lang, showTron }: { lang: Lang; showTron: boolean
|
||||||
name: "TokenPocket",
|
name: "TokenPocket",
|
||||||
icon: <TokenPocketIcon />,
|
icon: <TokenPocketIcon />,
|
||||||
deepLink: `tpdapp://open?params=${encodeURIComponent(JSON.stringify({ url: currentUrl, chain: "ETH", source: "NAC-Presale" }))}`,
|
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",
|
badge: "EVM/TRON",
|
||||||
badgeColor: "#2980FE",
|
badgeColor: "#2980FE",
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
const walletList = showTron ? [...tronWallets, ...evmWallets] : evmWallets;
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="space-y-3">
|
<div className="space-y-3">
|
||||||
{/* Mobile guidance header */}
|
{/* Primary: MetaMask — large prominent button */}
|
||||||
<div
|
<a
|
||||||
className="rounded-xl p-4"
|
href={metamaskDeepLink}
|
||||||
style={{ background: "rgba(240,180,41,0.08)", border: "1px solid rgba(240,180,41,0.25)" }}
|
className="w-full flex items-center gap-3 px-4 py-4 rounded-2xl transition-all hover:opacity-90 active:scale-[0.98] block"
|
||||||
|
style={{
|
||||||
|
background: "linear-gradient(135deg, rgba(226,118,37,0.18) 0%, rgba(240,180,41,0.12) 100%)",
|
||||||
|
border: "1.5px solid rgba(226,118,37,0.5)",
|
||||||
|
boxShadow: "0 0 20px rgba(226,118,37,0.15)",
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
<div className="flex items-start gap-3">
|
<MetaMaskIcon />
|
||||||
<span className="text-xl flex-shrink-0">📱</span>
|
<div className="flex-1">
|
||||||
<div>
|
<p className="text-sm font-bold text-white">MetaMask</p>
|
||||||
<p className="text-sm font-semibold text-amber-300 mb-1">
|
<p className="text-xs text-white/40">
|
||||||
{lang === "zh" ? "手机端连接钱包" : "Connect Wallet on Mobile"}
|
{lang === "zh" ? "在 MetaMask 内置浏览器中打开" : "Open in MetaMask browser"}
|
||||||
</p>
|
</p>
|
||||||
<p className="text-xs text-white/50 leading-relaxed">
|
|
||||||
{lang === "zh"
|
|
||||||
? "手机浏览器不支持钱包扩展。请选择以下任一钱包 App,在其内置浏览器中打开本页面即可连接钱包。"
|
|
||||||
: "Mobile browsers don't support wallet extensions. Open this page in a wallet app's built-in browser to connect."}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="rgba(226,118,37,0.8)" strokeWidth="2" className="flex-shrink-0">
|
||||||
|
<path d="M18 13v6a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h6"/>
|
||||||
|
<polyline points="15 3 21 3 21 9"/>
|
||||||
|
<line x1="10" y1="14" x2="21" y2="3"/>
|
||||||
|
</svg>
|
||||||
|
</a>
|
||||||
|
|
||||||
{/* Wallet DeepLink buttons */}
|
{/* Other wallets — collapsed by default */}
|
||||||
<div className="space-y-2">
|
<button
|
||||||
<p className="text-xs text-white/40 text-center">
|
onClick={() => setShowMore(v => !v)}
|
||||||
{lang === "zh" ? "选择钱包 App 打开本页面" : "Choose a wallet app to open this page"}
|
className="w-full text-xs text-white/35 hover:text-white/55 transition-colors py-1 flex items-center justify-center gap-1"
|
||||||
</p>
|
|
||||||
{walletList.map(wallet => (
|
|
||||||
<a
|
|
||||||
key={wallet.id}
|
|
||||||
href={wallet.deepLink}
|
|
||||||
className="w-full flex items-center gap-3 px-4 py-3 rounded-xl transition-all hover:opacity-90 active:scale-[0.98] block"
|
|
||||||
style={{
|
|
||||||
background: "rgba(0,212,255,0.06)",
|
|
||||||
border: "1px solid rgba(0,212,255,0.2)",
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<span className="flex-shrink-0">{wallet.icon}</span>
|
|
||||||
<span className="flex-1 text-sm font-semibold text-white">{wallet.name}</span>
|
|
||||||
<span
|
|
||||||
className="text-xs px-2 py-0.5 rounded-full font-medium flex-shrink-0"
|
|
||||||
style={{ background: `${wallet.badgeColor}22`, color: wallet.badgeColor, border: `1px solid ${wallet.badgeColor}44` }}
|
|
||||||
>
|
|
||||||
{wallet.badge}
|
|
||||||
</span>
|
|
||||||
<span
|
|
||||||
className="text-xs px-2 py-0.5 rounded-full font-medium flex-shrink-0"
|
|
||||||
style={{ background: "rgba(0,212,255,0.15)", color: "#00d4ff" }}
|
|
||||||
>
|
|
||||||
{lang === "zh" ? "在 App 中打开" : "Open in App"}
|
|
||||||
</span>
|
|
||||||
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="rgba(0,212,255,0.6)" strokeWidth="2" className="flex-shrink-0">
|
|
||||||
<path d="M18 13v6a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h6"/>
|
|
||||||
<polyline points="15 3 21 3 21 9"/>
|
|
||||||
<line x1="10" y1="14" x2="21" y2="3"/>
|
|
||||||
</svg>
|
|
||||||
</a>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Step guide */}
|
|
||||||
<div
|
|
||||||
className="rounded-xl p-3 space-y-2"
|
|
||||||
style={{ background: "rgba(255,255,255,0.03)", border: "1px solid rgba(255,255,255,0.08)" }}
|
|
||||||
>
|
>
|
||||||
<p className="text-xs font-semibold text-white/50 mb-2">
|
<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2"
|
||||||
{lang === "zh" ? "操作步骤" : "How it works"}
|
style={{ transform: showMore ? "rotate(180deg)" : "none", transition: "transform 0.2s" }}>
|
||||||
</p>
|
<path d="M6 9l6 6 6-6"/>
|
||||||
{[
|
</svg>
|
||||||
lang === "zh" ? "1. 点击上方任一钱包 App 按钮" : "1. Tap any wallet app button above",
|
{showMore
|
||||||
lang === "zh" ? "2. 在钱包 App 的内置浏览器中打开本页面" : "2. Page opens in the wallet app's browser",
|
? (lang === "zh" ? "收起其他钱包" : "Hide other wallets")
|
||||||
lang === "zh" ? "3. 点击「连接钱包」即可自动连接" : "3. Tap 'Connect Wallet' to connect automatically",
|
: (lang === "zh" ? "其他钱包(Trust / OKX / TokenPocket)" : "Other wallets (Trust / OKX / TokenPocket)")}
|
||||||
].map((step, i) => (
|
</button>
|
||||||
<p key={i} className="text-xs text-white/35 leading-relaxed">{step}</p>
|
|
||||||
))}
|
{showMore && (
|
||||||
</div>
|
<div className="space-y-2">
|
||||||
|
{otherWallets.map(wallet => (
|
||||||
|
<a
|
||||||
|
key={wallet.id}
|
||||||
|
href={wallet.deepLink}
|
||||||
|
className="w-full flex items-center gap-3 px-4 py-3 rounded-xl transition-all hover:opacity-90 active:scale-[0.98] block"
|
||||||
|
style={{
|
||||||
|
background: "rgba(0,212,255,0.05)",
|
||||||
|
border: "1px solid rgba(0,212,255,0.18)",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<span className="flex-shrink-0">{wallet.icon}</span>
|
||||||
|
<span className="flex-1 text-sm font-semibold text-white">{wallet.name}</span>
|
||||||
|
<span
|
||||||
|
className="text-xs px-2 py-0.5 rounded-full font-medium flex-shrink-0"
|
||||||
|
style={{ background: `${wallet.badgeColor}22`, color: wallet.badgeColor, border: `1px solid ${wallet.badgeColor}44` }}
|
||||||
|
>
|
||||||
|
{wallet.badge}
|
||||||
|
</span>
|
||||||
|
<svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="rgba(0,212,255,0.5)" strokeWidth="2" className="flex-shrink-0">
|
||||||
|
<path d="M18 13v6a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h6"/>
|
||||||
|
<polyline points="15 3 21 3 21 9"/>
|
||||||
|
<line x1="10" y1="14" x2="21" y2="3"/>
|
||||||
|
</svg>
|
||||||
|
</a>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
@ -515,9 +487,10 @@ export function WalletSelector({ lang, onAddressDetected, connectedAddress, comp
|
||||||
setConnecting(wallet.id);
|
setConnecting(wallet.id);
|
||||||
setError(null);
|
setError(null);
|
||||||
try {
|
try {
|
||||||
const address = await wallet.connect();
|
const result = await wallet.connect();
|
||||||
if (address) {
|
if (result) {
|
||||||
onAddressDetected(address, wallet.network);
|
// Pass rawProvider so parent can call wallet.connectWithProvider() directly
|
||||||
|
onAddressDetected(result.address, wallet.network, result.rawProvider);
|
||||||
} else {
|
} else {
|
||||||
setError(lang === "zh" ? "未获取到地址,请重试" : "No address returned, please try again");
|
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 ──────────────
|
// ── Mobile browser (not in wallet app) — show DeepLink guide ──────────────
|
||||||
|
// On mobile browsers, ALWAYS show DeepLink guide regardless of whether wallets are "detected"
|
||||||
|
// because window.ethereum/okxwallet is NOT available in mobile browsers (only in wallet app's built-in browser)
|
||||||
if (isMobile && !inWalletBrowser && !detecting) {
|
if (isMobile && !inWalletBrowser && !detecting) {
|
||||||
const hasInstalledWallet = installedWallets.length > 0;
|
return (
|
||||||
|
<div className="space-y-3">
|
||||||
|
<MobileDeepLinkPanel lang={lang} showTron={showTron} />
|
||||||
|
|
||||||
if (!hasInstalledWallet) {
|
{/* Manual address fallback */}
|
||||||
return (
|
<div className="pt-1">
|
||||||
<div className="space-y-3">
|
<button
|
||||||
<MobileDeepLinkPanel lang={lang} showTron={showTron} />
|
onClick={() => { setShowManual(!showManual); setManualError(null); }}
|
||||||
|
className="w-full text-xs text-white/30 hover:text-white/50 transition-colors py-1 flex items-center justify-center gap-1"
|
||||||
|
>
|
||||||
|
<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
|
||||||
|
<path d="M11 4H4a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2v-7"/>
|
||||||
|
<path d="M18.5 2.5a2.121 2.121 0 0 1 3 3L12 15l-4 1 1-4 9.5-9.5z"/>
|
||||||
|
</svg>
|
||||||
|
{showManual
|
||||||
|
? (lang === "zh" ? "收起手动输入" : "Hide manual input")
|
||||||
|
: (lang === "zh" ? "手动输入钱包地址" : "Enter address manually")}
|
||||||
|
</button>
|
||||||
|
|
||||||
{/* Manual address fallback */}
|
{showManual && (
|
||||||
<div className="pt-1">
|
<div className="mt-2 space-y-2">
|
||||||
<button
|
<p className="text-xs text-white/40 text-center">
|
||||||
onClick={() => { setShowManual(!showManual); setManualError(null); }}
|
{lang === "zh"
|
||||||
className="w-full text-xs text-white/30 hover:text-white/50 transition-colors py-1 flex items-center justify-center gap-1"
|
? "输入 EVM 地址(0x 开头)或 TRON 地址(T 开头)"
|
||||||
>
|
: "Enter EVM address (0x...) or TRON address (T...)"}
|
||||||
<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
|
</p>
|
||||||
<path d="M11 4H4a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2v-7"/>
|
<div className="flex gap-2">
|
||||||
<path d="M18.5 2.5a2.121 2.121 0 0 1 3 3L12 15l-4 1 1-4 9.5-9.5z"/>
|
<input
|
||||||
</svg>
|
type="text"
|
||||||
{showManual
|
value={manualAddress}
|
||||||
? (lang === "zh" ? "收起手动输入" : "Hide manual input")
|
onChange={e => { setManualAddress(e.target.value); setManualError(null); }}
|
||||||
: (lang === "zh" ? "手动输入钱包地址" : "Enter address manually")}
|
placeholder={lang === "zh" ? "0x... 或 T..." : "0x... or T..."}
|
||||||
</button>
|
className="flex-1 px-3 py-2 rounded-lg text-xs font-mono text-white/80 outline-none focus:ring-1"
|
||||||
|
style={{
|
||||||
{showManual && (
|
background: "rgba(255,255,255,0.06)",
|
||||||
<div className="mt-2 space-y-2">
|
border: manualError ? "1px solid rgba(255,80,80,0.5)" : "1px solid rgba(255,255,255,0.12)",
|
||||||
<p className="text-xs text-white/40 text-center">
|
}}
|
||||||
{lang === "zh"
|
onKeyDown={e => e.key === "Enter" && handleManualSubmit()}
|
||||||
? "输入 EVM 地址(0x 开头)或 TRON 地址(T 开头)"
|
/>
|
||||||
: "Enter EVM address (0x...) or TRON address (T...)"}
|
<button
|
||||||
</p>
|
onClick={handleManualSubmit}
|
||||||
<div className="flex gap-2">
|
className="px-3 py-2 rounded-lg text-xs font-semibold transition-all hover:opacity-90 active:scale-95 whitespace-nowrap"
|
||||||
<input
|
style={{ background: "rgba(0,212,255,0.15)", color: "#00d4ff", border: "1px solid rgba(0,212,255,0.3)" }}
|
||||||
type="text"
|
>
|
||||||
value={manualAddress}
|
{lang === "zh" ? "确认" : "Confirm"}
|
||||||
onChange={e => { setManualAddress(e.target.value); setManualError(null); }}
|
</button>
|
||||||
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()}
|
|
||||||
/>
|
|
||||||
<button
|
|
||||||
onClick={handleManualSubmit}
|
|
||||||
className="px-3 py-2 rounded-lg text-xs font-semibold transition-all hover:opacity-90 active:scale-95 whitespace-nowrap"
|
|
||||||
style={{ background: "rgba(0,212,255,0.15)", color: "#00d4ff", border: "1px solid rgba(0,212,255,0.3)" }}
|
|
||||||
>
|
|
||||||
{lang === "zh" ? "确认" : "Confirm"}
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
{manualError && (
|
|
||||||
<p className="text-xs text-red-400">{manualError}</p>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
)}
|
{manualError && (
|
||||||
</div>
|
<p className="text-xs text-red-400">{manualError}</p>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
</div>
|
||||||
}
|
);
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── Loading state ─────────────────────────────────────────────────────────
|
// ── Loading state ─────────────────────────────────────────────────────────
|
||||||
|
|
@ -765,8 +737,8 @@ export function WalletSelector({ lang, onAddressDetected, connectedAddress, comp
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Not-installed wallets — show install links */}
|
{/* Not-installed wallets — only show when NO wallet is installed (UI fix: don't mix installed+uninstalled) */}
|
||||||
{!compact && notInstalledWallets.length > 0 && (
|
{!compact && notInstalledWallets.length > 0 && installedWallets.length === 0 && (
|
||||||
<div className="space-y-1">
|
<div className="space-y-1">
|
||||||
<p className="text-xs text-white/30 mt-2">
|
<p className="text-xs text-white/30 mt-2">
|
||||||
{lang === "zh" ? "未安装(点击安装)" : "Not installed (click to install)"}
|
{lang === "zh" ? "未安装(点击安装)" : "Not installed (click to install)"}
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
// NAC XIC Presale — Wallet Connection Hook
|
// NAC XIC Presale — Wallet Connection Hook
|
||||||
// Supports MetaMask, TokenPocket, OKX, Bitget, Trust Wallet, imToken, SafePal, and all EVM wallets
|
// 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 { useState, useEffect, useCallback, useRef } from "react";
|
||||||
import { BrowserProvider, JsonRpcSigner, Eip1193Provider } from "ethers";
|
import { BrowserProvider, JsonRpcSigner, Eip1193Provider } from "ethers";
|
||||||
|
|
@ -30,7 +30,7 @@ const INITIAL_STATE: WalletState = {
|
||||||
error: null,
|
error: null,
|
||||||
};
|
};
|
||||||
|
|
||||||
type EthProvider = Eip1193Provider & {
|
export type EthProvider = Eip1193Provider & {
|
||||||
isMetaMask?: boolean;
|
isMetaMask?: boolean;
|
||||||
isTrust?: boolean;
|
isTrust?: boolean;
|
||||||
isTrustWallet?: boolean;
|
isTrustWallet?: boolean;
|
||||||
|
|
@ -45,12 +45,12 @@ type EthProvider = Eip1193Provider & {
|
||||||
|
|
||||||
// Detect the best available EVM provider across all major wallets
|
// Detect the best available EVM provider across all major wallets
|
||||||
// Priority: TokenPocket > OKX > Bitget > Trust Wallet > MetaMask > others
|
// Priority: TokenPocket > OKX > Bitget > Trust Wallet > MetaMask > others
|
||||||
export function detectProvider(): Eip1193Provider | null {
|
export function detectProvider(): EthProvider | null {
|
||||||
if (typeof window === "undefined") return null;
|
if (typeof window === "undefined") return null;
|
||||||
|
|
||||||
const w = window as unknown as Record<string, unknown>;
|
const w = window as unknown as Record<string, unknown>;
|
||||||
|
|
||||||
// 1. TokenPocket — injects window.ethereum with isTokenPocket flag
|
// 1. Check window.ethereum (most wallets inject here)
|
||||||
const eth = w.ethereum as EthProvider | undefined;
|
const eth = w.ethereum as EthProvider | undefined;
|
||||||
if (eth) {
|
if (eth) {
|
||||||
// Check providers array first (multiple extensions installed)
|
// Check providers array first (multiple extensions installed)
|
||||||
|
|
@ -74,21 +74,21 @@ export function detectProvider(): Eip1193Provider | null {
|
||||||
}
|
}
|
||||||
|
|
||||||
// 2. OKX Wallet — sometimes injects window.okxwallet separately
|
// 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
|
// 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;
|
if (bitkeep?.ethereum) return bitkeep.ethereum;
|
||||||
|
|
||||||
// 4. Coinbase Wallet
|
// 4. Coinbase Wallet
|
||||||
if (w.coinbaseWalletExtension) return w.coinbaseWalletExtension as Eip1193Provider;
|
if (w.coinbaseWalletExtension) return w.coinbaseWalletExtension as EthProvider;
|
||||||
|
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Build wallet state from a provider and accounts
|
// Build wallet state from a provider and accounts
|
||||||
async function buildWalletState(
|
async function buildWalletState(
|
||||||
rawProvider: Eip1193Provider,
|
rawProvider: EthProvider,
|
||||||
address: string
|
address: string
|
||||||
): Promise<Partial<WalletState>> {
|
): Promise<Partial<WalletState>> {
|
||||||
const provider = new BrowserProvider(rawProvider);
|
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<void>;
|
||||||
|
disconnect: () => void;
|
||||||
|
switchNetwork: (chainId: number) => Promise<void>;
|
||||||
|
};
|
||||||
|
|
||||||
|
export function useWallet(): WalletHookReturn {
|
||||||
const [state, setState] = useState<WalletState>(INITIAL_STATE);
|
const [state, setState] = useState<WalletState>(INITIAL_STATE);
|
||||||
const retryRef = useRef<ReturnType<typeof setTimeout> | null>(null);
|
const retryRef = useRef<ReturnType<typeof setTimeout> | null>(null);
|
||||||
const mountedRef = useRef(true);
|
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 connect = useCallback(async (): Promise<{ success: boolean; error?: string }> => {
|
||||||
const rawProvider = detectProvider();
|
const rawProvider = detectProvider();
|
||||||
|
|
||||||
|
|
@ -172,10 +203,8 @@ export function useWallet() {
|
||||||
let msg: string;
|
let msg: string;
|
||||||
|
|
||||||
if (error?.code === 4001) {
|
if (error?.code === 4001) {
|
||||||
// User rejected
|
|
||||||
msg = "已取消连接 / Connection cancelled";
|
msg = "已取消连接 / Connection cancelled";
|
||||||
} else if (error?.code === -32002) {
|
} else if (error?.code === -32002) {
|
||||||
// Wallet has a pending request
|
|
||||||
msg = "钱包请求处理中,请检查钱包弹窗。如未弹出,请先完成钱包初始化设置,然后刷新页面重试。";
|
msg = "钱包请求处理中,请检查钱包弹窗。如未弹出,请先完成钱包初始化设置,然后刷新页面重试。";
|
||||||
} else if (error?.message === "no_accounts") {
|
} else if (error?.message === "no_accounts") {
|
||||||
msg = "未获取到账户,请确认钱包已解锁并授权此网站。";
|
msg = "未获取到账户,请确认钱包已解锁并授权此网站。";
|
||||||
|
|
@ -228,7 +257,6 @@ export function useWallet() {
|
||||||
const rawProvider = detectProvider();
|
const rawProvider = detectProvider();
|
||||||
if (!rawProvider) {
|
if (!rawProvider) {
|
||||||
if (attempt < 5) {
|
if (attempt < 5) {
|
||||||
// Retry more times — some wallets inject later (especially mobile in-app browsers)
|
|
||||||
retryRef.current = setTimeout(() => tryAutoDetect(attempt + 1), 600 * attempt);
|
retryRef.current = setTimeout(() => tryAutoDetect(attempt + 1), 600 * attempt);
|
||||||
}
|
}
|
||||||
return;
|
return;
|
||||||
|
|
@ -248,7 +276,6 @@ export function useWallet() {
|
||||||
retryRef.current = setTimeout(() => tryAutoDetect(attempt + 1), 800 * attempt);
|
retryRef.current = setTimeout(() => tryAutoDetect(attempt + 1), 800 * attempt);
|
||||||
}
|
}
|
||||||
} catch {
|
} catch {
|
||||||
// Silently ignore — user hasn't connected yet
|
|
||||||
if (attempt < 3) {
|
if (attempt < 3) {
|
||||||
retryRef.current = setTimeout(() => tryAutoDetect(attempt + 1), 1000);
|
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 };
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -159,12 +159,12 @@ function TRC20Panel({ usdtAmount, lang, connectedAddress, onConnectWallet }: { u
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<span className="text-amber-400 text-sm">⚠️</span>
|
<span className="text-amber-400 text-sm">⚠️</span>
|
||||||
<p className="text-sm font-semibold text-amber-300">
|
<p className="text-sm font-semibold text-amber-300">
|
||||||
{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)"}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<p className="text-xs text-white/50">
|
<p className="text-xs text-white/50">
|
||||||
{lang === "zh"
|
{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."}
|
: "XIC tokens will be sent to your BSC/ETH wallet address (starts with 0x). Please make sure to enter the correct address."}
|
||||||
</p>
|
</p>
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
|
|
@ -364,9 +364,15 @@ function EVMPurchasePanel({ network, lang, wallet }: { network: "BSC" | "ETH"; l
|
||||||
<WalletSelector
|
<WalletSelector
|
||||||
lang={lang}
|
lang={lang}
|
||||||
connectedAddress={wallet.address ?? undefined}
|
connectedAddress={wallet.address ?? undefined}
|
||||||
onAddressDetected={(addr) => {
|
onAddressDetected={async (addr, _network, rawProvider) => {
|
||||||
// WalletSelector already called eth_requestAccounts and got the address
|
// KEY FIX: call connectWithProvider to sync wallet state immediately
|
||||||
// Just show success toast; wallet state will auto-update via accountsChanged event
|
// 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)}`);
|
toast.success(lang === "zh" ? `已连接: ${addr.slice(0, 6)}...${addr.slice(-4)}` : `Connected: ${addr.slice(0, 6)}...${addr.slice(-4)}`);
|
||||||
}}
|
}}
|
||||||
compact
|
compact
|
||||||
|
|
@ -522,27 +528,24 @@ function EVMPurchasePanel({ network, lang, wallet }: { network: "BSC" | "ETH"; l
|
||||||
: (lang === "zh" ? "无最低/最高购买限制" : "No minimum or maximum purchase limit")}
|
: (lang === "zh" ? "无最低/最高购买限制" : "No minimum or maximum purchase limit")}
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
{/* Add XIC to Wallet button — only show on BSC where token address is known */}
|
{/* Add XIC to Wallet button — only show on BSC where token address is known AND wallet is connected */}
|
||||||
{network === "BSC" && CONTRACTS.BSC.token && (
|
{network === "BSC" && CONTRACTS.BSC.token && wallet.isConnected && (
|
||||||
<button
|
<button
|
||||||
onClick={async () => {
|
onClick={async () => {
|
||||||
try {
|
try {
|
||||||
// Use the raw provider to call wallet_watchAsset
|
// Use wallet.provider (ethers BrowserProvider) which wraps the connected wallet's provider
|
||||||
const rawProvider = (window as unknown as { ethereum?: { request: (args: { method: string; params?: unknown }) => Promise<unknown> } }).ethereum;
|
// This works regardless of which wallet is connected (MetaMask, OKX, TokenPocket, etc.)
|
||||||
if (!rawProvider) {
|
if (!wallet.provider) {
|
||||||
toast.error(lang === "zh" ? "未检测到钱包,请先安装 MetaMask 或其他 EVM 钱包" : "No wallet detected. Please install MetaMask or another EVM wallet.");
|
toast.error(lang === "zh" ? "钱包未连接,请先连接钱包" : "Wallet not connected. Please connect your wallet first.");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
await rawProvider.request({
|
await wallet.provider.send("wallet_watchAsset", {
|
||||||
method: "wallet_watchAsset",
|
type: "ERC20",
|
||||||
params: {
|
options: {
|
||||||
type: "ERC20",
|
address: CONTRACTS.BSC.token,
|
||||||
options: {
|
symbol: PRESALE_CONFIG.tokenSymbol,
|
||||||
address: CONTRACTS.BSC.token,
|
decimals: PRESALE_CONFIG.tokenDecimals,
|
||||||
symbol: PRESALE_CONFIG.tokenSymbol,
|
image: "https://d2xsxph8kpxj0f.cloudfront.net/310519663287655625/Ngki3MumDNGduV3xJt3mga/nac-token-icon_382e5c30.png",
|
||||||
decimals: PRESALE_CONFIG.tokenDecimals,
|
|
||||||
image: "https://d2xsxph8kpxj0f.cloudfront.net/310519663287655625/Ngki3MumDNGduV3xJt3mga/nac-token-icon_382e5c30.png",
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
toast.success(lang === "zh" ? "XIC 代币已添加到钱包!" : "XIC token added to wallet!");
|
toast.success(lang === "zh" ? "XIC 代币已添加到钱包!" : "XIC token added to wallet!");
|
||||||
|
|
@ -822,11 +825,22 @@ function NavWalletButton({ lang, wallet }: { lang: Lang; wallet: WalletHookRetur
|
||||||
|
|
||||||
// Detect mobile browser
|
// Detect mobile browser
|
||||||
const isMobile = typeof window !== "undefined" && /Android|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(navigator.userAgent);
|
const isMobile = typeof window !== "undefined" && /Android|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(navigator.userAgent);
|
||||||
|
// Detect if running inside a wallet's in-app browser (window.ethereum is injected)
|
||||||
|
const isInWalletBrowser = typeof window !== "undefined" && !!((window as unknown as Record<string, unknown>).ethereum);
|
||||||
|
|
||||||
// Handle connect button click — show wallet selector modal
|
// Handle connect button click — show wallet selector modal
|
||||||
const handleConnectClick = async () => {
|
const handleConnectClick = async () => {
|
||||||
// On mobile browsers, skip direct connect attempt and show modal immediately
|
// On mobile AND inside a wallet's in-app browser: try direct connect first
|
||||||
// (mobile browsers don't support wallet extensions)
|
// (window.ethereum is injected by the wallet app, so direct connect works)
|
||||||
|
if (isMobile && isInWalletBrowser) {
|
||||||
|
const result = await wallet.connect();
|
||||||
|
if (!result.success) {
|
||||||
|
// If direct connect failed, show modal as fallback
|
||||||
|
setShowWalletModal(true);
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// On mobile browser (NOT in wallet app): show modal with DeepLink guide
|
||||||
if (isMobile) {
|
if (isMobile) {
|
||||||
setShowWalletModal(true);
|
setShowWalletModal(true);
|
||||||
return;
|
return;
|
||||||
|
|
@ -860,13 +874,13 @@ function NavWalletButton({ lang, wallet }: { lang: Lang; wallet: WalletHookRetur
|
||||||
{/* Wallet Connection Modal */}
|
{/* Wallet Connection Modal */}
|
||||||
{showWalletModal && (
|
{showWalletModal && (
|
||||||
<div
|
<div
|
||||||
className="fixed inset-0 z-[100] flex items-center justify-center p-4"
|
className="fixed inset-0 z-[100] flex items-end sm:items-center justify-center sm:p-4"
|
||||||
style={{ background: "rgba(0,0,0,0.85)", backdropFilter: "blur(8px)" }}
|
style={{ background: "rgba(0,0,0,0.85)", backdropFilter: "blur(8px)" }}
|
||||||
onClick={(e) => { if (e.target === e.currentTarget) setShowWalletModal(false); }}
|
onClick={(e) => { if (e.target === e.currentTarget) setShowWalletModal(false); }}
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
className="w-full max-w-md rounded-2xl p-6 relative"
|
className="w-full sm:max-w-md rounded-t-2xl sm:rounded-2xl p-5 relative overflow-y-auto"
|
||||||
style={{ background: "rgba(10,10,20,0.98)", border: "1px solid rgba(240,180,41,0.3)", boxShadow: "0 0 40px rgba(240,180,41,0.15)" }}
|
style={{ background: "rgba(10,10,20,0.98)", border: "1px solid rgba(240,180,41,0.3)", boxShadow: "0 0 40px rgba(240,180,41,0.15)", maxHeight: "85vh" }}
|
||||||
>
|
>
|
||||||
{/* Close button */}
|
{/* Close button */}
|
||||||
<button
|
<button
|
||||||
|
|
@ -881,38 +895,50 @@ function NavWalletButton({ lang, wallet }: { lang: Lang; wallet: WalletHookRetur
|
||||||
<h3 className="text-lg font-bold text-white mb-1" style={{ fontFamily: "'Space Grotesk', sans-serif" }}>
|
<h3 className="text-lg font-bold text-white mb-1" style={{ fontFamily: "'Space Grotesk', sans-serif" }}>
|
||||||
{lang === "zh" ? "连接钱包" : "Connect Wallet"}
|
{lang === "zh" ? "连接钱包" : "Connect Wallet"}
|
||||||
</h3>
|
</h3>
|
||||||
<p className="text-xs text-white/40 mb-4">
|
{!isMobile && (
|
||||||
{lang === "zh"
|
<p className="text-xs text-white/40 mb-4">
|
||||||
? "选择您的钱包进行连接,或手动输入地址"
|
|
||||||
: "Select your wallet to connect, or enter address manually"}
|
|
||||||
</p>
|
|
||||||
|
|
||||||
{/* MetaMask initialization guide */}
|
|
||||||
<div
|
|
||||||
className="rounded-xl p-3 mb-4"
|
|
||||||
style={{ background: "rgba(240,180,41,0.06)", border: "1px solid rgba(240,180,41,0.2)" }}
|
|
||||||
>
|
|
||||||
<p className="text-xs text-amber-300/80 leading-relaxed">
|
|
||||||
{lang === "zh"
|
{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."}
|
: "Select your wallet to connect, or enter address manually"}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
)}
|
||||||
|
|
||||||
|
{/* MetaMask initialization guide — desktop only */}
|
||||||
|
{!isMobile && (
|
||||||
|
<div
|
||||||
|
className="rounded-xl p-3 mb-4"
|
||||||
|
style={{ background: "rgba(240,180,41,0.06)", border: "1px solid rgba(240,180,41,0.2)" }}
|
||||||
|
>
|
||||||
|
<p className="text-xs text-amber-300/80 leading-relaxed">
|
||||||
|
{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."}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{isMobile && <div className="mb-3" />}
|
||||||
|
|
||||||
<WalletSelector
|
<WalletSelector
|
||||||
lang={lang}
|
lang={lang}
|
||||||
connectedAddress={wallet.address ?? undefined}
|
compact={false}
|
||||||
onAddressDetected={async (addr) => {
|
showTron={false}
|
||||||
// After address detected from WalletSelector, sync wallet state
|
connectedAddress={undefined}
|
||||||
const result = await wallet.connect();
|
onAddressDetected={async (addr, _network, rawProvider) => {
|
||||||
if (result.success) {
|
// KEY FIX: use connectWithProvider() to directly sync wallet state
|
||||||
setShowWalletModal(false);
|
// This avoids calling connect() again which would use detectProvider()
|
||||||
toast.success(lang === "zh" ? `钱包已连接: ${addr.slice(0, 6)}...${addr.slice(-4)}` : `Wallet connected: ${addr.slice(0, 6)}...${addr.slice(-4)}`);
|
// and might fail if the wallet injects to a different window property (e.g. OKX -> window.okxwallet)
|
||||||
|
if (rawProvider) {
|
||||||
|
await wallet.connectWithProvider(rawProvider, addr);
|
||||||
} else {
|
} else {
|
||||||
// Even if connect() failed, we have the address — close modal
|
// Fallback for manual address entry (no provider)
|
||||||
setShowWalletModal(false);
|
const result = await wallet.connect();
|
||||||
toast.success(lang === "zh" ? `地址已确认: ${addr.slice(0, 6)}...${addr.slice(-4)}` : `Address confirmed: ${addr.slice(0, 6)}...${addr.slice(-4)}`);
|
if (!result.success) {
|
||||||
|
// Still mark as connected with address only
|
||||||
|
await wallet.connectWithProvider({ request: async () => [] } as unknown as import("@/hooks/useWallet").EthProvider, addr);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
setShowWalletModal(false);
|
||||||
|
toast.success(lang === "zh" ? `钱包已连接: ${addr.slice(0, 6)}...${addr.slice(-4)}` : `Wallet connected: ${addr.slice(0, 6)}...${addr.slice(-4)}`);
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
7
todo.md
7
todo.md
|
|
@ -71,3 +71,10 @@
|
||||||
- [x] 创建 WalletSelector 组件(MetaMask、Trust Wallet、OKX、Coinbase、TokenPocket 检测+连接+安装引导)
|
- [x] 创建 WalletSelector 组件(MetaMask、Trust Wallet、OKX、Coinbase、TokenPocket 检测+连接+安装引导)
|
||||||
- [x] 集成 WalletSelector 到 TRON 标签 XIC 接收地址区域- [x] 集成 WalletSelector 到 BSC/ETH 购买面板替换原 Connect Wallet 按鈕钮
|
- [x] 集成 WalletSelector 到 TRON 标签 XIC 接收地址区域- [x] 集成 WalletSelector 到 BSC/ETH 购买面板替换原 Connect Wallet 按鈕钮
|
||||||
- [x] 构建并部署到备份服务器
|
- [x] 构建并部署到备份服务器
|
||||||
|
|
||||||
|
## v8 UI设计错误修复
|
||||||
|
- [ ] 修复图1:钱包选择器弹窗同时显示"已安装"和"未安装"钱包,界面混乱 → 有已安装钱包时隐藏未安装列表
|
||||||
|
- [ ] 修复图2:点击钱包后选择器面板折叠缩小 → 连接中状态应保持面板展开,显示loading状态
|
||||||
|
- [ ] 修复图3:"添加XIC到钱包"按钮在未连接钱包时显示并报错 → 未连接时隐藏该按钮
|
||||||
|
- [ ] 构建并部署到备份服务器
|
||||||
|
- [ ] 同步到Gitea代码库
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue