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:
parent
8bd78c3fc0
commit
706eead8b3
|
|
@ -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); }}
|
||||||
|
|
|
||||||
|
|
@ -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 };
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue