nac-presale/client/src/components/WalletSelector.tsx

740 lines
31 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

// NAC XIC Presale — Wallet Selector Component
// Detects installed EVM wallets and shows connect/install buttons for each
// v3: added mobile detection, DeepLink support for MetaMask/Trust/OKX App
import { useState, useEffect, useCallback } from "react";
type Lang = "zh" | "en";
interface WalletInfo {
id: string;
name: string;
icon: React.ReactNode;
installUrl: string;
mobileDeepLink?: string; // DeepLink to open current page in wallet's in-app browser
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>
);
// ── Mobile detection ──────────────────────────────────────────────────────────
function isMobileBrowser(): boolean {
if (typeof window === "undefined") return false;
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 {
if (typeof window === "undefined") return false;
const ua = navigator.userAgent.toLowerCase();
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;
return !!(
eth?.isMetaMask ||
eth?.isTrust ||
eth?.isTrustWallet ||
eth?.isOKExWallet ||
eth?.isOkxWallet ||
ua.includes("metamask") ||
ua.includes("trust") ||
ua.includes("okex") ||
ua.includes("tokenpocket") ||
ua.includes("bitkeep")
);
}
// 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 ────────────────────────────────────────────────
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 (err: unknown) {
const error = err as { code?: number; message?: string };
// User rejected
if (error?.code === 4001) throw new Error("user_rejected");
// MetaMask not initialized / locked
if (error?.code === -32002) throw new Error("wallet_pending");
throw err;
}
}
// ── Wallet definitions ────────────────────────────────────────────────────────
function buildWallets(): WalletInfo[] {
return [
{
id: "metamask",
name: "MetaMask",
icon: <MetaMaskIcon />,
installUrl: "https://metamask.io/download/",
mobileDeepLink: buildDeepLink("https://metamask.app.link/dapp/"),
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",
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;
},
},
{
id: "okx",
name: "OKX Wallet",
icon: <OKXIcon />,
installUrl: "https://www.okx.com/web3",
mobileDeepLink: buildDeepLink("okx://wallet/dapp/url?dappUrl=https://"),
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;
},
},
];
}
// Validate Ethereum address format
function isValidEthAddress(addr: string): boolean {
return /^0x[0-9a-fA-F]{40}$/.test(addr);
}
// ── Mobile DeepLink Panel ─────────────────────────────────────────────────────
function MobileDeepLinkPanel({ lang }: { lang: Lang }) {
const currentUrl = typeof window !== "undefined" ? window.location.href : "https://pre-sale.newassetchain.io";
const urlWithoutProtocol = currentUrl.replace(/^https?:\/\//, "");
const mobileWallets = [
{
id: "metamask",
name: "MetaMask",
icon: <MetaMaskIcon />,
deepLink: `https://metamask.app.link/dapp/${urlWithoutProtocol}`,
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",
name: "OKX Wallet",
icon: <OKXIcon />,
deepLink: `okx://wallet/dapp/url?dappUrl=${encodeURIComponent(currentUrl)}`,
installUrl: "https://www.okx.com/web3",
color: "#00F0FF",
},
{
id: "tokenpocket",
name: "TokenPocket",
icon: <TokenPocketIcon />,
deepLink: `tpoutside://pull?param=${encodeURIComponent(JSON.stringify({ url: currentUrl }))}`,
installUrl: "https://www.tokenpocket.pro/en/download/app",
color: "#2980FE",
},
];
return (
<div className="space-y-3">
{/* Mobile guidance header */}
<div
className="rounded-xl p-4"
style={{ background: "rgba(240,180,41,0.08)", border: "1px solid rgba(240,180,41,0.25)" }}
>
<div className="flex items-start gap-3">
<span className="text-xl flex-shrink-0">📱</span>
<div>
<p className="text-sm font-semibold text-amber-300 mb-1">
{lang === "zh" ? "手机端连接钱包" : "Connect Wallet on Mobile"}
</p>
<p className="text-xs text-white/50 leading-relaxed">
{lang === "zh"
? "手机浏览器不支持钱包扩展。请选择以下任一钱包 App在其内置浏览器中打开本页面即可连接钱包。"
: "Mobile browsers don't support wallet extensions. Open this page in a wallet app's built-in browser to connect."}
</p>
</div>
</div>
</div>
{/* Wallet DeepLink buttons */}
<div className="space-y-2">
<p className="text-xs text-white/40 text-center">
{lang === "zh" ? "选择钱包 App 打开本页面" : "Choose a wallet app to open this page"}
</p>
{mobileWallets.map(wallet => (
<a
key={wallet.id}
href={wallet.deepLink}
className="w-full flex items-center gap-3 px-4 py-3 rounded-xl transition-all hover:opacity-90 active:scale-[0.98] block"
style={{
background: "rgba(0,212,255,0.06)",
border: "1px solid rgba(0,212,255,0.2)",
}}
>
<span className="flex-shrink-0">{wallet.icon}</span>
<span className="flex-1 text-sm font-semibold text-white">{wallet.name}</span>
<span
className="text-xs px-2 py-0.5 rounded-full font-medium flex-shrink-0"
style={{ background: "rgba(0,212,255,0.15)", color: "#00d4ff" }}
>
{lang === "zh" ? "在 App 中打开" : "Open in App"}
</span>
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="rgba(0,212,255,0.6)" strokeWidth="2" className="flex-shrink-0">
<path d="M18 13v6a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h6"/>
<polyline points="15 3 21 3 21 9"/>
<line x1="10" y1="14" x2="21" y2="3"/>
</svg>
</a>
))}
</div>
{/* Step guide */}
<div
className="rounded-xl p-3 space-y-2"
style={{ background: "rgba(255,255,255,0.03)", border: "1px solid rgba(255,255,255,0.08)" }}
>
<p className="text-xs font-semibold text-white/50 mb-2">
{lang === "zh" ? "操作步骤" : "How it works"}
</p>
{[
lang === "zh" ? "1. 点击上方任一钱包 App 按钮" : "1. Tap any wallet app button above",
lang === "zh" ? "2. 在钱包 App 的内置浏览器中打开本页面" : "2. Page opens in the wallet app's browser",
lang === "zh" ? "3. 点击「连接钱包」即可自动连接" : "3. Tap 'Connect Wallet' to connect automatically",
].map((step, i) => (
<p key={i} className="text-xs text-white/35 leading-relaxed">{step}</p>
))}
</div>
</div>
);
}
// ── 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);
const [detecting, setDetecting] = useState(true);
const [showManual, setShowManual] = useState(false);
const [manualAddress, setManualAddress] = useState("");
const [manualError, setManualError] = useState<string | null>(null);
const [isMobile] = useState(() => isMobileBrowser());
const [inWalletBrowser] = useState(() => isInWalletBrowser());
const detectWallets = useCallback(() => {
setDetecting(true);
setError(null);
// Wait for wallet extensions to fully inject (up to 1500ms)
const timer = setTimeout(() => {
setWallets(buildWallets());
setDetecting(false);
}, 1500);
return () => clearTimeout(timer);
}, []);
useEffect(() => {
const cleanup = detectWallets();
return cleanup;
}, [detectWallets]);
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 (err: unknown) {
const error = err as Error;
if (error.message === "user_rejected") {
setError(lang === "zh" ? "已取消连接" : "Connection cancelled");
} else if (error.message === "wallet_pending") {
setError(lang === "zh" ? "钱包请求处理中,请检查钱包弹窗" : "Wallet request pending, please check your wallet popup");
} else if (error.message?.includes("not initialized") || error.message?.includes("setup")) {
setError(lang === "zh"
? "请先完成钱包初始化设置,然后刷新页面重试"
: "Please complete wallet setup first, then refresh the page");
} else {
setError(lang === "zh" ? "连接失败,请重试" : "Connection failed, please try again");
}
} finally {
setConnecting(null);
}
};
const handleManualSubmit = () => {
const addr = manualAddress.trim();
if (!addr) {
setManualError(lang === "zh" ? "请输入钱包地址" : "Please enter wallet address");
return;
}
if (!isValidEthAddress(addr)) {
setManualError(lang === "zh" ? "地址格式无效请输入正确的以太坊地址0x开头42位" : "Invalid address format. Must be 0x followed by 40 hex characters");
return;
}
setManualError(null);
onAddressDetected(addr);
};
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>
);
}
// ── Mobile browser (not in wallet app) — show DeepLink guide ──────────────
if (isMobile && !inWalletBrowser && !detecting) {
const hasInstalledWallet = installedWallets.length > 0;
if (!hasInstalledWallet) {
return (
<div className="space-y-3">
<MobileDeepLinkPanel lang={lang} />
{/* Manual address fallback */}
<div className="pt-1">
<button
onClick={() => { setShowManual(!showManual); setManualError(null); }}
className="w-full text-xs text-white/30 hover:text-white/50 transition-colors py-1 flex items-center justify-center gap-1"
>
<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
<path d="M11 4H4a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2v-7"/>
<path d="M18.5 2.5a2.121 2.121 0 0 1 3 3L12 15l-4 1 1-4 9.5-9.5z"/>
</svg>
{showManual
? (lang === "zh" ? "收起手动输入" : "Hide manual input")
: (lang === "zh" ? "手动输入钱包地址" : "Enter address manually")}
</button>
{showManual && (
<div className="mt-2 space-y-2">
<p className="text-xs text-white/40 text-center">
{lang === "zh"
? "直接输入您的 EVM 钱包地址0x 开头)"
: "Enter your EVM wallet address (starts with 0x)"}
</p>
<div className="flex gap-2">
<input
type="text"
value={manualAddress}
onChange={e => { setManualAddress(e.target.value); setManualError(null); }}
placeholder={lang === "zh" ? "0x..." : "0x..."}
className="flex-1 px-3 py-2 rounded-lg text-xs font-mono text-white/80 outline-none focus:ring-1"
style={{
background: "rgba(255,255,255,0.06)",
border: manualError ? "1px solid rgba(255,80,80,0.5)" : "1px solid rgba(255,255,255,0.12)",
}}
onKeyDown={e => e.key === "Enter" && handleManualSubmit()}
/>
<button
onClick={handleManualSubmit}
className="px-3 py-2 rounded-lg text-xs font-semibold transition-all hover:opacity-90 active:scale-95 whitespace-nowrap"
style={{ background: "rgba(0,212,255,0.15)", color: "#00d4ff", border: "1px solid rgba(0,212,255,0.3)" }}
>
{lang === "zh" ? "确认" : "Confirm"}
</button>
</div>
{manualError && (
<p className="text-xs text-red-400">{manualError}</p>
)}
</div>
)}
</div>
</div>
);
}
}
// ── Loading state ─────────────────────────────────────────────────────────
if (detecting) {
return (
<div className="space-y-3">
<div className="flex items-center justify-between">
<p className="text-xs font-semibold text-white/60 uppercase tracking-wider">
{lang === "zh" ? "选择钱包自动填充地址" : "Select wallet to auto-fill address"}
</p>
</div>
<div className="flex items-center justify-center py-4 gap-2">
<svg className="animate-spin w-4 h-4 text-white/40" 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>
<span className="text-xs text-white/40">
{lang === "zh" ? "正在检测钱包..." : "Detecting wallets..."}
</span>
</div>
</div>
);
}
return (
<div className="space-y-3">
<div className="flex items-center justify-between">
<p className="text-xs font-semibold text-white/60 uppercase tracking-wider">
{lang === "zh" ? "选择钱包自动填充地址" : "Select wallet to auto-fill address"}
</p>
{/* Refresh detection button */}
<button
onClick={detectWallets}
disabled={detecting}
className="flex items-center gap-1 text-xs px-2 py-1 rounded-lg transition-all hover:opacity-80"
style={{ background: "rgba(0,212,255,0.1)", color: "rgba(0,212,255,0.7)", border: "1px solid rgba(0,212,255,0.2)" }}
title={lang === "zh" ? "重新检测钱包" : "Re-detect wallets"}
>
<svg
width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2"
className={detecting ? "animate-spin" : ""}
>
<path d="M23 4v6h-6M1 20v-6h6"/>
<path d="M3.51 9a9 9 0 0 1 14.85-3.36L23 10M1 14l4.64 4.36A9 9 0 0 0 20.49 15"/>
</svg>
{detecting
? (lang === "zh" ? "检测中..." : "Detecting...")
: (lang === "zh" ? "刷新" : "Refresh")}
</button>
</div>
{/* 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 — desktop */}
{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 mb-3">
{lang === "zh"
? "请安装以下任一钱包,完成设置后点击上方「刷新」按钮"
: "Install any wallet below, then click Refresh above after setup"}
</p>
<p className="text-xs text-amber-400/70">
{lang === "zh"
? "💡 已安装MetaMask请先完成钱包初始化创建或导入钱包再点击刷新"
: "💡 Have MetaMask? Complete wallet setup (create or import) first, then click Refresh"}
</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>
)}
{/* Manual address input — divider */}
<div className="pt-1">
<button
onClick={() => { setShowManual(!showManual); setManualError(null); }}
className="w-full text-xs text-white/30 hover:text-white/50 transition-colors py-1 flex items-center justify-center gap-1"
>
<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
<path d="M11 4H4a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2v-7"/>
<path d="M18.5 2.5a2.121 2.121 0 0 1 3 3L12 15l-4 1 1-4 9.5-9.5z"/>
</svg>
{showManual
? (lang === "zh" ? "收起手动输入" : "Hide manual input")
: (lang === "zh" ? "手动输入钱包地址" : "Enter address manually")}
</button>
{showManual && (
<div className="mt-2 space-y-2">
<p className="text-xs text-white/40 text-center">
{lang === "zh"
? "直接输入您的 EVM 钱包地址0x 开头)"
: "Enter your EVM wallet address (starts with 0x)"}
</p>
<div className="flex gap-2">
<input
type="text"
value={manualAddress}
onChange={e => { setManualAddress(e.target.value); setManualError(null); }}
placeholder={lang === "zh" ? "0x..." : "0x..."}
className="flex-1 px-3 py-2 rounded-lg text-xs font-mono text-white/80 outline-none focus:ring-1"
style={{
background: "rgba(255,255,255,0.06)",
border: manualError ? "1px solid rgba(255,80,80,0.5)" : "1px solid rgba(255,255,255,0.12)",
}}
onKeyDown={e => e.key === "Enter" && handleManualSubmit()}
/>
<button
onClick={handleManualSubmit}
className="px-3 py-2 rounded-lg text-xs font-semibold transition-all hover:opacity-90 active:scale-95 whitespace-nowrap"
style={{ background: "rgba(0,212,255,0.15)", color: "#00d4ff", border: "1px solid rgba(0,212,255,0.3)" }}
>
{lang === "zh" ? "确认" : "Confirm"}
</button>
</div>
{manualError && (
<p className="text-xs text-red-400">{manualError}</p>
)}
</div>
)}
</div>
</div>
);
}