feat: 支持前10大EVM钱包,连接后自动识别链切换网络标签,watchAsset使用正确provider

- useWallet.ts: forceConnect接受specificProvider参数,暴露watchAsset()方法
- WalletSelector.tsx: 支持MetaMask/OKX/TP/Trust/Coinbase/Bitget/Rabby/SafePal/imToken/Phantom
  connect()返回{address, provider},onAddressDetected传递provider
- Home.tsx: forceConnect传入provider,连接成功后自动切换BSC/ETH网络标签
  连接成功后自动触发watchAsset弹出添加代币确认框
  handleAddToken改用wallet.watchAsset()而非window.ethereum
  NavWalletButton增加onNetworkDetected回调prop
This commit is contained in:
NAC Admin 2026-03-10 09:22:54 +08:00
parent 8bd78c3fc0
commit 706eead8b3
3 changed files with 364 additions and 195 deletions

View File

@ -1,26 +1,44 @@
// NAC XIC Presale — Wallet Selector Component // NAC XIC Presale — Wallet Selector Component
// Detects installed EVM wallets and shows connect/install buttons for each // Detects installed EVM wallets and shows connect/install buttons for each
// v3: added mobile detection, DeepLink support for MetaMask/Trust/OKX App // v5: connect() returns { address, provider } so caller can use the correct wallet provider
import { useState, useEffect, useCallback } from "react"; import { useState, useEffect, useCallback } from "react";
import { Eip1193Provider } from "ethers";
type Lang = "zh" | "en"; type Lang = "zh" | "en";
type EthProvider = Eip1193Provider & {
isMetaMask?: boolean;
isTrust?: boolean;
isTrustWallet?: boolean;
isOKExWallet?: boolean;
isOkxWallet?: boolean;
isCoinbaseWallet?: boolean;
isTokenPocket?: boolean;
isBitkeep?: boolean;
isBitgetWallet?: boolean;
isRabby?: boolean;
isSafePal?: boolean;
isImToken?: boolean;
isPhantom?: boolean;
providers?: EthProvider[];
};
interface WalletInfo { interface WalletInfo {
id: string; id: string;
name: string; name: string;
icon: React.ReactNode; icon: React.ReactNode;
installUrl: string; installUrl: string;
mobileDeepLink?: string; // DeepLink to open current page in wallet's in-app browser mobileDeepLink?: string;
isInstalled: () => boolean; isInstalled: () => boolean;
connect: () => Promise<string | null>; connect: () => Promise<{ address: string; provider: EthProvider } | null>;
} }
interface WalletSelectorProps { interface WalletSelectorProps {
lang: Lang; lang: Lang;
onAddressDetected: (address: string) => void; onAddressDetected: (address: string, provider: EthProvider) => void;
connectedAddress?: string; connectedAddress?: string;
compact?: boolean; // compact mode for BSC/ETH panel compact?: boolean;
} }
// ── Wallet Icons ────────────────────────────────────────────────────────────── // ── Wallet Icons ──────────────────────────────────────────────────────────────
@ -81,6 +99,40 @@ const BitgetIcon = () => (
</svg> </svg>
); );
const RabbyIcon = () => (
<svg width="24" height="24" viewBox="0 0 24 24" fill="none">
<rect width="24" height="24" rx="12" fill="#7B4FFF"/>
<path d="M12 5c-3.87 0-7 2.69-7 6 0 2.21 1.34 4.15 3.35 5.26L8 19h8l-.35-2.74C17.66 15.15 19 13.21 19 11c0-3.31-3.13-6-7-6z" fill="white" fillOpacity="0.9"/>
<circle cx="9.5" cy="11" r="1.5" fill="#7B4FFF"/>
<circle cx="14.5" cy="11" r="1.5" fill="#7B4FFF"/>
</svg>
);
const SafePalIcon = () => (
<svg width="24" height="24" viewBox="0 0 24 24" fill="none">
<rect width="24" height="24" rx="6" fill="#1A1A2E"/>
<path d="M12 4L6 7v5c0 3.31 2.69 6 6 6s6-2.69 6-6V7L12 4z" fill="#00D4FF" fillOpacity="0.8"/>
<path d="M10 12l2 2 4-4" stroke="#1A1A2E" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round"/>
</svg>
);
const ImTokenIcon = () => (
<svg width="24" height="24" viewBox="0 0 24 24" fill="none">
<rect width="24" height="24" rx="12" fill="#11C4D1"/>
<path d="M8 12c0-2.21 1.79-4 4-4s4 1.79 4 4-1.79 4-4 4-4-1.79-4-4z" fill="white"/>
<path d="M12 10v4M10 12h4" stroke="#11C4D1" strokeWidth="1.5" strokeLinecap="round"/>
</svg>
);
const PhantomIcon = () => (
<svg width="24" height="24" viewBox="0 0 24 24" fill="none">
<rect width="24" height="24" rx="12" fill="#AB9FF2"/>
<path d="M6 12c0-3.31 2.69-6 6-6s6 2.69 6 6c0 1.5-.55 2.87-1.46 3.92L18 19h-3l-1.5-2.5c-.32.03-.65.05-.99.05A6 6 0 0 1 6 12z" fill="white"/>
<circle cx="10" cy="12" r="1.2" fill="#AB9FF2"/>
<circle cx="14" cy="12" r="1.2" fill="#AB9FF2"/>
</svg>
);
// ── Mobile detection ────────────────────────────────────────────────────────── // ── Mobile detection ──────────────────────────────────────────────────────────
function isMobileBrowser(): boolean { function isMobileBrowser(): boolean {
@ -88,50 +140,35 @@ function isMobileBrowser(): boolean {
return /Android|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(navigator.userAgent); 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 { function isInWalletBrowser(): boolean {
if (typeof window === "undefined") return false; if (typeof window === "undefined") return false;
const ua = navigator.userAgent.toLowerCase(); const ua = navigator.userAgent.toLowerCase();
const w = window as unknown as Record<string, unknown>; const w = window as unknown as Record<string, unknown>;
const eth = w.ethereum as { isMetaMask?: boolean; isTrust?: boolean; isTrustWallet?: boolean; isOKExWallet?: boolean; isOkxWallet?: boolean } | undefined; const eth = w.ethereum as EthProvider | undefined;
return !!( return !!(
eth?.isMetaMask || eth?.isMetaMask ||
eth?.isTrust || eth?.isTrust ||
eth?.isTrustWallet || eth?.isTrustWallet ||
eth?.isOKExWallet || eth?.isOKExWallet ||
eth?.isOkxWallet || eth?.isOkxWallet ||
eth?.isTokenPocket ||
eth?.isBitkeep ||
eth?.isBitgetWallet ||
eth?.isRabby ||
eth?.isSafePal ||
eth?.isImToken ||
ua.includes("metamask") || ua.includes("metamask") ||
ua.includes("trust") || ua.includes("trust") ||
ua.includes("okex") || ua.includes("okex") ||
ua.includes("tokenpocket") || ua.includes("tokenpocket") ||
ua.includes("bitkeep") ua.includes("bitkeep") ||
ua.includes("imtoken") ||
ua.includes("safepal")
); );
} }
// Build DeepLink URL for opening current page in wallet's in-app browser
function buildDeepLink(walletScheme: string): string {
const currentUrl = typeof window !== "undefined" ? window.location.href : "https://pre-sale.newassetchain.io";
// Remove protocol from URL for deeplink
const urlWithoutProtocol = currentUrl.replace(/^https?:\/\//, "");
return `${walletScheme}${urlWithoutProtocol}`;
}
// ── Provider detection helpers ──────────────────────────────────────────────── // ── 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<unknown>;
};
function getEth(): EthProvider | null { function getEth(): EthProvider | null {
if (typeof window === "undefined") return null; if (typeof window === "undefined") return null;
return (window as unknown as { ethereum?: EthProvider }).ethereum ?? null; return (window as unknown as { ethereum?: EthProvider }).ethereum ?? null;
@ -148,6 +185,18 @@ function getBitget(): EthProvider | null {
return w.bitkeep?.ethereum ?? null; return w.bitkeep?.ethereum ?? null;
} }
function getSafePal(): EthProvider | null {
if (typeof window === "undefined") return null;
const w = window as unknown as { safepalProvider?: EthProvider };
return w.safepalProvider ?? null;
}
function getImToken(): EthProvider | null {
if (typeof window === "undefined") return null;
const w = window as unknown as { imToken?: EthProvider };
return w.imToken ?? null;
}
// Find a specific provider from the providers array or direct injection // Find a specific provider from the providers array or direct injection
function findProvider(predicate: (p: EthProvider) => boolean): EthProvider | null { function findProvider(predicate: (p: EthProvider) => boolean): EthProvider | null {
const eth = getEth(); const eth = getEth();
@ -158,15 +207,16 @@ 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> { // Connect to a specific provider and return { address, provider }
async function connectProvider(provider: EthProvider): Promise<{ address: string; provider: 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];
if (!address) return null;
return { address, provider };
} catch (err: unknown) { } catch (err: unknown) {
const error = err as { code?: number; message?: string }; const error = err as { code?: number; message?: string };
// User rejected
if (error?.code === 4001) throw new Error("user_rejected"); if (error?.code === 4001) throw new Error("user_rejected");
// MetaMask not initialized / locked
if (error?.code === -32002) throw new Error("wallet_pending"); if (error?.code === -32002) throw new Error("wallet_pending");
throw err; throw err;
} }
@ -175,29 +225,20 @@ async function requestAccounts(provider: EthProvider): Promise<string | null> {
// ── Wallet definitions ──────────────────────────────────────────────────────── // ── Wallet definitions ────────────────────────────────────────────────────────
function buildWallets(): WalletInfo[] { function buildWallets(): WalletInfo[] {
const currentUrl = typeof window !== "undefined" ? window.location.href : "https://pre-sale.newassetchain.io";
const urlNoProto = currentUrl.replace(/^https?:\/\//, "");
return [ return [
{ {
id: "metamask", id: "metamask",
name: "MetaMask", name: "MetaMask",
icon: <MetaMaskIcon />, icon: <MetaMaskIcon />,
installUrl: "https://metamask.io/download/", installUrl: "https://metamask.io/download/",
mobileDeepLink: buildDeepLink("https://metamask.app.link/dapp/"), mobileDeepLink: `https://metamask.app.link/dapp/${urlNoProto}`,
isInstalled: () => !!findProvider(p => !!p.isMetaMask), isInstalled: () => !!findProvider(p => !!p.isMetaMask),
connect: async () => { connect: async () => {
const p = findProvider(p => !!p.isMetaMask) ?? getEth(); const p = findProvider(p => !!p.isMetaMask) ?? getEth();
return p ? requestAccounts(p) : null; return p ? connectProvider(p) : null;
},
},
{
id: "trust",
name: "Trust Wallet",
icon: <TrustWalletIcon />,
installUrl: "https://trustwallet.com/download",
mobileDeepLink: buildDeepLink("https://link.trustwallet.com/open_url?coin_id=60&url=https://"),
isInstalled: () => !!findProvider(p => !!(p.isTrust || p.isTrustWallet)),
connect: async () => {
const p = findProvider(p => !!(p.isTrust || p.isTrustWallet)) ?? getEth();
return p ? requestAccounts(p) : null;
}, },
}, },
{ {
@ -205,22 +246,23 @@ function buildWallets(): WalletInfo[] {
name: "OKX Wallet", name: "OKX Wallet",
icon: <OKXIcon />, icon: <OKXIcon />,
installUrl: "https://www.okx.com/web3", installUrl: "https://www.okx.com/web3",
mobileDeepLink: buildDeepLink("okx://wallet/dapp/url?dappUrl=https://"), mobileDeepLink: `okx://wallet/dapp/url?dappUrl=${encodeURIComponent(currentUrl)}`,
isInstalled: () => !!(getOKX() || findProvider(p => !!(p.isOKExWallet || p.isOkxWallet))), isInstalled: () => !!(getOKX() || findProvider(p => !!(p.isOKExWallet || p.isOkxWallet))),
connect: async () => { connect: async () => {
const p = getOKX() ?? findProvider(p => !!(p.isOKExWallet || p.isOkxWallet)); const p = getOKX() ?? findProvider(p => !!(p.isOKExWallet || p.isOkxWallet));
return p ? requestAccounts(p) : null; return p ? connectProvider(p) : null;
}, },
}, },
{ {
id: "coinbase", id: "trust",
name: "Coinbase Wallet", name: "Trust Wallet",
icon: <CoinbaseIcon />, icon: <TrustWalletIcon />,
installUrl: "https://www.coinbase.com/wallet/downloads", installUrl: "https://trustwallet.com/download",
isInstalled: () => !!findProvider(p => !!p.isCoinbaseWallet), mobileDeepLink: `https://link.trustwallet.com/open_url?coin_id=60&url=${encodeURIComponent(currentUrl)}`,
isInstalled: () => !!findProvider(p => !!(p.isTrust || p.isTrustWallet)),
connect: async () => { connect: async () => {
const p = findProvider(p => !!p.isCoinbaseWallet) ?? getEth(); const p = findProvider(p => !!(p.isTrust || p.isTrustWallet)) ?? getEth();
return p ? requestAccounts(p) : null; return p ? connectProvider(p) : null;
}, },
}, },
{ {
@ -228,10 +270,11 @@ function buildWallets(): WalletInfo[] {
name: "TokenPocket", name: "TokenPocket",
icon: <TokenPocketIcon />, icon: <TokenPocketIcon />,
installUrl: "https://www.tokenpocket.pro/en/download/app", installUrl: "https://www.tokenpocket.pro/en/download/app",
mobileDeepLink: `tpoutside://pull?param=${encodeURIComponent(JSON.stringify({ url: currentUrl }))}`,
isInstalled: () => !!findProvider(p => !!p.isTokenPocket), isInstalled: () => !!findProvider(p => !!p.isTokenPocket),
connect: async () => { connect: async () => {
const p = findProvider(p => !!p.isTokenPocket) ?? getEth(); const p = findProvider(p => !!p.isTokenPocket) ?? getEth();
return p ? requestAccounts(p) : null; return p ? connectProvider(p) : null;
}, },
}, },
{ {
@ -242,7 +285,63 @@ function buildWallets(): WalletInfo[] {
isInstalled: () => !!(getBitget() || findProvider(p => !!(p.isBitkeep || p.isBitgetWallet))), isInstalled: () => !!(getBitget() || findProvider(p => !!(p.isBitkeep || p.isBitgetWallet))),
connect: async () => { connect: async () => {
const p = getBitget() ?? findProvider(p => !!(p.isBitkeep || p.isBitgetWallet)); const p = getBitget() ?? findProvider(p => !!(p.isBitkeep || p.isBitgetWallet));
return p ? requestAccounts(p) : null; return p ? connectProvider(p) : null;
},
},
{
id: "coinbase",
name: "Coinbase Wallet",
icon: <CoinbaseIcon />,
installUrl: "https://www.coinbase.com/wallet/downloads",
isInstalled: () => !!findProvider(p => !!p.isCoinbaseWallet),
connect: async () => {
const p = findProvider(p => !!p.isCoinbaseWallet) ?? getEth();
return p ? connectProvider(p) : null;
},
},
{
id: "rabby",
name: "Rabby Wallet",
icon: <RabbyIcon />,
installUrl: "https://rabby.io/",
isInstalled: () => !!findProvider(p => !!p.isRabby),
connect: async () => {
const p = findProvider(p => !!p.isRabby) ?? getEth();
return p ? connectProvider(p) : null;
},
},
{
id: "safepal",
name: "SafePal",
icon: <SafePalIcon />,
installUrl: "https://www.safepal.com/download",
isInstalled: () => !!(getSafePal() || findProvider(p => !!p.isSafePal)),
connect: async () => {
const p = getSafePal() ?? findProvider(p => !!p.isSafePal) ?? getEth();
return p ? connectProvider(p) : null;
},
},
{
id: "imtoken",
name: "imToken",
icon: <ImTokenIcon />,
installUrl: "https://token.im/download",
mobileDeepLink: `imtokenv2://navigate/DAppBrowser?url=${encodeURIComponent(currentUrl)}`,
isInstalled: () => !!(getImToken() || findProvider(p => !!p.isImToken)),
connect: async () => {
const p = getImToken() ?? findProvider(p => !!p.isImToken) ?? getEth();
return p ? connectProvider(p) : null;
},
},
{
id: "phantom",
name: "Phantom (EVM)",
icon: <PhantomIcon />,
installUrl: "https://phantom.app/download",
isInstalled: () => !!findProvider(p => !!p.isPhantom),
connect: async () => {
const p = findProvider(p => !!p.isPhantom) ?? getEth();
return p ? connectProvider(p) : null;
}, },
}, },
]; ];
@ -257,24 +356,15 @@ function isValidEthAddress(addr: string): boolean {
function MobileDeepLinkPanel({ lang }: { lang: Lang }) { function MobileDeepLinkPanel({ lang }: { lang: Lang }) {
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 urlNoProto = currentUrl.replace(/^https?:\/\//, "");
const mobileWallets = [ const mobileWallets = [
{ {
id: "metamask", id: "metamask",
name: "MetaMask", name: "MetaMask",
icon: <MetaMaskIcon />, icon: <MetaMaskIcon />,
deepLink: `https://metamask.app.link/dapp/${urlWithoutProtocol}`, deepLink: `https://metamask.app.link/dapp/${urlNoProto}`,
installUrl: "https://metamask.io/download/", installUrl: "https://metamask.io/download/",
color: "#E27625",
},
{
id: "trust",
name: "Trust Wallet",
icon: <TrustWalletIcon />,
deepLink: `https://link.trustwallet.com/open_url?coin_id=60&url=${encodeURIComponent(currentUrl)}`,
installUrl: "https://trustwallet.com/download",
color: "#3375BB",
}, },
{ {
id: "okx", id: "okx",
@ -282,7 +372,13 @@ function MobileDeepLinkPanel({ lang }: { lang: Lang }) {
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", installUrl: "https://www.okx.com/web3",
color: "#00F0FF", },
{
id: "trust",
name: "Trust Wallet",
icon: <TrustWalletIcon />,
deepLink: `https://link.trustwallet.com/open_url?coin_id=60&url=${encodeURIComponent(currentUrl)}`,
installUrl: "https://trustwallet.com/download",
}, },
{ {
id: "tokenpocket", id: "tokenpocket",
@ -290,13 +386,25 @@ function MobileDeepLinkPanel({ lang }: { lang: Lang }) {
icon: <TokenPocketIcon />, icon: <TokenPocketIcon />,
deepLink: `tpoutside://pull?param=${encodeURIComponent(JSON.stringify({ url: currentUrl }))}`, deepLink: `tpoutside://pull?param=${encodeURIComponent(JSON.stringify({ url: currentUrl }))}`,
installUrl: "https://www.tokenpocket.pro/en/download/app", installUrl: "https://www.tokenpocket.pro/en/download/app",
color: "#2980FE", },
{
id: "imtoken",
name: "imToken",
icon: <ImTokenIcon />,
deepLink: `imtokenv2://navigate/DAppBrowser?url=${encodeURIComponent(currentUrl)}`,
installUrl: "https://token.im/download",
},
{
id: "safepal",
name: "SafePal",
icon: <SafePalIcon />,
deepLink: `safepal://browser?url=${encodeURIComponent(currentUrl)}`,
installUrl: "https://www.safepal.com/download",
}, },
]; ];
return ( return (
<div className="space-y-3"> <div className="space-y-3">
{/* Mobile guidance header */}
<div <div
className="rounded-xl p-4" className="rounded-xl p-4"
style={{ background: "rgba(240,180,41,0.08)", border: "1px solid rgba(240,180,41,0.25)" }} style={{ background: "rgba(240,180,41,0.08)", border: "1px solid rgba(240,180,41,0.25)" }}
@ -309,14 +417,13 @@ function MobileDeepLinkPanel({ lang }: { lang: Lang }) {
</p> </p>
<p className="text-xs text-white/50 leading-relaxed"> <p className="text-xs text-white/50 leading-relaxed">
{lang === "zh" {lang === "zh"
? "手机浏览器不支持钱包扩展。请选择以下任一钱包 App在其内置浏览器中打开本页面即可连接钱包。" ? "请选择以下钱包 App在其内置浏览器中打开本页面即可连接钱包。"
: "Mobile browsers don't support wallet extensions. Open this page in a wallet app's built-in browser to connect."} : "Open this page in a wallet app's built-in browser to connect."}
</p> </p>
</div> </div>
</div> </div>
</div> </div>
{/* Wallet DeepLink buttons */}
<div className="space-y-2"> <div className="space-y-2">
<p className="text-xs text-white/40 text-center"> <p className="text-xs text-white/40 text-center">
{lang === "zh" ? "选择钱包 App 打开本页面" : "Choose a wallet app to open this page"} {lang === "zh" ? "选择钱包 App 打开本页面" : "Choose a wallet app to open this page"}
@ -348,7 +455,6 @@ function MobileDeepLinkPanel({ lang }: { lang: Lang }) {
))} ))}
</div> </div>
{/* Step guide */}
<div <div
className="rounded-xl p-3 space-y-2" 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)" }} style={{ background: "rgba(255,255,255,0.03)", border: "1px solid rgba(255,255,255,0.08)" }}
@ -384,7 +490,6 @@ export function WalletSelector({ lang, onAddressDetected, connectedAddress, comp
const detectWallets = useCallback(() => { const detectWallets = useCallback(() => {
setDetecting(true); setDetecting(true);
setError(null); setError(null);
// Wait for wallet extensions to fully inject (up to 1500ms)
const timer = setTimeout(() => { const timer = setTimeout(() => {
setWallets(buildWallets()); setWallets(buildWallets());
setDetecting(false); setDetecting(false);
@ -401,9 +506,9 @@ 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); onAddressDetected(result.address, result.provider);
} else { } else {
setError(lang === "zh" ? "未获取到地址,请重试" : "No address returned, please try again"); setError(lang === "zh" ? "未获取到地址,请重试" : "No address returned, please try again");
} }
@ -436,7 +541,15 @@ export function WalletSelector({ lang, onAddressDetected, connectedAddress, comp
return; return;
} }
setManualError(null); setManualError(null);
onAddressDetected(addr); // For manual input, use window.ethereum as fallback provider
const fallbackProvider = (window as unknown as { ethereum?: EthProvider }).ethereum;
if (fallbackProvider) {
onAddressDetected(addr, fallbackProvider);
} else {
// No provider available — create a minimal stub so the callback still works
const stub = { request: async () => { throw new Error("no provider"); } } as unknown as EthProvider;
onAddressDetected(addr, stub);
}
}; };
const installedWallets = wallets.filter(w => w.isInstalled()); const installedWallets = wallets.filter(w => w.isInstalled());
@ -469,7 +582,6 @@ export function WalletSelector({ lang, onAddressDetected, connectedAddress, comp
<div className="space-y-3"> <div className="space-y-3">
<MobileDeepLinkPanel lang={lang} /> <MobileDeepLinkPanel lang={lang} />
{/* Manual address fallback */}
<div className="pt-1"> <div className="pt-1">
<button <button
onClick={() => { setShowManual(!showManual); setManualError(null); }} onClick={() => { setShowManual(!showManual); setManualError(null); }}
@ -529,7 +641,7 @@ export function WalletSelector({ lang, onAddressDetected, connectedAddress, comp
<div className="space-y-3"> <div className="space-y-3">
<div className="flex items-center justify-between"> <div className="flex items-center justify-between">
<p className="text-xs font-semibold text-white/60 uppercase tracking-wider"> <p className="text-xs font-semibold text-white/60 uppercase tracking-wider">
{lang === "zh" ? "选择钱包自动填充地址" : "Select wallet to auto-fill address"} {lang === "zh" ? "选择钱包连接" : "Select wallet to connect"}
</p> </p>
</div> </div>
<div className="flex items-center justify-center py-4 gap-2"> <div className="flex items-center justify-center py-4 gap-2">
@ -549,9 +661,8 @@ export function WalletSelector({ lang, onAddressDetected, connectedAddress, comp
<div className="space-y-3"> <div className="space-y-3">
<div className="flex items-center justify-between"> <div className="flex items-center justify-between">
<p className="text-xs font-semibold text-white/60 uppercase tracking-wider"> <p className="text-xs font-semibold text-white/60 uppercase tracking-wider">
{lang === "zh" ? "选择钱包自动填充地址" : "Select wallet to auto-fill address"} {lang === "zh" ? "选择钱包连接" : "Select wallet to connect"}
</p> </p>
{/* Refresh detection button */}
<button <button
onClick={detectWallets} onClick={detectWallets}
disabled={detecting} disabled={detecting}
@ -637,7 +748,7 @@ export function WalletSelector({ lang, onAddressDetected, connectedAddress, comp
<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)"}
</p> </p>
<div className="grid grid-cols-3 gap-2"> <div className="grid grid-cols-4 gap-2">
{notInstalledWallets.map(wallet => ( {notInstalledWallets.map(wallet => (
<a <a
key={wallet.id} key={wallet.id}
@ -661,7 +772,7 @@ export function WalletSelector({ lang, onAddressDetected, connectedAddress, comp
{/* In compact mode, show install links inline */} {/* In compact mode, show install links inline */}
{compact && notInstalledWallets.length > 0 && installedWallets.length === 0 && ( {compact && notInstalledWallets.length > 0 && installedWallets.length === 0 && (
<div className="flex flex-wrap gap-2"> <div className="flex flex-wrap gap-2">
{notInstalledWallets.slice(0, 4).map(wallet => ( {notInstalledWallets.slice(0, 5).map(wallet => (
<a <a
key={wallet.id} key={wallet.id}
href={wallet.installUrl} href={wallet.installUrl}
@ -685,7 +796,7 @@ export function WalletSelector({ lang, onAddressDetected, connectedAddress, comp
<p className="text-xs text-red-400 text-center">{error}</p> <p className="text-xs text-red-400 text-center">{error}</p>
)} )}
{/* Manual address input — divider */} {/* Manual address input */}
<div className="pt-1"> <div className="pt-1">
<button <button
onClick={() => { setShowManual(!showManual); setManualError(null); }} onClick={() => { setShowManual(!showManual); setManualError(null); }}

View File

@ -1,10 +1,10 @@
// NAC XIC Presale — Wallet Connection Hook // NAC XIC Presale — Wallet Connection Hook
// Supports MetaMask, Trust Wallet, OKX Wallet, Coinbase Wallet, and all EVM-compatible wallets // Supports MetaMask, Trust Wallet, OKX Wallet, Coinbase Wallet, and all EVM-compatible wallets
// v4: added forceConnect(address) for WalletSelector callback sync // v5: forceConnect accepts specific provider, exposes watchAsset method
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";
import { shortenAddress, switchToNetwork } from "@/lib/contracts"; import { shortenAddress, switchToNetwork, CONTRACTS, PRESALE_CONFIG } from "@/lib/contracts";
export type NetworkType = "BSC" | "ETH" | "TRON"; export type NetworkType = "BSC" | "ETH" | "TRON";
@ -62,19 +62,16 @@ export function detectProvider(): Eip1193Provider | null {
// Check if MetaMask is installed but not yet initialized (no wallet created/imported) // Check if MetaMask is installed but not yet initialized (no wallet created/imported)
export async function checkWalletReady(rawProvider: Eip1193Provider): Promise<{ ready: boolean; reason?: string }> { export async function checkWalletReady(rawProvider: Eip1193Provider): Promise<{ ready: boolean; reason?: string }> {
try { 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<string[]> }).request({ const accounts = await (rawProvider as { request: (args: { method: string }) => Promise<string[]> }).request({
method: "eth_accounts", method: "eth_accounts",
}); });
// If we get here, the wallet is at least initialized (even if locked / no accounts) void accounts;
return { ready: true }; return { ready: true };
} catch (err: unknown) { } catch (err: unknown) {
const error = err as { code?: number; message?: string }; const error = err as { code?: number; message?: string };
// -32002: Request already pending (MetaMask not initialized or another request pending)
if (error?.code === -32002) { if (error?.code === -32002) {
return { ready: false, reason: "pending" }; return { ready: false, reason: "pending" };
} }
// Any other error — treat as not ready
return { ready: false, reason: error?.message || "unknown" }; return { ready: false, reason: error?.message || "unknown" };
} }
} }
@ -122,6 +119,8 @@ export function useWallet() {
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);
// Track the raw provider used for the current connection (for watchAsset, switchNetwork, etc.)
const rawProviderRef = useRef<Eip1193Provider | null>(null);
useEffect(() => { useEffect(() => {
mountedRef.current = true; mountedRef.current = true;
@ -144,7 +143,6 @@ export function useWallet() {
setState(s => ({ ...s, isConnecting: true, error: null })); setState(s => ({ ...s, isConnecting: true, error: null }));
try { try {
// Request accounts — this triggers the wallet popup
const accounts = await (rawProvider as { const accounts = await (rawProvider as {
request: (args: { method: string; params?: unknown[] }) => Promise<string[]> request: (args: { method: string; params?: unknown[] }) => Promise<string[]>
}).request({ }).request({
@ -156,6 +154,7 @@ export function useWallet() {
throw new Error("no_accounts"); throw new Error("no_accounts");
} }
rawProviderRef.current = rawProvider;
const partial = await buildWalletState(rawProvider, accounts[0]); const partial = await buildWalletState(rawProvider, accounts[0]);
if (mountedRef.current) setState({ ...INITIAL_STATE, ...partial }); if (mountedRef.current) setState({ ...INITIAL_STATE, ...partial });
return { success: true }; return { success: true };
@ -165,10 +164,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) {
// MetaMask has a pending request — usually means it's not initialized or popup is already open
msg = "钱包请求处理中,请检查 MetaMask 弹窗。如未弹出,请先完成 MetaMask 初始化设置(创建或导入钱包),然后刷新页面重试。"; msg = "钱包请求处理中,请检查 MetaMask 弹窗。如未弹出,请先完成 MetaMask 初始化设置(创建或导入钱包),然后刷新页面重试。";
} else if (error?.message === "no_accounts") { } else if (error?.message === "no_accounts") {
msg = "未获取到账户,请确认钱包已解锁并授权此网站。"; msg = "未获取到账户,请确认钱包已解锁并授权此网站。";
@ -187,14 +184,14 @@ export function useWallet() {
} }
}, []); }, []);
// ── Force connect with known address (from WalletSelector callback) ───────── // ── Force connect with known address and specific provider (from WalletSelector callback) ─────
// Use this when WalletSelector has already called eth_requestAccounts and got the address. // Use this when WalletSelector has already called eth_requestAccounts and got the address.
// Directly builds wallet state without triggering another popup. // Pass the specific provider used (OKX/MetaMask/TP) so all subsequent operations use the right wallet.
const forceConnect = useCallback(async (address: string): Promise<void> => { const forceConnect = useCallback(async (address: string, specificProvider?: Eip1193Provider): Promise<void> => {
if (!address) return; if (!address) return;
const rawProvider = detectProvider(); // Use the provided specific provider first, then fall back to detectProvider
const rawProvider = specificProvider ?? detectProvider();
if (!rawProvider) { if (!rawProvider) {
// No provider available — set minimal connected state with just the address
if (mountedRef.current) { if (mountedRef.current) {
setState({ setState({
...INITIAL_STATE, ...INITIAL_STATE,
@ -205,11 +202,11 @@ export function useWallet() {
} }
return; return;
} }
rawProviderRef.current = rawProvider;
try { try {
const partial = await buildWalletState(rawProvider, address); const partial = await buildWalletState(rawProvider, address);
if (mountedRef.current) setState({ ...INITIAL_STATE, ...partial }); if (mountedRef.current) setState({ ...INITIAL_STATE, ...partial });
} catch { } catch {
// Fallback: set minimal state
if (mountedRef.current) { if (mountedRef.current) {
setState({ setState({
...INITIAL_STATE, ...INITIAL_STATE,
@ -223,16 +220,18 @@ export function useWallet() {
// ── Disconnect ────────────────────────────────────────────────────────────── // ── Disconnect ──────────────────────────────────────────────────────────────
const disconnect = useCallback(() => { const disconnect = useCallback(() => {
rawProviderRef.current = null;
setState(INITIAL_STATE); setState(INITIAL_STATE);
}, []); }, []);
// ── Switch Network ────────────────────────────────────────────────────────── // ── Switch Network ──────────────────────────────────────────────────────────
const switchNetwork = useCallback(async (chainId: number) => { const switchNetwork = useCallback(async (chainId: number) => {
try { try {
await switchToNetwork(chainId); // Use the tracked raw provider (user's chosen wallet)
const rawProvider = detectProvider(); const rp = rawProviderRef.current ?? detectProvider();
if (rawProvider) { await switchToNetwork(chainId, rp as { request: (args: { method: string; params?: unknown[] }) => Promise<unknown> } | undefined);
const provider = new BrowserProvider(rawProvider); if (rp) {
const provider = new BrowserProvider(rp);
const network = await provider.getNetwork(); const network = await provider.getNetwork();
let signer: JsonRpcSigner | null = null; let signer: JsonRpcSigner | null = null;
try { signer = await provider.getSigner(); } catch { /* ignore */ } try { signer = await provider.getSigner(); } catch { /* ignore */ }
@ -251,6 +250,32 @@ export function useWallet() {
} }
}, []); }, []);
// ── Watch Asset (add token to wallet) ──────────────────────────────────────
// Calls wallet_watchAsset on the user's connected wallet (correct provider)
const watchAsset = useCallback(async (network: "BSC" | "ETH" = "BSC"): Promise<boolean> => {
const rp = rawProviderRef.current ?? detectProvider();
if (!rp) return false;
try {
const tokenAddress = network === "BSC" ? CONTRACTS.BSC.token : CONTRACTS.ETH.token;
if (!tokenAddress) return false;
await (rp as { request: (args: { method: string; params?: unknown[] }) => Promise<unknown> }).request({
method: "wallet_watchAsset",
params: [{
type: "ERC20",
options: {
address: tokenAddress,
symbol: PRESALE_CONFIG.tokenSymbol,
decimals: PRESALE_CONFIG.tokenDecimals,
image: "https://d2xsxph8kpxj0f.cloudfront.net/310519663287655625/Ngki3MumDNGduV3xJt3mga/nac-token-icon_382e5c30.png",
},
}],
});
return true;
} catch {
return false;
}
}, []);
// ── Auto-detect on page load (silent, no popup) ───────────────────────────── // ── Auto-detect on page load (silent, no popup) ─────────────────────────────
useEffect(() => { useEffect(() => {
let cancelled = false; let cancelled = false;
@ -268,10 +293,11 @@ export function useWallet() {
try { try {
const accounts = await (rawProvider as { request: (args: { method: string }) => Promise<string[]> }).request({ const accounts = await (rawProvider as { request: (args: { method: string }) => Promise<string[]> }).request({
method: "eth_accounts", // Silent — no popup method: "eth_accounts",
}); });
if (cancelled) return; if (cancelled) return;
if (accounts && accounts.length > 0) { if (accounts && accounts.length > 0) {
rawProviderRef.current = rawProvider;
const partial = await buildWalletState(rawProvider, accounts[0]); const partial = await buildWalletState(rawProvider, accounts[0]);
if (!cancelled && mountedRef.current) { if (!cancelled && mountedRef.current) {
setState({ ...INITIAL_STATE, ...partial }); setState({ ...INITIAL_STATE, ...partial });
@ -307,6 +333,7 @@ export function useWallet() {
const accs = accounts as string[]; const accs = accounts as string[];
if (!mountedRef.current) return; if (!mountedRef.current) return;
if (!accs || accs.length === 0) { if (!accs || accs.length === 0) {
rawProviderRef.current = null;
setState(INITIAL_STATE); setState(INITIAL_STATE);
} else { } else {
try { try {
@ -356,5 +383,5 @@ export function useWallet() {
}; };
}, []); }, []);
return { ...state, connect, forceConnect, disconnect, switchNetwork }; return { ...state, connect, forceConnect, disconnect, switchNetwork, watchAsset };
} }

View File

@ -199,12 +199,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">
@ -213,7 +213,7 @@ function TRC20Panel({ usdtAmount, lang, connectedAddress, onConnectWallet }: { u
<WalletSelector <WalletSelector
lang={lang} lang={lang}
connectedAddress={connectedAddress} connectedAddress={connectedAddress}
onAddressDetected={(addr) => { onAddressDetected={(addr, _provider) => {
setEvmAddress(addr); setEvmAddress(addr);
setEvmAddrError(""); setEvmAddrError("");
toast.success(lang === "zh" ? "XIC接收地址已自动填充" : "XIC receiving address auto-filled!"); toast.success(lang === "zh" ? "XIC接收地址已自动填充" : "XIC receiving address auto-filled!");
@ -420,12 +420,16 @@ function EVMPurchasePanel({ network, lang, wallet }: { network: "BSC" | "ETH"; l
<WalletSelector <WalletSelector
lang={lang} lang={lang}
connectedAddress={wallet.address ?? undefined} connectedAddress={wallet.address ?? undefined}
onAddressDetected={async (addr) => { onAddressDetected={async (addr, provider) => {
// Use forceConnect to directly sync wallet state with the detected address // Use the specific provider from WalletSelector (OKX/MetaMask/TP etc.)
// (avoids triggering another eth_requestAccounts popup) // This ensures all subsequent operations use the correct wallet
await wallet.forceConnect(addr); await wallet.forceConnect(addr, provider);
setShowWalletModal(false); setShowWalletModal(false);
toast.success(lang === "zh" ? `钱包已连接: ${addr.slice(0, 6)}...${addr.slice(-4)}` : `Wallet connected: ${addr.slice(0, 6)}...${addr.slice(-4)}`); toast.success(lang === "zh" ? `钱包已连接: ${addr.slice(0, 6)}...${addr.slice(-4)}` : `Wallet connected: ${addr.slice(0, 6)}...${addr.slice(-4)}`);
// Auto-trigger watchAsset so the wallet pops open and user can see it's connected
setTimeout(async () => {
try { await wallet.watchAsset(network); } catch { /* ignore */ }
}, 800);
}} }}
compact compact
/> />
@ -455,24 +459,13 @@ function EVMPurchasePanel({ network, lang, wallet }: { network: "BSC" | "ETH"; l
if (purchaseState.step === "success") { if (purchaseState.step === "success") {
const handleAddToken = async () => { const handleAddToken = async () => {
try { try {
const ethereum = (window as unknown as Record<string, unknown>).ethereum as { request?: (args: { method: string; params?: unknown[] }) => Promise<unknown> } | undefined; // Use wallet.watchAsset() which uses the user's connected wallet provider (not window.ethereum)
if (!ethereum?.request) { const success = await wallet.watchAsset(network);
if (success) {
toast.success(t("add_token_success"));
} else {
toast.error(t("add_token_fail")); toast.error(t("add_token_fail"));
return;
} }
await ethereum.request({
method: 'wallet_watchAsset',
params: [{
type: 'ERC20',
options: {
address: '0xc65e7A2738eD884dB8d26a6eb2fEcF7daCA2e90C',
symbol: 'XIC',
decimals: 18,
image: 'https://d2xsxph8kpxj0f.cloudfront.net/310519663287655625/Ngki3MumDNGduV3xJt3mga/nac-token-icon_382e5c30.png',
},
}],
});
toast.success(t("add_token_success"));
} catch { } catch {
toast.error(t("add_token_fail")); toast.error(t("add_token_fail"));
} }
@ -867,7 +860,7 @@ function ChatSupport({ lang }: { lang: Lang }) {
// ─── Navbar Wallet Button ───────────────────────────────────────────────────── // ─── Navbar Wallet Button ─────────────────────────────────────────────────────
type WalletHookReturn = ReturnType<typeof useWallet>; type WalletHookReturn = ReturnType<typeof useWallet>;
function NavWalletButton({ lang, wallet }: { lang: Lang; wallet: WalletHookReturn }) { function NavWalletButton({ lang, wallet, onNetworkDetected }: { lang: Lang; wallet: WalletHookReturn; onNetworkDetected?: (chainId: number) => void }) {
const { t } = useTranslation(lang); const { t } = useTranslation(lang);
const [showMenu, setShowMenu] = useState(false); const [showMenu, setShowMenu] = useState(false);
const [showWalletModal, setShowWalletModal] = useState(false); const [showWalletModal, setShowWalletModal] = useState(false);
@ -963,17 +956,23 @@ function NavWalletButton({ lang, wallet }: { lang: Lang; wallet: WalletHookRetur
<WalletSelector <WalletSelector
lang={lang} lang={lang}
connectedAddress={wallet.address ?? undefined} connectedAddress={wallet.address ?? undefined}
onAddressDetected={async (addr) => { onAddressDetected={async (addr, provider) => {
// After address detected from WalletSelector, sync wallet state // Use the specific provider from WalletSelector (OKX/MetaMask/TP etc.)
const result = await wallet.connect(); await wallet.forceConnect(addr, provider);
if (result.success) { setShowWalletModal(false);
setShowWalletModal(false); toast.success(lang === "zh" ? `钱包已连接: ${addr.slice(0, 6)}...${addr.slice(-4)}` : `Wallet connected: ${addr.slice(0, 6)}...${addr.slice(-4)}`);
toast.success(lang === "zh" ? `钱包已连接: ${addr.slice(0, 6)}...${addr.slice(-4)}` : `Wallet connected: ${addr.slice(0, 6)}...${addr.slice(-4)}`); // Auto-switch to the network matching the wallet's current chain
} else { setTimeout(() => {
// Even if connect() failed, we have the address — close modal const chainId = wallet.chainId;
setShowWalletModal(false); if (chainId && onNetworkDetected) onNetworkDetected(chainId);
toast.success(lang === "zh" ? `地址已确认: ${addr.slice(0, 6)}...${addr.slice(-4)}` : `Address confirmed: ${addr.slice(0, 6)}...${addr.slice(-4)}`); }, 300);
} // Auto-trigger watchAsset so wallet pops open confirming connection
setTimeout(async () => {
try {
const net = wallet.chainId === 1 ? "ETH" : "BSC";
await wallet.watchAsset(net);
} catch { /* ignore */ }
}, 1000);
}} }}
/> />
</div> </div>
@ -1116,7 +1115,10 @@ export default function Home() {
</span> </span>
</Link> </Link>
<LangToggle lang={lang} setLang={setLang} /> <LangToggle lang={lang} setLang={setLang} />
<NavWalletButton lang={lang} wallet={wallet} /> <NavWalletButton lang={lang} wallet={wallet} onNetworkDetected={(chainId) => {
if (chainId === 1) setActiveNetwork("ETH");
else if (chainId === 56) setActiveNetwork("BSC");
}} />
</div> </div>
</nav> </nav>
@ -1235,6 +1237,67 @@ export default function Home() {
<span className="text-xs font-medium text-white/80 counter-digit">{value}</span> <span className="text-xs font-medium text-white/80 counter-digit">{value}</span>
</div> </div>
))} ))}
{/* Contract Address Row with Copy Button */}
<div className="flex items-center justify-between py-1" style={{ borderBottom: "1px solid rgba(255,255,255,0.04)" }}>
<span className="text-xs text-white/40">{t("token_contract_addr")}</span>
<div className="flex items-center gap-1.5">
<span className="text-xs font-medium text-white/60 counter-digit" style={{ fontSize: "10px" }}>
{CONTRACTS.BSC.token.slice(0, 6)}...{CONTRACTS.BSC.token.slice(-4)}
</span>
<button
onClick={() => {
navigator.clipboard.writeText(CONTRACTS.BSC.token);
toast.success(lang === "zh" ? "已复制合约地址" : "Contract address copied!");
}}
className="flex items-center justify-center w-5 h-5 rounded transition-all hover:bg-white/10"
style={{ color: "#00d4ff" }}
title={lang === "zh" ? "复制地址" : "Copy address"}
>
<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
<rect x="9" y="9" width="13" height="13" rx="2" ry="2"/>
<path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"/>
</svg>
</button>
</div>
</div>
{/* Add XIC to Wallet — calls wallet_watchAsset to open MetaMask/OKX add-token dialog */}
<button
onClick={async () => {
try {
const ethereum = (window as unknown as Record<string, unknown>).ethereum as
| { request?: (args: { method: string; params?: unknown[] }) => Promise<unknown> }
| undefined;
if (!ethereum?.request) {
toast.error(lang === "zh" ? "请先安装 MetaMask 或 OKX 钱包" : "Please install MetaMask or OKX Wallet first");
return;
}
await ethereum.request({
method: 'wallet_watchAsset',
params: [{
type: 'ERC20',
options: {
address: CONTRACTS.BSC.token,
symbol: 'XIC',
decimals: 18,
image: 'https://d2xsxph8kpxj0f.cloudfront.net/310519663287655625/Ngki3MumDNGduV3xJt3mga/nac-token-icon_382e5c30.png',
},
}],
});
toast.success(lang === "zh" ? "XIC 已添加到钱包" : "XIC added to wallet!");
} catch {
toast.error(lang === "zh" ? "添加失败,请重试" : "Failed to add token, please try again");
}
}}
className="w-full py-2.5 rounded-xl text-xs font-semibold flex items-center justify-center gap-2 transition-all hover:opacity-90 active:scale-[0.98]"
style={{ background: "rgba(240,180,41,0.12)", border: "1px solid rgba(240,180,41,0.35)", color: "#f0b429" }}
>
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
<path d="M21 12V7H5a2 2 0 0 1 0-4h14v4"/>
<path d="M3 5v14a2 2 0 0 0 2 2h16v-5"/>
<path d="M18 12a2 2 0 0 0 0 4h4v-4z"/>
</svg>
{lang === "zh" ? "添加 XIC 到钱包" : "Add XIC to Wallet"}
</button>
<a <a
href={`https://bscscan.com/address/${CONTRACTS.BSC.token}`} href={`https://bscscan.com/address/${CONTRACTS.BSC.token}`}
target="_blank" target="_blank"
@ -1262,55 +1325,23 @@ export default function Home() {
</div> </div>
</div> </div>
{/* How to Buy Guide — 3-Step Cards */} {/* How to Buy Guide — 3 Text Paragraphs */}
<div className="mb-6"> <div className="mb-6 rounded-xl p-4 space-y-3" style={{ background: "rgba(255,255,255,0.03)", border: "1px solid rgba(255,255,255,0.07)" }}>
<p className="text-xs font-semibold uppercase tracking-widest text-white/40 mb-3">{t("guide_title")}</p> <p className="text-xs font-semibold uppercase tracking-widest text-white/40">{t("guide_title")}</p>
<div className="grid grid-cols-1 md:grid-cols-3 gap-3"> {/* Step 1 */}
{/* Step 1 */} <div>
<div className="rounded-xl p-3 space-y-2" style={{ background: "rgba(0,212,255,0.05)", border: "1px solid rgba(0,212,255,0.2)" }}> <span className="text-xs font-semibold" style={{ color: "#00d4ff" }}>{t("guide_step1_title")}</span>
<div className="flex items-center gap-2"> <span className="text-xs text-white/60">{t("guide_step1_1")}{t("guide_step1_2")}{t("guide_step1_3")}</span>
<div className="w-6 h-6 rounded-full flex items-center justify-center text-xs font-bold" style={{ background: "rgba(0,212,255,0.2)", color: "#00d4ff" }}>1</div> </div>
<span className="text-xs font-semibold" style={{ color: "#00d4ff" }}>{t("guide_step1_title")}</span> {/* Step 2 */}
</div> <div>
<ul className="space-y-1"> <span className="text-xs font-semibold" style={{ color: "#f0b429" }}>{t("guide_step2_title")}</span>
{[t("guide_step1_1"), t("guide_step1_2"), t("guide_step1_3")].map((item, i) => ( <span className="text-xs text-white/60">{t("guide_step2_1")}{t("guide_step2_2")}{t("guide_step2_3")}{t("guide_step2_4")}</span>
<li key={i} className="text-xs text-white/60 flex items-start gap-1.5"> </div>
<span className="text-white/30 mt-0.5 flex-shrink-0"></span> {/* Step 3 */}
<span>{item}</span> <div>
</li> <span className="text-xs font-semibold" style={{ color: "#00e676" }}>{t("guide_step3_title")}</span>
))} <span className="text-xs text-white/60">{t("guide_step3_1")}{t("guide_step3_2")}{t("guide_step3_3")}{t("guide_step3_4")}</span>
</ul>
</div>
{/* Step 2 */}
<div className="rounded-xl p-3 space-y-2" style={{ background: "rgba(240,180,41,0.05)", border: "1px solid rgba(240,180,41,0.2)" }}>
<div className="flex items-center gap-2">
<div className="w-6 h-6 rounded-full flex items-center justify-center text-xs font-bold" style={{ background: "rgba(240,180,41,0.2)", color: "#f0b429" }}>2</div>
<span className="text-xs font-semibold" style={{ color: "#f0b429" }}>{t("guide_step2_title")}</span>
</div>
<ul className="space-y-1">
{[t("guide_step2_1"), t("guide_step2_2"), t("guide_step2_3"), t("guide_step2_4")].map((item, i) => (
<li key={i} className="text-xs text-white/60 flex items-start gap-1.5">
<span className="text-white/30 mt-0.5 flex-shrink-0"></span>
<span>{item}</span>
</li>
))}
</ul>
</div>
{/* Step 3 */}
<div className="rounded-xl p-3 space-y-2" style={{ background: "rgba(0,230,118,0.05)", border: "1px solid rgba(0,230,118,0.2)" }}>
<div className="flex items-center gap-2">
<div className="w-6 h-6 rounded-full flex items-center justify-center text-xs font-bold" style={{ background: "rgba(0,230,118,0.2)", color: "#00e676" }}>3</div>
<span className="text-xs font-semibold" style={{ color: "#00e676" }}>{t("guide_step3_title")}</span>
</div>
<ul className="space-y-1">
{[t("guide_step3_1"), t("guide_step3_2"), t("guide_step3_3"), t("guide_step3_4")].map((item, i) => (
<li key={i} className="text-xs text-white/60 flex items-start gap-1.5">
<span className="text-white/30 mt-0.5 flex-shrink-0"></span>
<span>{item}</span>
</li>
))}
</ul>
</div>
</div> </div>
</div> </div>