Checkpoint: v7: 多钱包列表选择器(MetaMask/Trust Wallet/OKX/Coinbase/TokenPocket 检测+连接+安装引导),集成到 BSC/ETH 面板和 TRON 面板;重写 useWallet hook 支持所有主流 EVM 钱包自动识别;文案"EVM 地址"→"XIC 接收地址";后台一键开启/关闭预售功能;已部署到 pre-sale.newassetchain.io
This commit is contained in:
parent
809b6327b8
commit
81c9b2544f
|
|
@ -0,0 +1,372 @@
|
|||
// NAC XIC Presale — WalletSelector Component
|
||||
// Detects installed EVM wallets and shows connect/install buttons for each
|
||||
|
||||
import { useState, useEffect } from "react";
|
||||
|
||||
type Lang = "zh" | "en";
|
||||
|
||||
interface WalletInfo {
|
||||
id: string;
|
||||
name: string;
|
||||
icon: React.ReactNode;
|
||||
installUrl: string;
|
||||
isInstalled: () => boolean;
|
||||
connect: () => Promise<string | null>;
|
||||
}
|
||||
|
||||
interface WalletSelectorProps {
|
||||
lang: Lang;
|
||||
onAddressDetected: (address: string) => void;
|
||||
connectedAddress?: string;
|
||||
compact?: boolean; // compact mode for BSC/ETH panel
|
||||
}
|
||||
|
||||
// ── Wallet Icons ──────────────────────────────────────────────────────────────
|
||||
|
||||
const MetaMaskIcon = () => (
|
||||
<svg width="24" height="24" viewBox="0 0 35 33" fill="none">
|
||||
<path d="M32.96 1L19.4 10.7l2.5-5.9L32.96 1z" fill="#E17726" stroke="#E17726" strokeWidth="0.25" strokeLinecap="round" strokeLinejoin="round"/>
|
||||
<path d="M2.04 1l13.46 9.8-2.38-5.99L2.04 1z" fill="#E27625" stroke="#E27625" strokeWidth="0.25" strokeLinecap="round" strokeLinejoin="round"/>
|
||||
<path d="M28.22 23.53l-3.61 5.53 7.73 2.13 2.22-7.54-6.34-.12z" fill="#E27625" stroke="#E27625" strokeWidth="0.25" strokeLinecap="round" strokeLinejoin="round"/>
|
||||
<path d="M.44 23.65l2.2 7.54 7.72-2.13-3.6-5.53-6.32.12z" fill="#E27625" stroke="#E27625" strokeWidth="0.25" strokeLinecap="round" strokeLinejoin="round"/>
|
||||
<path d="M9.97 14.46l-2.16 3.26 7.69.35-.26-8.27-5.27 4.66z" fill="#E27625" stroke="#E27625" strokeWidth="0.25" strokeLinecap="round" strokeLinejoin="round"/>
|
||||
<path d="M25.03 14.46l-5.35-4.75-.17 8.36 7.68-.35-2.16-3.26z" fill="#E27625" stroke="#E27625" strokeWidth="0.25" strokeLinecap="round" strokeLinejoin="round"/>
|
||||
<path d="M10.36 29.06l4.63-2.24-3.99-3.11-.64 5.35z" fill="#E27625" stroke="#E27625" strokeWidth="0.25" strokeLinecap="round" strokeLinejoin="round"/>
|
||||
<path d="M20.01 26.82l4.63 2.24-.64-5.35-3.99 3.11z" fill="#E27625" stroke="#E27625" strokeWidth="0.25" strokeLinecap="round" strokeLinejoin="round"/>
|
||||
</svg>
|
||||
);
|
||||
|
||||
const TrustWalletIcon = () => (
|
||||
<svg width="24" height="24" viewBox="0 0 24 24" fill="none">
|
||||
<circle cx="12" cy="12" r="12" fill="#3375BB"/>
|
||||
<path d="M12 4.5L6 7.5v5c0 3.31 2.57 6.41 6 7.5 3.43-1.09 6-4.19 6-7.5v-5L12 4.5z" fill="white" fillOpacity="0.9"/>
|
||||
<path d="M10.5 12.5l1.5 1.5 3-3" stroke="#3375BB" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round"/>
|
||||
</svg>
|
||||
);
|
||||
|
||||
const OKXIcon = () => (
|
||||
<svg width="24" height="24" viewBox="0 0 24 24" fill="none">
|
||||
<rect width="24" height="24" rx="6" fill="#000"/>
|
||||
<rect x="4" y="4" width="6" height="6" rx="1" fill="white"/>
|
||||
<rect x="14" y="4" width="6" height="6" rx="1" fill="white"/>
|
||||
<rect x="4" y="14" width="6" height="6" rx="1" fill="white"/>
|
||||
<rect x="14" y="14" width="6" height="6" rx="1" fill="white"/>
|
||||
<rect x="9" y="9" width="6" height="6" rx="1" fill="white"/>
|
||||
</svg>
|
||||
);
|
||||
|
||||
const CoinbaseIcon = () => (
|
||||
<svg width="24" height="24" viewBox="0 0 24 24" fill="none">
|
||||
<circle cx="12" cy="12" r="12" fill="#0052FF"/>
|
||||
<circle cx="12" cy="12" r="7" fill="white"/>
|
||||
<rect x="9" y="10.5" width="6" height="3" rx="1.5" fill="#0052FF"/>
|
||||
</svg>
|
||||
);
|
||||
|
||||
const TokenPocketIcon = () => (
|
||||
<svg width="24" height="24" viewBox="0 0 24 24" fill="none">
|
||||
<rect width="24" height="24" rx="6" fill="#2980FE"/>
|
||||
<path d="M7 8h5a3 3 0 0 1 0 6H7V8z" fill="white"/>
|
||||
<rect x="7" y="15" width="2.5" height="3" rx="1" fill="white"/>
|
||||
</svg>
|
||||
);
|
||||
|
||||
const BitgetIcon = () => (
|
||||
<svg width="24" height="24" viewBox="0 0 24 24" fill="none">
|
||||
<rect width="24" height="24" rx="6" fill="#00F0FF"/>
|
||||
<path d="M7 8h5a3 3 0 0 1 0 6H7V8z" fill="#000"/>
|
||||
<path d="M12 14h2a3 3 0 0 1 0 6h-2v-6z" fill="#000"/>
|
||||
</svg>
|
||||
);
|
||||
|
||||
// ── 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 {
|
||||
if (typeof window === "undefined") return null;
|
||||
return (window as unknown as { ethereum?: EthProvider }).ethereum ?? null;
|
||||
}
|
||||
|
||||
function getOKX(): EthProvider | null {
|
||||
if (typeof window === "undefined") return null;
|
||||
return (window as unknown as { okxwallet?: EthProvider }).okxwallet ?? null;
|
||||
}
|
||||
|
||||
function getBitget(): EthProvider | null {
|
||||
if (typeof window === "undefined") return null;
|
||||
const w = window as unknown as { bitkeep?: { ethereum?: EthProvider } };
|
||||
return w.bitkeep?.ethereum ?? null;
|
||||
}
|
||||
|
||||
// Find a specific provider from the providers array or direct injection
|
||||
function findProvider(predicate: (p: EthProvider) => boolean): EthProvider | null {
|
||||
const eth = getEth();
|
||||
if (!eth) return null;
|
||||
if (eth.providers && Array.isArray(eth.providers)) {
|
||||
return eth.providers.find(predicate) ?? null;
|
||||
}
|
||||
return predicate(eth) ? eth : null;
|
||||
}
|
||||
|
||||
async function requestAccounts(provider: EthProvider): Promise<string | null> {
|
||||
try {
|
||||
const accounts = await provider.request({ method: "eth_requestAccounts" }) as string[];
|
||||
return accounts?.[0] ?? null;
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
// ── Wallet definitions ────────────────────────────────────────────────────────
|
||||
|
||||
function buildWallets(): WalletInfo[] {
|
||||
return [
|
||||
{
|
||||
id: "metamask",
|
||||
name: "MetaMask",
|
||||
icon: <MetaMaskIcon />,
|
||||
installUrl: "https://metamask.io/download/",
|
||||
isInstalled: () => !!findProvider(p => !!p.isMetaMask),
|
||||
connect: async () => {
|
||||
const p = findProvider(p => !!p.isMetaMask) ?? getEth();
|
||||
return p ? requestAccounts(p) : null;
|
||||
},
|
||||
},
|
||||
{
|
||||
id: "trust",
|
||||
name: "Trust Wallet",
|
||||
icon: <TrustWalletIcon />,
|
||||
installUrl: "https://trustwallet.com/download",
|
||||
isInstalled: () => !!findProvider(p => !!(p.isTrust || p.isTrustWallet)),
|
||||
connect: async () => {
|
||||
const p = findProvider(p => !!(p.isTrust || p.isTrustWallet)) ?? getEth();
|
||||
return p ? requestAccounts(p) : null;
|
||||
},
|
||||
},
|
||||
{
|
||||
id: "okx",
|
||||
name: "OKX Wallet",
|
||||
icon: <OKXIcon />,
|
||||
installUrl: "https://www.okx.com/web3",
|
||||
isInstalled: () => !!(getOKX() || findProvider(p => !!(p.isOKExWallet || p.isOkxWallet))),
|
||||
connect: async () => {
|
||||
const p = getOKX() ?? findProvider(p => !!(p.isOKExWallet || p.isOkxWallet));
|
||||
return p ? requestAccounts(p) : null;
|
||||
},
|
||||
},
|
||||
{
|
||||
id: "coinbase",
|
||||
name: "Coinbase Wallet",
|
||||
icon: <CoinbaseIcon />,
|
||||
installUrl: "https://www.coinbase.com/wallet/downloads",
|
||||
isInstalled: () => !!findProvider(p => !!p.isCoinbaseWallet),
|
||||
connect: async () => {
|
||||
const p = findProvider(p => !!p.isCoinbaseWallet) ?? getEth();
|
||||
return p ? requestAccounts(p) : null;
|
||||
},
|
||||
},
|
||||
{
|
||||
id: "tokenpocket",
|
||||
name: "TokenPocket",
|
||||
icon: <TokenPocketIcon />,
|
||||
installUrl: "https://www.tokenpocket.pro/en/download/app",
|
||||
isInstalled: () => !!findProvider(p => !!p.isTokenPocket),
|
||||
connect: async () => {
|
||||
const p = findProvider(p => !!p.isTokenPocket) ?? getEth();
|
||||
return p ? requestAccounts(p) : null;
|
||||
},
|
||||
},
|
||||
{
|
||||
id: "bitget",
|
||||
name: "Bitget Wallet",
|
||||
icon: <BitgetIcon />,
|
||||
installUrl: "https://web3.bitget.com/en/wallet-download",
|
||||
isInstalled: () => !!(getBitget() || findProvider(p => !!(p.isBitkeep || p.isBitgetWallet))),
|
||||
connect: async () => {
|
||||
const p = getBitget() ?? findProvider(p => !!(p.isBitkeep || p.isBitgetWallet));
|
||||
return p ? requestAccounts(p) : null;
|
||||
},
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
// ── WalletSelector Component ──────────────────────────────────────────────────
|
||||
|
||||
export function WalletSelector({ lang, onAddressDetected, connectedAddress, compact = false }: WalletSelectorProps) {
|
||||
const [wallets, setWallets] = useState<WalletInfo[]>([]);
|
||||
const [connecting, setConnecting] = useState<string | null>(null);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
// Build wallet list after a short delay to allow extensions to inject
|
||||
const timer = setTimeout(() => {
|
||||
setWallets(buildWallets());
|
||||
}, 400);
|
||||
return () => clearTimeout(timer);
|
||||
}, []);
|
||||
|
||||
const handleConnect = async (wallet: WalletInfo) => {
|
||||
setConnecting(wallet.id);
|
||||
setError(null);
|
||||
try {
|
||||
const address = await wallet.connect();
|
||||
if (address) {
|
||||
onAddressDetected(address);
|
||||
} else {
|
||||
setError(lang === "zh" ? "未获取到地址,请重试" : "No address returned, please try again");
|
||||
}
|
||||
} catch {
|
||||
setError(lang === "zh" ? "连接失败,请重试" : "Connection failed, please try again");
|
||||
} finally {
|
||||
setConnecting(null);
|
||||
}
|
||||
};
|
||||
|
||||
const installedWallets = wallets.filter(w => w.isInstalled());
|
||||
const notInstalledWallets = wallets.filter(w => !w.isInstalled());
|
||||
|
||||
// If connected address is already set, show compact confirmation
|
||||
if (connectedAddress) {
|
||||
return (
|
||||
<div
|
||||
className="rounded-xl p-3 flex items-center gap-3"
|
||||
style={{ background: "rgba(0,230,118,0.08)", border: "1px solid rgba(0,230,118,0.25)" }}
|
||||
>
|
||||
<div className="w-2 h-2 rounded-full bg-green-400 flex-shrink-0" />
|
||||
<div className="flex-1 min-w-0">
|
||||
<p className="text-xs text-green-400 font-semibold">
|
||||
{lang === "zh" ? "钱包已连接" : "Wallet Connected"}
|
||||
</p>
|
||||
<p className="text-xs text-white/50 font-mono truncate">{connectedAddress}</p>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="space-y-3">
|
||||
<p className="text-xs font-semibold text-white/60 uppercase tracking-wider">
|
||||
{lang === "zh" ? "选择钱包自动填充地址" : "Select wallet to auto-fill address"}
|
||||
</p>
|
||||
|
||||
{/* Installed wallets */}
|
||||
{installedWallets.length > 0 && (
|
||||
<div className="space-y-2">
|
||||
{installedWallets.map(wallet => (
|
||||
<button
|
||||
key={wallet.id}
|
||||
onClick={() => handleConnect(wallet)}
|
||||
disabled={connecting === wallet.id}
|
||||
className="w-full flex items-center gap-3 px-4 py-3 rounded-xl transition-all hover:opacity-90 active:scale-[0.98]"
|
||||
style={{
|
||||
background: "rgba(0,212,255,0.08)",
|
||||
border: "1px solid rgba(0,212,255,0.3)",
|
||||
}}
|
||||
>
|
||||
<span className="flex-shrink-0">{wallet.icon}</span>
|
||||
<span className="flex-1 text-left text-sm font-semibold text-white">{wallet.name}</span>
|
||||
<span
|
||||
className="text-xs px-2 py-0.5 rounded-full font-medium"
|
||||
style={{ background: "rgba(0,212,255,0.15)", color: "#00d4ff" }}
|
||||
>
|
||||
{lang === "zh" ? "已安装" : "Installed"}
|
||||
</span>
|
||||
{connecting === wallet.id ? (
|
||||
<svg className="animate-spin w-4 h-4 text-white/60 flex-shrink-0" fill="none" viewBox="0 0 24 24">
|
||||
<circle className="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="4"/>
|
||||
<path className="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4z"/>
|
||||
</svg>
|
||||
) : (
|
||||
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="rgba(0,212,255,0.7)" strokeWidth="2" className="flex-shrink-0">
|
||||
<path d="M5 12h14M12 5l7 7-7 7"/>
|
||||
</svg>
|
||||
)}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* No wallets installed */}
|
||||
{installedWallets.length === 0 && (
|
||||
<div
|
||||
className="rounded-xl p-4 text-center"
|
||||
style={{ background: "rgba(255,255,255,0.04)", border: "1px dashed rgba(255,255,255,0.15)" }}
|
||||
>
|
||||
<p className="text-sm text-white/50 mb-1">
|
||||
{lang === "zh" ? "未检测到 EVM 钱包" : "No EVM wallet detected"}
|
||||
</p>
|
||||
<p className="text-xs text-white/30">
|
||||
{lang === "zh" ? "请安装以下任一钱包后刷新页面" : "Install any wallet below and refresh the page"}
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Not-installed wallets — show install links */}
|
||||
{!compact && notInstalledWallets.length > 0 && (
|
||||
<div className="space-y-1">
|
||||
<p className="text-xs text-white/30 mt-2">
|
||||
{lang === "zh" ? "未安装(点击安装)" : "Not installed (click to install)"}
|
||||
</p>
|
||||
<div className="grid grid-cols-3 gap-2">
|
||||
{notInstalledWallets.map(wallet => (
|
||||
<a
|
||||
key={wallet.id}
|
||||
href={wallet.installUrl}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="flex flex-col items-center gap-1.5 p-2.5 rounded-xl transition-all hover:opacity-80"
|
||||
style={{
|
||||
background: "rgba(255,255,255,0.04)",
|
||||
border: "1px solid rgba(255,255,255,0.08)",
|
||||
}}
|
||||
>
|
||||
<span className="opacity-40">{wallet.icon}</span>
|
||||
<span className="text-xs text-white/30 text-center leading-tight">{wallet.name}</span>
|
||||
</a>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* In compact mode, show install links inline */}
|
||||
{compact && notInstalledWallets.length > 0 && installedWallets.length === 0 && (
|
||||
<div className="flex flex-wrap gap-2">
|
||||
{notInstalledWallets.slice(0, 4).map(wallet => (
|
||||
<a
|
||||
key={wallet.id}
|
||||
href={wallet.installUrl}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="flex items-center gap-1.5 px-3 py-1.5 rounded-lg text-xs transition-all hover:opacity-80"
|
||||
style={{
|
||||
background: "rgba(255,255,255,0.05)",
|
||||
border: "1px solid rgba(255,255,255,0.1)",
|
||||
color: "rgba(255,255,255,0.4)",
|
||||
}}
|
||||
>
|
||||
<span className="opacity-50">{wallet.icon}</span>
|
||||
{lang === "zh" ? `安装 ${wallet.name}` : `Install ${wallet.name}`}
|
||||
</a>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{error && (
|
||||
<p className="text-xs text-red-400 text-center">{error}</p>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
@ -1,7 +1,8 @@
|
|||
// NAC XIC Presale — Wallet Connection Hook
|
||||
// Supports MetaMask / any EVM-compatible wallet (BSC + ETH)
|
||||
// Supports MetaMask, Trust Wallet, OKX Wallet, Coinbase Wallet, and all EVM-compatible wallets
|
||||
// Robust auto-detect with retry, multi-provider support, and graceful fallback
|
||||
|
||||
import { useState, useEffect, useCallback } from "react";
|
||||
import { useState, useEffect, useCallback, useRef } from "react";
|
||||
import { BrowserProvider, JsonRpcSigner, Eip1193Provider } from "ethers";
|
||||
import { shortenAddress, switchToNetwork } from "@/lib/contracts";
|
||||
|
||||
|
|
@ -29,118 +30,246 @@ const INITIAL_STATE: WalletState = {
|
|||
error: null,
|
||||
};
|
||||
|
||||
// Detect the best available EVM provider across all major wallets
|
||||
function detectProvider(): Eip1193Provider | null {
|
||||
if (typeof window === "undefined") return null;
|
||||
|
||||
// Check for multiple injected providers (e.g., MetaMask + Coinbase both installed)
|
||||
const w = window as unknown as Record<string, unknown>;
|
||||
const eth = w.ethereum as (Eip1193Provider & { providers?: Eip1193Provider[]; isMetaMask?: boolean; isTrust?: boolean; isOKExWallet?: boolean; isCoinbaseWallet?: boolean }) | undefined;
|
||||
|
||||
if (!eth) {
|
||||
// Fallback: check wallet-specific globals
|
||||
if (w.okxwallet) return w.okxwallet as Eip1193Provider;
|
||||
if (w.coinbaseWalletExtension) return w.coinbaseWalletExtension as Eip1193Provider;
|
||||
return null;
|
||||
}
|
||||
|
||||
// If multiple providers are injected (common when multiple extensions installed)
|
||||
if (eth.providers && Array.isArray(eth.providers) && eth.providers.length > 0) {
|
||||
// Prefer MetaMask if available, otherwise use first provider
|
||||
const metamask = eth.providers.find((p: Eip1193Provider & { isMetaMask?: boolean }) => p.isMetaMask);
|
||||
return metamask ?? eth.providers[0];
|
||||
}
|
||||
|
||||
return eth;
|
||||
}
|
||||
|
||||
// Build wallet state from a provider and accounts
|
||||
async function buildWalletState(
|
||||
rawProvider: Eip1193Provider,
|
||||
address: string
|
||||
): Promise<Partial<WalletState>> {
|
||||
const provider = new BrowserProvider(rawProvider);
|
||||
let chainId: number | null = null;
|
||||
let signer: JsonRpcSigner | null = null;
|
||||
|
||||
try {
|
||||
const network = await provider.getNetwork();
|
||||
chainId = Number(network.chainId);
|
||||
} catch {
|
||||
// Some wallets don't support getNetwork immediately — try eth_chainId directly
|
||||
try {
|
||||
const chainHex = await (rawProvider as { request: (args: { method: string }) => Promise<string> }).request({ method: "eth_chainId" });
|
||||
chainId = parseInt(chainHex, 16);
|
||||
} catch {
|
||||
chainId = null;
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
signer = await provider.getSigner();
|
||||
} catch {
|
||||
// getSigner may fail on some wallets before full connection — that's OK
|
||||
signer = null;
|
||||
}
|
||||
|
||||
return {
|
||||
address,
|
||||
shortAddress: shortenAddress(address),
|
||||
isConnected: true,
|
||||
chainId,
|
||||
provider,
|
||||
signer,
|
||||
isConnecting: false,
|
||||
error: null,
|
||||
};
|
||||
}
|
||||
|
||||
export function useWallet() {
|
||||
const [state, setState] = useState<WalletState>(INITIAL_STATE);
|
||||
const retryRef = useRef<ReturnType<typeof setTimeout> | null>(null);
|
||||
const mountedRef = useRef(true);
|
||||
|
||||
useEffect(() => {
|
||||
mountedRef.current = true;
|
||||
return () => {
|
||||
mountedRef.current = false;
|
||||
if (retryRef.current) clearTimeout(retryRef.current);
|
||||
};
|
||||
}, []);
|
||||
|
||||
// ── Connect (explicit user action) ─────────────────────────────────────────
|
||||
const connect = useCallback(async () => {
|
||||
if (!window.ethereum) {
|
||||
setState(s => ({ ...s, error: "Please install MetaMask or a compatible wallet." }));
|
||||
const rawProvider = detectProvider();
|
||||
if (!rawProvider) {
|
||||
setState(s => ({ ...s, error: "请安装 MetaMask 或其他 EVM 兼容钱包 / Please install MetaMask or a compatible wallet." }));
|
||||
return;
|
||||
}
|
||||
setState(s => ({ ...s, isConnecting: true, error: null }));
|
||||
try {
|
||||
const provider = new BrowserProvider(window.ethereum as Eip1193Provider);
|
||||
const accounts = await provider.send("eth_requestAccounts", []);
|
||||
const network = await provider.getNetwork();
|
||||
const signer = await provider.getSigner();
|
||||
const address = accounts[0] as string;
|
||||
setState({
|
||||
address,
|
||||
shortAddress: shortenAddress(address),
|
||||
isConnected: true,
|
||||
chainId: Number(network.chainId),
|
||||
provider,
|
||||
signer,
|
||||
isConnecting: false,
|
||||
error: null,
|
||||
// Request accounts — this triggers the wallet popup
|
||||
const accounts = await (rawProvider as { request: (args: { method: string; params?: unknown[] }) => Promise<string[]> }).request({
|
||||
method: "eth_requestAccounts",
|
||||
params: [],
|
||||
});
|
||||
if (!accounts || accounts.length === 0) throw new Error("No accounts returned");
|
||||
const partial = await buildWalletState(rawProvider, accounts[0]);
|
||||
if (mountedRef.current) setState({ ...INITIAL_STATE, ...partial });
|
||||
} catch (err: unknown) {
|
||||
setState(s => ({
|
||||
...s,
|
||||
isConnecting: false,
|
||||
error: (err as Error).message || "Failed to connect wallet",
|
||||
}));
|
||||
const msg = (err as Error).message || "Failed to connect wallet";
|
||||
if (mountedRef.current) setState(s => ({ ...s, isConnecting: false, error: msg }));
|
||||
}
|
||||
}, []);
|
||||
|
||||
// ── Disconnect ──────────────────────────────────────────────────────────────
|
||||
const disconnect = useCallback(() => {
|
||||
setState(INITIAL_STATE);
|
||||
}, []);
|
||||
|
||||
// ── Switch Network ──────────────────────────────────────────────────────────
|
||||
const switchNetwork = useCallback(async (chainId: number) => {
|
||||
try {
|
||||
await switchToNetwork(chainId);
|
||||
if (window.ethereum) {
|
||||
const provider = new BrowserProvider(window.ethereum as Eip1193Provider);
|
||||
const rawProvider = detectProvider();
|
||||
if (rawProvider) {
|
||||
const provider = new BrowserProvider(rawProvider);
|
||||
const network = await provider.getNetwork();
|
||||
const signer = await provider.getSigner();
|
||||
setState(s => ({
|
||||
...s,
|
||||
chainId: Number(network.chainId),
|
||||
provider,
|
||||
signer,
|
||||
error: null,
|
||||
}));
|
||||
}
|
||||
} catch (err: unknown) {
|
||||
setState(s => ({ ...s, error: (err as Error).message }));
|
||||
}
|
||||
}, []);
|
||||
|
||||
// Auto-detect already-connected wallet on page load (silent, no popup)
|
||||
useEffect(() => {
|
||||
const autoDetect = async () => {
|
||||
if (!window.ethereum) return;
|
||||
try {
|
||||
const provider = new BrowserProvider(window.ethereum as Eip1193Provider);
|
||||
// Use eth_accounts (not eth_requestAccounts) — silent, no popup
|
||||
const accounts = await provider.send("eth_accounts", []);
|
||||
if (accounts && accounts.length > 0) {
|
||||
const network = await provider.getNetwork();
|
||||
const signer = await provider.getSigner();
|
||||
const address = accounts[0] as string;
|
||||
setState({
|
||||
address,
|
||||
shortAddress: shortenAddress(address),
|
||||
isConnected: true,
|
||||
let signer: JsonRpcSigner | null = null;
|
||||
try { signer = await provider.getSigner(); } catch { /* ignore */ }
|
||||
if (mountedRef.current) {
|
||||
setState(s => ({
|
||||
...s,
|
||||
chainId: Number(network.chainId),
|
||||
provider,
|
||||
signer,
|
||||
isConnecting: false,
|
||||
error: null,
|
||||
});
|
||||
}));
|
||||
}
|
||||
}
|
||||
} catch (err: unknown) {
|
||||
if (mountedRef.current) setState(s => ({ ...s, error: (err as Error).message }));
|
||||
}
|
||||
}, []);
|
||||
|
||||
// ── Auto-detect on page load (silent, no popup) ─────────────────────────────
|
||||
useEffect(() => {
|
||||
let cancelled = false;
|
||||
|
||||
const tryAutoDetect = async (attempt: number) => {
|
||||
if (cancelled) return;
|
||||
|
||||
const rawProvider = detectProvider();
|
||||
if (!rawProvider) {
|
||||
// Wallet extension may not be injected yet — retry up to 3 times
|
||||
if (attempt < 3) {
|
||||
retryRef.current = setTimeout(() => tryAutoDetect(attempt + 1), 800 * attempt);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const accounts = await (rawProvider as { request: (args: { method: string }) => Promise<string[]> }).request({
|
||||
method: "eth_accounts", // Silent — no popup
|
||||
});
|
||||
if (cancelled) return;
|
||||
if (accounts && accounts.length > 0) {
|
||||
const partial = await buildWalletState(rawProvider, accounts[0]);
|
||||
if (!cancelled && mountedRef.current) {
|
||||
setState({ ...INITIAL_STATE, ...partial });
|
||||
}
|
||||
} else if (attempt < 3) {
|
||||
// Accounts empty — wallet might not have finished loading, retry
|
||||
retryRef.current = setTimeout(() => tryAutoDetect(attempt + 1), 1000 * attempt);
|
||||
}
|
||||
} catch {
|
||||
// Silently ignore — user hasn't connected yet
|
||||
}
|
||||
};
|
||||
autoDetect();
|
||||
|
||||
// Small initial delay to let wallet extensions inject themselves
|
||||
retryRef.current = setTimeout(() => tryAutoDetect(1), 300);
|
||||
|
||||
return () => {
|
||||
cancelled = true;
|
||||
if (retryRef.current) clearTimeout(retryRef.current);
|
||||
};
|
||||
}, []);
|
||||
|
||||
// Listen for account/chain changes
|
||||
// ── Listen for account / chain changes ─────────────────────────────────────
|
||||
useEffect(() => {
|
||||
if (!window.ethereum) return;
|
||||
const handleAccountsChanged = (accounts: unknown) => {
|
||||
const rawProvider = detectProvider();
|
||||
if (!rawProvider) return;
|
||||
|
||||
const eth = rawProvider as {
|
||||
on?: (event: string, handler: (data: unknown) => void) => void;
|
||||
removeListener?: (event: string, handler: (data: unknown) => void) => void;
|
||||
};
|
||||
if (!eth.on) return;
|
||||
|
||||
const handleAccountsChanged = async (accounts: unknown) => {
|
||||
const accs = accounts as string[];
|
||||
if (accs.length === 0) {
|
||||
if (!mountedRef.current) return;
|
||||
if (!accs || accs.length === 0) {
|
||||
setState(INITIAL_STATE);
|
||||
} else {
|
||||
setState(s => ({
|
||||
...s,
|
||||
address: accs[0],
|
||||
shortAddress: shortenAddress(accs[0]),
|
||||
isConnected: true,
|
||||
}));
|
||||
// Re-build full state with new address
|
||||
try {
|
||||
const partial = await buildWalletState(rawProvider, accs[0]);
|
||||
if (mountedRef.current) setState({ ...INITIAL_STATE, ...partial });
|
||||
} catch {
|
||||
if (mountedRef.current) {
|
||||
setState(s => ({
|
||||
...s,
|
||||
address: accs[0],
|
||||
shortAddress: shortenAddress(accs[0]),
|
||||
isConnected: true,
|
||||
}));
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
const handleChainChanged = () => {
|
||||
window.location.reload();
|
||||
|
||||
const handleChainChanged = async () => {
|
||||
if (!mountedRef.current) return;
|
||||
// Re-fetch network info instead of reloading the page
|
||||
try {
|
||||
const provider = new BrowserProvider(rawProvider);
|
||||
const network = await provider.getNetwork();
|
||||
let signer: JsonRpcSigner | null = null;
|
||||
try { signer = await provider.getSigner(); } catch { /* ignore */ }
|
||||
if (mountedRef.current) {
|
||||
setState(s => ({
|
||||
...s,
|
||||
chainId: Number(network.chainId),
|
||||
provider,
|
||||
signer,
|
||||
}));
|
||||
}
|
||||
} catch {
|
||||
// If we can't get network info, reload as last resort
|
||||
window.location.reload();
|
||||
}
|
||||
};
|
||||
window.ethereum.on("accountsChanged", handleAccountsChanged);
|
||||
window.ethereum.on("chainChanged", handleChainChanged);
|
||||
|
||||
eth.on("accountsChanged", handleAccountsChanged);
|
||||
eth.on("chainChanged", handleChainChanged);
|
||||
|
||||
return () => {
|
||||
window.ethereum?.removeListener("accountsChanged", handleAccountsChanged);
|
||||
window.ethereum?.removeListener("chainChanged", handleChainChanged);
|
||||
if (eth.removeListener) {
|
||||
eth.removeListener("accountsChanged", handleAccountsChanged);
|
||||
eth.removeListener("chainChanged", handleChainChanged);
|
||||
}
|
||||
};
|
||||
}, []);
|
||||
|
||||
|
|
|
|||
|
|
@ -130,19 +130,31 @@ function SettingsPanel({ token }: { token: string }) {
|
|||
const [telegramStatus, setTelegramStatus] = useState<"idle" | "testing" | "success" | "error">("idle");
|
||||
const [telegramError, setTelegramError] = useState("");
|
||||
|
||||
// ── Presale Active/Paused Toggle ──────────────────────────────────────────
|
||||
const isPresaleLive = (configData?.find(c => c.key === "presaleStatus")?.value ?? "live") === "live";
|
||||
const [togglingPresale, setTogglingPresale] = useState(false);
|
||||
|
||||
const setConfigMutation = trpc.admin.setConfig.useMutation({
|
||||
onSuccess: (_, vars) => {
|
||||
setSavedKeys(prev => { const s = new Set(Array.from(prev)); s.add(vars.key); return s; });
|
||||
setSavingKey(null);
|
||||
setTogglingPresale(false);
|
||||
refetchConfig();
|
||||
setTimeout(() => setSavedKeys(prev => { const n = new Set(Array.from(prev)); n.delete(vars.key); return n; }), 2000);
|
||||
},
|
||||
onError: (err) => {
|
||||
setSavingKey(null);
|
||||
setTogglingPresale(false);
|
||||
alert(`Save failed: ${err.message}`);
|
||||
},
|
||||
});
|
||||
|
||||
const handleTogglePresale = () => {
|
||||
const newStatus = isPresaleLive ? "paused" : "live";
|
||||
setTogglingPresale(true);
|
||||
setConfigMutation.mutate({ token, key: "presaleStatus", value: newStatus });
|
||||
};
|
||||
|
||||
const testTelegramMutation = trpc.admin.testTelegram.useMutation({
|
||||
onSuccess: () => {
|
||||
setTelegramStatus("success");
|
||||
|
|
@ -269,6 +281,66 @@ function SettingsPanel({ token }: { token: string }) {
|
|||
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
{/* ── Presale Active Toggle ── */}
|
||||
<div
|
||||
className="rounded-2xl p-6"
|
||||
style={{
|
||||
background: isPresaleLive ? "rgba(0,230,118,0.06)" : "rgba(255,60,60,0.06)",
|
||||
border: isPresaleLive ? "2px solid rgba(0,230,118,0.45)" : "2px solid rgba(255,60,60,0.45)",
|
||||
}}
|
||||
>
|
||||
<div className="flex items-center justify-between gap-4 flex-wrap">
|
||||
<div>
|
||||
<div className="flex items-center gap-3 mb-1">
|
||||
<div
|
||||
className="w-3 h-3 rounded-full"
|
||||
style={{
|
||||
background: isPresaleLive ? "#00e676" : "#ff4444",
|
||||
boxShadow: isPresaleLive ? "0 0 8px rgba(0,230,118,0.8)" : "0 0 8px rgba(255,68,68,0.8)",
|
||||
animation: isPresaleLive ? "pulse 2s infinite" : "none",
|
||||
}}
|
||||
/>
|
||||
<h3
|
||||
className="text-lg font-bold"
|
||||
style={{
|
||||
color: isPresaleLive ? "#00e676" : "#ff6060",
|
||||
fontFamily: "'Space Grotesk', sans-serif",
|
||||
}}
|
||||
>
|
||||
{isPresaleLive ? "预售进行中 PRESALE LIVE" : "预售已暂停 PRESALE PAUSED"}
|
||||
</h3>
|
||||
</div>
|
||||
<p className="text-xs ml-6" style={{ color: "rgba(255,255,255,0.45)" }}>
|
||||
{isPresaleLive
|
||||
? "用户当前可正常购买 XIC 代币。点击《暂停预售》可立即封禁所有购买入口。"
|
||||
: "预售已暂停,首页购买按钮已禁用。点击《开启预售》可重新开放购买。"}
|
||||
</p>
|
||||
</div>
|
||||
<button
|
||||
onClick={handleTogglePresale}
|
||||
disabled={togglingPresale}
|
||||
className="flex-shrink-0 px-8 py-3 rounded-xl font-bold text-base transition-all"
|
||||
style={{
|
||||
background: isPresaleLive
|
||||
? "linear-gradient(135deg, rgba(255,60,60,0.25) 0%, rgba(255,60,60,0.15) 100%)"
|
||||
: "linear-gradient(135deg, rgba(0,230,118,0.25) 0%, rgba(0,230,118,0.15) 100%)",
|
||||
border: isPresaleLive ? "1.5px solid rgba(255,60,60,0.6)" : "1.5px solid rgba(0,230,118,0.6)",
|
||||
color: isPresaleLive ? "#ff6060" : "#00e676",
|
||||
fontFamily: "'Space Grotesk', sans-serif",
|
||||
opacity: togglingPresale ? 0.6 : 1,
|
||||
cursor: togglingPresale ? "not-allowed" : "pointer",
|
||||
letterSpacing: "0.03em",
|
||||
}}
|
||||
>
|
||||
{togglingPresale
|
||||
? "处理中..."
|
||||
: isPresaleLive
|
||||
? "⏸ 暂停预售"
|
||||
: "▶ 开启预售"}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Presale Parameters */}
|
||||
<div className="rounded-2xl p-5" style={{ background: "rgba(255,255,255,0.02)", border: "1px solid rgba(240,180,41,0.15)" }}>
|
||||
<h3 className="text-sm font-semibold mb-4" style={{ color: "#f0b429" }}>Presale Parameters 预售参数</h3>
|
||||
|
|
|
|||
|
|
@ -11,6 +11,7 @@ import { usePresale } from "@/hooks/usePresale";
|
|||
import { CONTRACTS, PRESALE_CONFIG, formatNumber, shortenAddress } from "@/lib/contracts";
|
||||
import { trpc } from "@/lib/trpc";
|
||||
import { type Lang, useTranslation } from "@/lib/i18n";
|
||||
import { WalletSelector } from "@/components/WalletSelector";
|
||||
|
||||
// ─── Network Tab Types ────────────────────────────────────────────────────────
|
||||
type NetworkTab = "BSC" | "ETH" | "TRON";
|
||||
|
|
@ -111,9 +112,6 @@ function TRC20Panel({ usdtAmount, lang, connectedAddress, onConnectWallet }: { u
|
|||
const [evmAddress, setEvmAddress] = useState(connectedAddress || "");
|
||||
const [evmAddrError, setEvmAddrError] = useState("");
|
||||
const [submitted, setSubmitted] = useState(false);
|
||||
const [isAutoConnecting, setIsAutoConnecting] = useState(false);
|
||||
const hasEthereum = typeof window !== "undefined" && !!window.ethereum;
|
||||
|
||||
// TronLink detection state
|
||||
const [tronAddress, setTronAddress] = useState<string | null>(null);
|
||||
const [isTronConnecting, setIsTronConnecting] = useState(false);
|
||||
|
|
@ -164,30 +162,10 @@ function TRC20Panel({ usdtAmount, lang, connectedAddress, onConnectWallet }: { u
|
|||
}
|
||||
}, [connectedAddress, submitted]);
|
||||
|
||||
// Auto-connect EVM wallet from within TRC20 panel
|
||||
const handleAutoConnectEVM = async () => {
|
||||
if (!window.ethereum) return;
|
||||
setIsAutoConnecting(true);
|
||||
try {
|
||||
const accounts = await window.ethereum.request({ method: "eth_requestAccounts" }) as string[];
|
||||
if (accounts && accounts.length > 0 && !submitted) {
|
||||
setEvmAddress(accounts[0]);
|
||||
setEvmAddrError("");
|
||||
toast.success(lang === "zh" ? "EVM地址已自动填充!" : "EVM address auto-filled!");
|
||||
}
|
||||
// Also notify parent to update wallet state
|
||||
if (onConnectWallet) onConnectWallet();
|
||||
} catch {
|
||||
// User rejected or error — silently ignore
|
||||
} finally {
|
||||
setIsAutoConnecting(false);
|
||||
}
|
||||
};
|
||||
|
||||
const submitTrc20Mutation = trpc.presale.registerTrc20Intent.useMutation({
|
||||
onSuccess: () => {
|
||||
setSubmitted(true);
|
||||
toast.success(lang === "zh" ? "EVM地址已保存!" : "EVM address saved!");
|
||||
toast.success(lang === "zh" ? "XIC接收地址已保存!" : "XIC receiving address saved!");
|
||||
},
|
||||
onError: (err: { message: string }) => {
|
||||
toast.error(err.message);
|
||||
|
|
@ -202,8 +180,8 @@ function TRC20Panel({ usdtAmount, lang, connectedAddress, onConnectWallet }: { u
|
|||
};
|
||||
|
||||
const validateEvmAddress = (addr: string) => {
|
||||
if (!addr) return lang === "zh" ? "请输入您的EVM地址" : "Please enter your EVM address";
|
||||
if (!/^0x[0-9a-fA-F]{40}$/.test(addr)) return lang === "zh" ? "无效的EVM地址格式(应以0x开头,共42位)" : "Invalid EVM address format (must start with 0x, 42 chars)";
|
||||
if (!addr) return lang === "zh" ? "请输入您的XIC接收地址" : "Please enter your XIC receiving address";
|
||||
if (!/^0x[0-9a-fA-F]{40}$/.test(addr)) return lang === "zh" ? "无效的XIC接收地址格式(应以0x开头,入42位)" : "Invalid XIC receiving address format (must start with 0x, 42 chars)";
|
||||
return "";
|
||||
};
|
||||
|
||||
|
|
@ -221,42 +199,33 @@ function TRC20Panel({ usdtAmount, lang, connectedAddress, onConnectWallet }: { u
|
|||
<div className="flex items-center gap-2">
|
||||
<span className="text-amber-400 text-sm">⚠️</span>
|
||||
<p className="text-sm font-semibold text-amber-300">
|
||||
{lang === "zh" ? "必填:您的EVM钱包地址(用于接收XIC代币)" : "Required: Your EVM Wallet Address (to receive XIC tokens)"}
|
||||
{lang === "zh" ? "必填:您的XIC接收地址(BSC/ETH钉包地址)" : "Required: Your XIC Receiving Address (BSC/ETH wallet address)"}
|
||||
</p>
|
||||
</div>
|
||||
<p className="text-xs text-white/50">
|
||||
{lang === "zh"
|
||||
? "XIC代币在BSC网络上发放。请提供您的BSC/ETH地址(0x开头),以便我们将代币发送给您。"
|
||||
: "XIC tokens are distributed on BSC. Please provide your BSC/ETH address (starts with 0x) so we can send your tokens."}
|
||||
? "XIC代币将发放到您的BSC/ETH钉包地址(0x开头)。请确保填写正确的地址,否则无法收到代币。"
|
||||
: "XIC tokens will be sent to your BSC/ETH wallet address (starts with 0x). Please make sure to enter the correct address."}
|
||||
</p>
|
||||
<div className="space-y-2">
|
||||
{/* Auto-connect button — shown when MetaMask is available but address not yet filled */}
|
||||
{hasEthereum && !connectedAddress && !evmAddress && (
|
||||
<button
|
||||
onClick={handleAutoConnectEVM}
|
||||
disabled={isAutoConnecting}
|
||||
className="w-full py-2.5 rounded-xl text-sm font-semibold flex items-center justify-center gap-2 transition-all hover:opacity-90"
|
||||
style={{
|
||||
background: "rgba(0,212,255,0.12)",
|
||||
border: "1px solid rgba(0,212,255,0.35)",
|
||||
color: "#00d4ff",
|
||||
{/* WalletSelector — shown when address not yet filled */}
|
||||
{!evmAddress && !submitted && (
|
||||
<WalletSelector
|
||||
lang={lang}
|
||||
connectedAddress={connectedAddress}
|
||||
onAddressDetected={(addr) => {
|
||||
setEvmAddress(addr);
|
||||
setEvmAddrError("");
|
||||
toast.success(lang === "zh" ? "XIC接收地址已自动填充!" : "XIC receiving address auto-filled!");
|
||||
if (onConnectWallet) onConnectWallet();
|
||||
}}
|
||||
>
|
||||
<svg width="16" height="16" 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>
|
||||
{isAutoConnecting
|
||||
? (lang === "zh" ? "连接中..." : "Connecting...")
|
||||
: (lang === "zh" ? "连接MetaMask自动填充地址" : "Connect MetaMask to auto-fill")}
|
||||
</button>
|
||||
/>
|
||||
)}
|
||||
<input
|
||||
type="text"
|
||||
value={evmAddress}
|
||||
onChange={e => { setEvmAddress(e.target.value); setEvmAddrError(""); setSubmitted(false); }}
|
||||
placeholder={lang === "zh" ? "0x... (您的BSC/ETH地址)" : "0x... (your BSC/ETH address)"}
|
||||
placeholder={lang === "zh" ? "0x... (您的XIC接收地址)" : "0x... (your XIC receiving address)"}
|
||||
className="w-full px-4 py-3 rounded-xl text-sm font-mono"
|
||||
style={{
|
||||
background: "rgba(255,255,255,0.05)",
|
||||
|
|
@ -266,7 +235,7 @@ function TRC20Panel({ usdtAmount, lang, connectedAddress, onConnectWallet }: { u
|
|||
}}
|
||||
/>
|
||||
{evmAddrError && <p className="text-xs text-red-400">{evmAddrError}</p>}
|
||||
{submitted && <p className="text-xs text-green-400">✓ {lang === "zh" ? "EVM地址已保存" : "EVM address saved"}</p>}
|
||||
{submitted && <p className="text-xs text-green-400">✓ {lang === "zh" ? "XIC接收地址已保存" : "XIC receiving address saved"}</p>}
|
||||
<button
|
||||
onClick={handleEvmSubmit}
|
||||
disabled={submitTrc20Mutation.isPending || submitted || !evmAddress}
|
||||
|
|
@ -278,7 +247,7 @@ function TRC20Panel({ usdtAmount, lang, connectedAddress, onConnectWallet }: { u
|
|||
opacity: !evmAddress ? 0.5 : 1,
|
||||
}}
|
||||
>
|
||||
{submitTrc20Mutation.isPending ? (lang === "zh" ? "保存中..." : "Saving...") : submitted ? (lang === "zh" ? "✓ 已保存" : "✓ Saved") : (lang === "zh" ? "保存EVM地址" : "Save EVM Address")}
|
||||
{submitTrc20Mutation.isPending ? (lang === "zh" ? "保存中..." : "Saving...") : submitted ? (lang === "zh" ? "✓ 已保存" : "✓ Saved") : (lang === "zh" ? "保存XIC接收地址" : "Save XIC Receiving Address")}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -308,8 +277,8 @@ function TRC20Panel({ usdtAmount, lang, connectedAddress, onConnectWallet }: { u
|
|||
</div>
|
||||
<p className="text-xs text-white/40">
|
||||
{lang === "zh"
|
||||
? "您的 TronLink 已连接。请在上方填写 EVM 地址以接收 XIC 代币,然后向下方地址发送 USDT。"
|
||||
: "TronLink connected. Please fill your EVM address above to receive XIC tokens, then send USDT to the address below."}
|
||||
? "您的 TronLink 已连接。请在上方填写 XIC 接收地址,然后向下方地址发送 USDT。"
|
||||
: "TronLink connected. Please fill your XIC receiving address above, then send USDT to the address below."}
|
||||
</p>
|
||||
</div>
|
||||
) : (
|
||||
|
|
@ -378,8 +347,8 @@ function TRC20Panel({ usdtAmount, lang, connectedAddress, onConnectWallet }: { u
|
|||
<div className="space-y-2">
|
||||
<StepBadge num={1} text={
|
||||
lang === "zh"
|
||||
? `在上方填写您的EVM地址并保存`
|
||||
: `Enter and save your EVM address above`
|
||||
? `在上方填写您的XIC接收地址并保存`
|
||||
: `Enter and save your XIC receiving address above`
|
||||
} />
|
||||
<StepBadge num={2} text={
|
||||
lang === "zh"
|
||||
|
|
@ -447,18 +416,17 @@ function EVMPurchasePanel({ network, lang, wallet }: { network: "BSC" | "ETH"; l
|
|||
if (!wallet.isConnected) {
|
||||
return (
|
||||
<div className="space-y-4">
|
||||
<div className="text-center py-6">
|
||||
<div className="text-4xl mb-3">🔗</div>
|
||||
<p className="text-white/60 text-sm mb-4">{t("buy_connect_msg")}</p>
|
||||
<button
|
||||
onClick={wallet.connect}
|
||||
disabled={wallet.isConnecting}
|
||||
className="btn-primary-nac w-full py-3 rounded-xl text-base font-bold pulse-amber"
|
||||
style={{ fontFamily: "'Space Grotesk', sans-serif" }}
|
||||
>
|
||||
{wallet.isConnecting ? t("nav_connecting") : t("buy_connect_btn")}
|
||||
</button>
|
||||
</div>
|
||||
<p className="text-sm text-white/60 text-center">{t("buy_connect_msg")}</p>
|
||||
<WalletSelector
|
||||
lang={lang}
|
||||
connectedAddress={wallet.address ?? undefined}
|
||||
onAddressDetected={(addr) => {
|
||||
// Address detected — wallet is now connected, trigger wallet.connect to sync state
|
||||
wallet.connect();
|
||||
toast.success(lang === "zh" ? `已连接: ${addr.slice(0, 6)}...${addr.slice(-4)}` : `Connected: ${addr.slice(0, 6)}...${addr.slice(-4)}`);
|
||||
}}
|
||||
compact
|
||||
/>
|
||||
<div className="text-xs text-white/40 text-center">{t("buy_connect_hint")}</div>
|
||||
</div>
|
||||
);
|
||||
|
|
@ -963,6 +931,8 @@ export default function Home() {
|
|||
|
||||
const stats = onChainStats || FALLBACK_STATS;
|
||||
const progressPct = stats.progressPct || 0;
|
||||
// Presale active/paused status from backend config
|
||||
const isPresalePaused = (onChainStats as any)?.presaleStatus === "paused";
|
||||
|
||||
// 钱包状态提升到顶层,共享给NavWalletButton和EVMPurchasePanel
|
||||
const wallet = useWallet();
|
||||
|
|
@ -971,6 +941,21 @@ export default function Home() {
|
|||
|
||||
return (
|
||||
<div className="min-h-screen" style={{ background: "#0a0a0f" }}>
|
||||
{/* ── Presale Paused Banner ── */}
|
||||
{isPresalePaused && (
|
||||
<div
|
||||
className="fixed top-0 left-0 right-0 z-[60] flex items-center justify-center gap-3 py-3 px-4 text-sm font-bold"
|
||||
style={{
|
||||
background: "linear-gradient(90deg, rgba(255,60,60,0.95) 0%, rgba(200,0,0,0.95) 100%)",
|
||||
color: "white",
|
||||
letterSpacing: "0.05em",
|
||||
}}
|
||||
>
|
||||
<span className="text-lg">⏸</span>
|
||||
<span>{lang === "zh" ? "预售活动已暂停,暂时无法购买。请关注官方渠道获取最新公告。" : "Presale is currently paused. Please follow our official channels for updates."}</span>
|
||||
<span className="text-lg">⏸</span>
|
||||
</div>
|
||||
)}
|
||||
{/* ── Navigation ── */}
|
||||
<nav className="fixed top-0 left-0 right-0 z-50 flex items-center justify-between px-6 py-4" style={{ background: "rgba(10,10,15,0.9)", borderBottom: "1px solid rgba(240,180,41,0.1)", backdropFilter: "blur(12px)" }}>
|
||||
<div className="flex items-center gap-3">
|
||||
|
|
@ -1155,7 +1140,26 @@ export default function Home() {
|
|||
</div>
|
||||
|
||||
{/* Purchase Area */}
|
||||
<div>
|
||||
<div className="relative">
|
||||
{/* Presale Paused Overlay */}
|
||||
{isPresalePaused && (
|
||||
<div
|
||||
className="absolute inset-0 z-10 flex flex-col items-center justify-center rounded-2xl gap-3"
|
||||
style={{
|
||||
background: "rgba(10,10,15,0.88)",
|
||||
border: "1.5px solid rgba(255,60,60,0.4)",
|
||||
backdropFilter: "blur(4px)",
|
||||
}}
|
||||
>
|
||||
<div className="text-4xl">⏸</div>
|
||||
<p className="text-base font-bold" style={{ color: "#ff6060", fontFamily: "'Space Grotesk', sans-serif" }}>
|
||||
{lang === "zh" ? "预售已暂停" : "Presale Paused"}
|
||||
</p>
|
||||
<p className="text-xs text-white/50 text-center px-4">
|
||||
{lang === "zh" ? "请关注官方 Telegram / Twitter 获取最新公告" : "Follow our official Telegram / Twitter for updates"}
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
{activeNetwork === "BSC" && <EVMPurchasePanel network="BSC" lang={lang} wallet={wallet} />}
|
||||
{activeNetwork === "ETH" && <EVMPurchasePanel network="ETH" lang={lang} wallet={wallet} />}
|
||||
{activeNetwork === "TRON" && (
|
||||
|
|
|
|||
|
|
@ -30,7 +30,10 @@ export const appRouter = router({
|
|||
presale: router({
|
||||
// Combined stats from BSC + ETH + TRC20
|
||||
stats: publicProcedure.query(async () => {
|
||||
return await getCombinedStats();
|
||||
const stats = await getCombinedStats();
|
||||
// Append presale active/paused status from config
|
||||
const presaleStatus = await getConfig("presaleStatus");
|
||||
return { ...stats, presaleStatus: presaleStatus ?? "live" };
|
||||
}),
|
||||
|
||||
// Single chain stats
|
||||
|
|
|
|||
12
todo.md
12
todo.md
|
|
@ -59,3 +59,15 @@
|
|||
- [x] 更新 XIC 代币合约地址为 0x59ff34dd59680a7125782b1f6df2a86ed46f5a24
|
||||
- [x] 为 TRON 标签添加 TronLink 钱包检测并自动填充 TRON 接收地址
|
||||
- [x] 构建并部署到备份服务器 pre-sale.newassetchain.io
|
||||
- [x] 后台 Site Settings 添加“一键开启/关闭预售活动”功能(数据库字段 + 后端 API + 前端开关 UI + 首页状态联动)
|
||||
|
||||
## v7 钉包连接全面修复
|
||||
- [x] 全面修复所有 EVM 钉包(MetaMask、Trust Wallet、OKX、Coinbase等)无法自动填写 EVM 地址的问题
|
||||
- [x] 重写 useWallet hook 支持所有主流 EVM 钉包自动识别
|
||||
- [x] 将页面所有“EVM 地址”文案改为“XIC 接收地址”(中文)/ "XIC Receiving Address"(英文)
|
||||
- [x] 构建并部署到备份服务器并验证
|
||||
|
||||
## v7 钱包列表选择器
|
||||
- [x] 创建 WalletSelector 组件(MetaMask、Trust Wallet、OKX、Coinbase、TokenPocket 检测+连接+安装引导)
|
||||
- [x] 集成 WalletSelector 到 TRON 标签 XIC 接收地址区域- [x] 集成 WalletSelector 到 BSC/ETH 购买面板替换原 Connect Wallet 按鈕钮
|
||||
- [x] 构建并部署到备份服务器
|
||||
|
|
|
|||
Loading…
Reference in New Issue