1036 lines
43 KiB
TypeScript
1036 lines
43 KiB
TypeScript
// NAC XIC Presale — Wallet Selector Component
|
||
// Detects installed EVM wallets and shows connect/install buttons for each
|
||
// v5: improved error handling, MetaMask permission reset guide, connection diagnostics
|
||
|
||
import { useState, useEffect, useCallback } from "react";
|
||
import type { EthProvider } from "@/hooks/useWallet";
|
||
|
||
type Lang = "zh" | "en";
|
||
|
||
interface WalletInfo {
|
||
id: string;
|
||
name: string;
|
||
icon: React.ReactNode;
|
||
installUrl: string;
|
||
mobileDeepLink?: string;
|
||
isInstalled: () => boolean;
|
||
connect: () => Promise<{ address: string; rawProvider: EthProvider } | null>;
|
||
network: "evm" | "tron";
|
||
}
|
||
|
||
interface WalletSelectorProps {
|
||
lang: Lang;
|
||
onAddressDetected: (address: string, network?: "evm" | "tron", rawProvider?: EthProvider) => void;
|
||
connectedAddress?: string;
|
||
compact?: boolean;
|
||
showTron?: boolean;
|
||
}
|
||
|
||
// ── 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>
|
||
);
|
||
|
||
const TronLinkIcon = () => (
|
||
<svg width="24" height="24" viewBox="0 0 24 24" fill="none">
|
||
<circle cx="12" cy="12" r="12" fill="#FF0013"/>
|
||
<path d="M17.5 9.5L12 5L6.5 9.5V15.5L12 20L17.5 15.5V9.5Z" fill="white" fillOpacity="0.15" stroke="white" strokeWidth="0.5"/>
|
||
<path d="M12 7.5L8 10.5V14.5L12 17.5L16 14.5V10.5L12 7.5Z" fill="white" fillOpacity="0.9"/>
|
||
<path d="M12 10L10 11.5V13.5L12 15L14 13.5V11.5L12 10Z" fill="#FF0013"/>
|
||
</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);
|
||
}
|
||
|
||
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;
|
||
const tronLink = w.tronLink as { ready?: boolean } | undefined;
|
||
return !!(
|
||
eth?.isMetaMask ||
|
||
eth?.isTrust ||
|
||
eth?.isTrustWallet ||
|
||
eth?.isOKExWallet ||
|
||
eth?.isOkxWallet ||
|
||
tronLink?.ready ||
|
||
ua.includes("metamask") ||
|
||
ua.includes("trust") ||
|
||
ua.includes("okex") ||
|
||
ua.includes("tokenpocket") ||
|
||
ua.includes("bitkeep") ||
|
||
ua.includes("tronlink")
|
||
);
|
||
}
|
||
|
||
// ── Provider detection helpers ────────────────────────────────────────────────
|
||
|
||
type TronLinkProvider = {
|
||
ready: boolean;
|
||
tronWeb?: {
|
||
defaultAddress?: { base58?: string };
|
||
trx?: unknown;
|
||
};
|
||
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;
|
||
}
|
||
|
||
function getTronLink(): TronLinkProvider | null {
|
||
if (typeof window === "undefined") return null;
|
||
const w = window as unknown as { tronLink?: TronLinkProvider; tronWeb?: TronLinkProvider["tronWeb"] };
|
||
if (w.tronLink?.ready) return w.tronLink;
|
||
if (w.tronWeb) {
|
||
return {
|
||
ready: true,
|
||
tronWeb: w.tronWeb,
|
||
request: async () => null,
|
||
};
|
||
}
|
||
return null;
|
||
}
|
||
|
||
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;
|
||
}
|
||
|
||
// ── Improved requestAccounts with better error classification ─────────────────
|
||
async function requestAccounts(provider: EthProvider): Promise<{ address: string; rawProvider: EthProvider } | null> {
|
||
try {
|
||
// First try eth_accounts (silent) to check if already connected
|
||
const existingAccounts = await provider.request({ method: "eth_accounts" }) as string[];
|
||
if (existingAccounts && existingAccounts.length > 0) {
|
||
return { address: existingAccounts[0], rawProvider: provider };
|
||
}
|
||
} catch {
|
||
// Ignore - proceed to eth_requestAccounts
|
||
}
|
||
|
||
try {
|
||
const accounts = await provider.request({ method: "eth_requestAccounts" }) as string[];
|
||
const address = accounts?.[0] ?? null;
|
||
if (!address) return null;
|
||
return { address, rawProvider: provider };
|
||
} catch (err: unknown) {
|
||
const error = err as { code?: number; message?: string };
|
||
console.log("[WalletSelector] requestAccounts error:", error.code, error.message);
|
||
if (error?.code === 4001) throw new Error("user_rejected");
|
||
if (error?.code === -32002) throw new Error("wallet_pending");
|
||
// Some wallets throw with message instead of code
|
||
if (error?.message?.toLowerCase().includes("user rejected") ||
|
||
error?.message?.toLowerCase().includes("user denied") ||
|
||
error?.message?.toLowerCase().includes("cancelled")) {
|
||
throw new Error("user_rejected");
|
||
}
|
||
throw err;
|
||
}
|
||
}
|
||
|
||
async function requestTronAccounts(provider: TronLinkProvider): Promise<{ address: string; rawProvider: EthProvider } | null> {
|
||
try {
|
||
if (provider.tronWeb?.defaultAddress?.base58) {
|
||
return { address: provider.tronWeb.defaultAddress.base58, rawProvider: provider as unknown as EthProvider };
|
||
}
|
||
const result = await provider.request({ method: "tron_requestAccounts" }) as { code?: number; message?: string };
|
||
if (result?.code === 200) {
|
||
const w = window as unknown as { tronWeb?: { defaultAddress?: { base58?: string } } };
|
||
const address = w.tronWeb?.defaultAddress?.base58 ?? null;
|
||
if (!address) return null;
|
||
return { address, rawProvider: provider as unknown as EthProvider };
|
||
}
|
||
if (result?.code === 4001) throw new Error("user_rejected");
|
||
return null;
|
||
} catch (err: unknown) {
|
||
const error = err as { code?: number; message?: string };
|
||
if (error?.code === 4001 || error?.message === "user_rejected") throw new Error("user_rejected");
|
||
throw err;
|
||
}
|
||
}
|
||
|
||
// ── Wallet definitions ────────────────────────────────────────────────────────
|
||
|
||
function buildWallets(showTron: boolean): WalletInfo[] {
|
||
const currentUrl = typeof window !== "undefined" ? window.location.href : "https://pre-sale.newassetchain.io";
|
||
const urlWithoutProtocol = currentUrl.replace(/^https?:\/\//, "");
|
||
|
||
const evmWallets: WalletInfo[] = [
|
||
{
|
||
id: "metamask",
|
||
name: "MetaMask",
|
||
icon: <MetaMaskIcon />,
|
||
installUrl: "https://metamask.io/download/",
|
||
mobileDeepLink: `https://metamask.app.link/dapp/${urlWithoutProtocol}`,
|
||
isInstalled: () => !!findProvider(p => !!p.isMetaMask),
|
||
connect: async () => {
|
||
const p = findProvider(p => !!p.isMetaMask) ?? getEth();
|
||
return p ? requestAccounts(p) : null;
|
||
},
|
||
network: "evm",
|
||
},
|
||
{
|
||
id: "trust",
|
||
name: "Trust Wallet",
|
||
icon: <TrustWalletIcon />,
|
||
installUrl: "https://trustwallet.com/download",
|
||
mobileDeepLink: `https://link.trustwallet.com/open_url?coin_id=60&url=${encodeURIComponent(currentUrl)}`,
|
||
isInstalled: () => !!findProvider(p => !!(p.isTrust || p.isTrustWallet)),
|
||
connect: async () => {
|
||
const p = findProvider(p => !!(p.isTrust || p.isTrustWallet)) ?? getEth();
|
||
return p ? requestAccounts(p) : null;
|
||
},
|
||
network: "evm",
|
||
},
|
||
{
|
||
id: "okx",
|
||
name: "OKX Wallet",
|
||
icon: <OKXIcon />,
|
||
installUrl: "https://www.okx.com/web3",
|
||
mobileDeepLink: `okx://wallet/dapp/url?dappUrl=${encodeURIComponent(currentUrl)}`,
|
||
isInstalled: () => !!(getOKX() || findProvider(p => !!(p.isOKExWallet || p.isOkxWallet))),
|
||
connect: async () => {
|
||
const p = getOKX() ?? findProvider(p => !!(p.isOKExWallet || p.isOkxWallet));
|
||
return p ? requestAccounts(p) : null;
|
||
},
|
||
network: "evm",
|
||
},
|
||
{
|
||
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;
|
||
},
|
||
network: "evm",
|
||
},
|
||
{
|
||
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;
|
||
},
|
||
network: "evm",
|
||
},
|
||
{
|
||
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;
|
||
},
|
||
network: "evm",
|
||
},
|
||
];
|
||
|
||
const tronWallets: WalletInfo[] = [
|
||
{
|
||
id: "tronlink",
|
||
name: "TronLink",
|
||
icon: <TronLinkIcon />,
|
||
installUrl: "https://www.tronlink.org/",
|
||
mobileDeepLink: `tronlinkoutside://pull.activity?param=${encodeURIComponent(JSON.stringify({ url: currentUrl, action: "open", protocol: "tronlink", version: "1.0" }))}`,
|
||
isInstalled: () => !!getTronLink(),
|
||
connect: async () => {
|
||
const tron = getTronLink();
|
||
if (!tron) return null;
|
||
return requestTronAccounts(tron);
|
||
},
|
||
network: "tron",
|
||
},
|
||
];
|
||
|
||
return showTron ? [...tronWallets, ...evmWallets] : evmWallets;
|
||
}
|
||
|
||
// Validate Ethereum address format
|
||
function isValidEthAddress(addr: string): boolean {
|
||
return /^0x[0-9a-fA-F]{40}$/.test(addr);
|
||
}
|
||
|
||
// Validate TRON address format (T + 33 base58 chars = 34 chars total)
|
||
function isValidTronAddress(addr: string): boolean {
|
||
return /^T[1-9A-HJ-NP-Za-km-z]{33}$/.test(addr);
|
||
}
|
||
|
||
// ── Mobile DeepLink Panel ─────────────────────────────────────────────────────
|
||
|
||
function MobileDeepLinkPanel({ lang, showTron }: { lang: Lang; showTron: boolean }) {
|
||
const currentUrl = typeof window !== "undefined" ? window.location.href : "https://pre-sale.newassetchain.io";
|
||
const urlWithoutProtocol = currentUrl.replace(/^https?:\/\//, "");
|
||
const [showMore, setShowMore] = useState(false);
|
||
|
||
const metamaskDeepLink = `https://metamask.app.link/dapp/${urlWithoutProtocol}`;
|
||
|
||
const otherWallets = [
|
||
...( showTron ? [{
|
||
id: "tronlink",
|
||
name: "TronLink",
|
||
icon: <TronLinkIcon />,
|
||
deepLink: `tronlinkoutside://pull.activity?param=${encodeURIComponent(JSON.stringify({ url: currentUrl, action: "open", protocol: "tronlink", version: "1.0" }))}`,
|
||
badge: "TRON",
|
||
badgeColor: "#FF0013",
|
||
}] : []),
|
||
{
|
||
id: "trust",
|
||
name: "Trust Wallet",
|
||
icon: <TrustWalletIcon />,
|
||
deepLink: `https://link.trustwallet.com/open_url?coin_id=60&url=${encodeURIComponent(currentUrl)}`,
|
||
badge: "EVM",
|
||
badgeColor: "#3375BB",
|
||
},
|
||
{
|
||
id: "okx",
|
||
name: "OKX Wallet",
|
||
icon: <OKXIcon />,
|
||
deepLink: `okx://wallet/dapp/url?dappUrl=${encodeURIComponent(currentUrl)}`,
|
||
badge: "EVM",
|
||
badgeColor: "#00F0FF",
|
||
},
|
||
{
|
||
id: "tokenpocket",
|
||
name: "TokenPocket",
|
||
icon: <TokenPocketIcon />,
|
||
deepLink: `tpdapp://open?params=${encodeURIComponent(JSON.stringify({ url: currentUrl, chain: "ETH", source: "NAC-Presale" }))}`,
|
||
badge: "EVM/TRON",
|
||
badgeColor: "#2980FE",
|
||
},
|
||
];
|
||
|
||
return (
|
||
<div className="space-y-3">
|
||
<a
|
||
href={metamaskDeepLink}
|
||
className="w-full flex items-center gap-3 px-4 py-4 rounded-2xl transition-all hover:opacity-90 active:scale-[0.98] block"
|
||
style={{
|
||
background: "linear-gradient(135deg, rgba(226,118,37,0.18) 0%, rgba(240,180,41,0.12) 100%)",
|
||
border: "1.5px solid rgba(226,118,37,0.5)",
|
||
boxShadow: "0 0 20px rgba(226,118,37,0.15)",
|
||
}}
|
||
>
|
||
<MetaMaskIcon />
|
||
<div className="flex-1">
|
||
<p className="text-sm font-bold text-white">MetaMask</p>
|
||
<p className="text-xs text-white/40">
|
||
{lang === "zh" ? "在 MetaMask 内置浏览器中打开" : "Open in MetaMask browser"}
|
||
</p>
|
||
</div>
|
||
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="rgba(226,118,37,0.8)" strokeWidth="2" className="flex-shrink-0">
|
||
<path d="M18 13v6a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h6"/>
|
||
<polyline points="15 3 21 3 21 9"/>
|
||
<line x1="10" y1="14" x2="21" y2="3"/>
|
||
</svg>
|
||
</a>
|
||
|
||
<button
|
||
onClick={() => setShowMore(v => !v)}
|
||
className="w-full text-xs text-white/35 hover:text-white/55 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"
|
||
style={{ transform: showMore ? "rotate(180deg)" : "none", transition: "transform 0.2s" }}>
|
||
<path d="M6 9l6 6 6-6"/>
|
||
</svg>
|
||
{showMore
|
||
? (lang === "zh" ? "收起其他钱包" : "Hide other wallets")
|
||
: (lang === "zh" ? "其他钱包(Trust / OKX / TokenPocket)" : "Other wallets (Trust / OKX / TokenPocket)")}
|
||
</button>
|
||
|
||
{showMore && (
|
||
<div className="space-y-2">
|
||
{otherWallets.map(wallet => (
|
||
<a
|
||
key={wallet.id}
|
||
href={wallet.deepLink}
|
||
className="w-full flex items-center gap-3 px-4 py-3 rounded-xl transition-all hover:opacity-90 active:scale-[0.98] block"
|
||
style={{
|
||
background: "rgba(0,212,255,0.05)",
|
||
border: "1px solid rgba(0,212,255,0.18)",
|
||
}}
|
||
>
|
||
<span className="flex-shrink-0">{wallet.icon}</span>
|
||
<span className="flex-1 text-sm font-semibold text-white">{wallet.name}</span>
|
||
<span
|
||
className="text-xs px-2 py-0.5 rounded-full font-medium flex-shrink-0"
|
||
style={{ background: `${wallet.badgeColor}22`, color: wallet.badgeColor, border: `1px solid ${wallet.badgeColor}44` }}
|
||
>
|
||
{wallet.badge}
|
||
</span>
|
||
<svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="rgba(0,212,255,0.5)" strokeWidth="2" className="flex-shrink-0">
|
||
<path d="M18 13v6a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h6"/>
|
||
<polyline points="15 3 21 3 21 9"/>
|
||
<line x1="10" y1="14" x2="21" y2="3"/>
|
||
</svg>
|
||
</a>
|
||
))}
|
||
</div>
|
||
)}
|
||
</div>
|
||
);
|
||
}
|
||
|
||
// ── Error Help Panel ──────────────────────────────────────────────────────────
|
||
// Shows specific troubleshooting steps based on error type
|
||
|
||
function ErrorHelpPanel({ errorType, walletName, lang, onRetry }: {
|
||
errorType: "user_rejected" | "wallet_pending" | "not_initialized" | "unknown";
|
||
walletName: string;
|
||
lang: Lang;
|
||
onRetry: () => void;
|
||
}) {
|
||
const isZh = lang === "zh";
|
||
|
||
if (errorType === "user_rejected") {
|
||
return (
|
||
<div
|
||
className="rounded-xl p-4 space-y-3"
|
||
style={{ background: "rgba(255,80,80,0.06)", border: "1px solid rgba(255,80,80,0.2)" }}
|
||
>
|
||
<div className="flex items-start gap-2">
|
||
<span className="text-red-400 text-base flex-shrink-0">✗</span>
|
||
<div>
|
||
<p className="text-sm font-semibold text-red-400">
|
||
{isZh ? "连接被拒绝" : "Connection Rejected"}
|
||
</p>
|
||
<p className="text-xs text-white/50 mt-1">
|
||
{isZh
|
||
? `${walletName} 拒绝了连接请求。可能原因:`
|
||
: `${walletName} rejected the connection. Possible reasons:`}
|
||
</p>
|
||
</div>
|
||
</div>
|
||
<div className="space-y-1.5 text-xs text-white/40 pl-4">
|
||
<p>
|
||
{isZh
|
||
? "1. 您在钱包弹窗中点击了「拒绝」"
|
||
: "1. You clicked \"Reject\" in the wallet popup"}
|
||
</p>
|
||
<p>
|
||
{isZh
|
||
? "2. 该网站之前被您在 MetaMask 中屏蔽(最常见原因)"
|
||
: "2. This site was previously blocked in your wallet (most common)"}
|
||
</p>
|
||
<p>
|
||
{isZh
|
||
? "3. 钱包弹窗未显示(被浏览器拦截)"
|
||
: "3. Wallet popup was blocked by browser"}
|
||
</p>
|
||
</div>
|
||
{walletName === "MetaMask" && (
|
||
<div
|
||
className="rounded-lg p-3 space-y-1.5"
|
||
style={{ background: "rgba(240,180,41,0.06)", border: "1px solid rgba(240,180,41,0.15)" }}
|
||
>
|
||
<p className="text-xs font-semibold text-amber-400">
|
||
{isZh ? "🔧 MetaMask 权限重置步骤:" : "🔧 MetaMask Permission Reset:"}
|
||
</p>
|
||
<div className="space-y-1 text-xs text-white/40">
|
||
<p>{isZh ? "① 点击 MetaMask 图标打开扩展" : "① Click MetaMask icon to open extension"}</p>
|
||
<p>{isZh ? "② 点击右上角菜单(三个点)" : "② Click top-right menu (three dots)"}</p>
|
||
<p>{isZh ? "③ 选择「已连接的网站」" : "③ Select \"Connected Sites\""}</p>
|
||
<p>{isZh ? "④ 找到本网站并删除" : "④ Find this site and remove it"}</p>
|
||
<p>{isZh ? "⑤ 回到此页面重新点击连接" : "⑤ Return here and try connecting again"}</p>
|
||
</div>
|
||
</div>
|
||
)}
|
||
<button
|
||
onClick={onRetry}
|
||
className="w-full py-2 rounded-lg text-xs font-semibold transition-all hover:opacity-90"
|
||
style={{ background: "rgba(0,212,255,0.12)", color: "#00d4ff", border: "1px solid rgba(0,212,255,0.25)" }}
|
||
>
|
||
{isZh ? "重试连接" : "Retry Connection"}
|
||
</button>
|
||
</div>
|
||
);
|
||
}
|
||
|
||
if (errorType === "wallet_pending") {
|
||
return (
|
||
<div
|
||
className="rounded-xl p-4 space-y-3"
|
||
style={{ background: "rgba(240,180,41,0.06)", border: "1px solid rgba(240,180,41,0.2)" }}
|
||
>
|
||
<div className="flex items-start gap-2">
|
||
<span className="text-amber-400 text-base flex-shrink-0">⏳</span>
|
||
<div>
|
||
<p className="text-sm font-semibold text-amber-400">
|
||
{isZh ? "钱包有待处理的请求" : "Wallet Has Pending Request"}
|
||
</p>
|
||
<p className="text-xs text-white/50 mt-1">
|
||
{isZh
|
||
? "请查看钱包弹窗并处理待处理的请求,然后重试"
|
||
: "Please check your wallet popup and handle the pending request, then retry"}
|
||
</p>
|
||
</div>
|
||
</div>
|
||
<button
|
||
onClick={onRetry}
|
||
className="w-full py-2 rounded-lg text-xs font-semibold transition-all hover:opacity-90"
|
||
style={{ background: "rgba(240,180,41,0.12)", color: "#f0b429", border: "1px solid rgba(240,180,41,0.25)" }}
|
||
>
|
||
{isZh ? "重试" : "Retry"}
|
||
</button>
|
||
</div>
|
||
);
|
||
}
|
||
|
||
if (errorType === "not_initialized") {
|
||
return (
|
||
<div
|
||
className="rounded-xl p-4 space-y-3"
|
||
style={{ background: "rgba(0,212,255,0.06)", border: "1px solid rgba(0,212,255,0.2)" }}
|
||
>
|
||
<div className="flex items-start gap-2">
|
||
<span className="text-cyan-400 text-base flex-shrink-0">⚙️</span>
|
||
<div>
|
||
<p className="text-sm font-semibold text-cyan-400">
|
||
{isZh ? "钱包未完成初始化" : "Wallet Not Initialized"}
|
||
</p>
|
||
<p className="text-xs text-white/50 mt-1">
|
||
{isZh
|
||
? "请先完成钱包设置(创建或导入钱包),然后刷新页面重试"
|
||
: "Please complete wallet setup (create or import wallet), then refresh the page"}
|
||
</p>
|
||
</div>
|
||
</div>
|
||
<button
|
||
onClick={() => window.location.reload()}
|
||
className="w-full py-2 rounded-lg text-xs font-semibold transition-all hover:opacity-90"
|
||
style={{ background: "rgba(0,212,255,0.12)", color: "#00d4ff", border: "1px solid rgba(0,212,255,0.25)" }}
|
||
>
|
||
{isZh ? "刷新页面" : "Refresh Page"}
|
||
</button>
|
||
</div>
|
||
);
|
||
}
|
||
|
||
// Unknown error
|
||
return (
|
||
<div
|
||
className="rounded-xl p-4 space-y-2"
|
||
style={{ background: "rgba(255,80,80,0.06)", border: "1px solid rgba(255,80,80,0.2)" }}
|
||
>
|
||
<p className="text-sm font-semibold text-red-400">
|
||
{isZh ? "连接失败" : "Connection Failed"}
|
||
</p>
|
||
<p className="text-xs text-white/40">
|
||
{isZh ? "请刷新页面后重试,或尝试其他钱包" : "Please refresh the page and try again, or try another wallet"}
|
||
</p>
|
||
<button
|
||
onClick={onRetry}
|
||
className="w-full py-2 rounded-lg text-xs font-semibold transition-all hover:opacity-90"
|
||
style={{ background: "rgba(0,212,255,0.12)", color: "#00d4ff", border: "1px solid rgba(0,212,255,0.25)" }}
|
||
>
|
||
{isZh ? "重试" : "Retry"}
|
||
</button>
|
||
</div>
|
||
);
|
||
}
|
||
|
||
// ── WalletSelector Component ──────────────────────────────────────────────────
|
||
|
||
export function WalletSelector({ lang, onAddressDetected, connectedAddress, compact = false, showTron = false }: WalletSelectorProps) {
|
||
const [wallets, setWallets] = useState<WalletInfo[]>([]);
|
||
const [connecting, setConnecting] = useState<string | null>(null);
|
||
const [errorType, setErrorType] = useState<"user_rejected" | "wallet_pending" | "not_initialized" | "unknown" | null>(null);
|
||
const [errorWalletName, setErrorWalletName] = useState<string>("");
|
||
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 [lastConnectedWallet, setLastConnectedWallet] = useState<WalletInfo | null>(null);
|
||
|
||
const detectWallets = useCallback(() => {
|
||
setDetecting(true);
|
||
setErrorType(null);
|
||
// Wait for wallet extensions to fully inject (up to 1500ms)
|
||
const timer = setTimeout(() => {
|
||
setWallets(buildWallets(showTron));
|
||
setDetecting(false);
|
||
}, 1500);
|
||
return () => clearTimeout(timer);
|
||
}, [showTron]);
|
||
|
||
useEffect(() => {
|
||
const cleanup = detectWallets();
|
||
return cleanup;
|
||
}, [detectWallets]);
|
||
|
||
const handleConnect = async (wallet: WalletInfo) => {
|
||
setConnecting(wallet.id);
|
||
setErrorType(null);
|
||
setLastConnectedWallet(wallet);
|
||
try {
|
||
const result = await wallet.connect();
|
||
if (result) {
|
||
onAddressDetected(result.address, wallet.network, result.rawProvider);
|
||
} else {
|
||
setErrorType("unknown");
|
||
setErrorWalletName(wallet.name);
|
||
}
|
||
} catch (err: unknown) {
|
||
const error = err as Error;
|
||
setErrorWalletName(wallet.name);
|
||
if (error.message === "user_rejected") {
|
||
setErrorType("user_rejected");
|
||
} else if (error.message === "wallet_pending") {
|
||
setErrorType("wallet_pending");
|
||
} else if (error.message?.includes("not initialized") || error.message?.includes("setup")) {
|
||
setErrorType("not_initialized");
|
||
} else {
|
||
setErrorType("unknown");
|
||
}
|
||
} finally {
|
||
setConnecting(null);
|
||
}
|
||
};
|
||
|
||
const handleRetry = useCallback(() => {
|
||
setErrorType(null);
|
||
if (lastConnectedWallet) {
|
||
handleConnect(lastConnectedWallet);
|
||
}
|
||
}, [lastConnectedWallet]); // eslint-disable-line react-hooks/exhaustive-deps
|
||
|
||
const handleManualSubmit = () => {
|
||
const addr = manualAddress.trim();
|
||
if (!addr) {
|
||
setManualError(lang === "zh" ? "请输入钱包地址" : "Please enter wallet address");
|
||
return;
|
||
}
|
||
if (isValidEthAddress(addr)) {
|
||
setManualError(null);
|
||
onAddressDetected(addr, "evm");
|
||
return;
|
||
}
|
||
if (isValidTronAddress(addr)) {
|
||
setManualError(null);
|
||
onAddressDetected(addr, "tron");
|
||
return;
|
||
}
|
||
setManualError(lang === "zh"
|
||
? "地址格式无效,请输入 EVM 地址(0x开头)或 TRON 地址(T开头)"
|
||
: "Invalid address. Enter an EVM address (0x...) or TRON address (T...)");
|
||
};
|
||
|
||
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) {
|
||
return (
|
||
<div className="space-y-3">
|
||
<MobileDeepLinkPanel lang={lang} showTron={showTron} />
|
||
|
||
{/* 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 开头)或 TRON 地址(T 开头)"
|
||
: "Enter EVM address (0x...) or TRON address (T...)"}
|
||
</p>
|
||
<div className="flex gap-2">
|
||
<input
|
||
type="text"
|
||
value={manualAddress}
|
||
onChange={e => { setManualAddress(e.target.value); setManualError(null); }}
|
||
placeholder={lang === "zh" ? "0x... 或 T..." : "0x... or T..."}
|
||
className="flex-1 px-3 py-2 rounded-lg text-xs font-mono text-white/80 outline-none focus:ring-1"
|
||
style={{
|
||
background: "rgba(255,255,255,0.06)",
|
||
border: manualError ? "1px solid rgba(255,80,80,0.5)" : "1px solid rgba(255,255,255,0.12)",
|
||
}}
|
||
onKeyDown={e => e.key === "Enter" && handleManualSubmit()}
|
||
/>
|
||
<button
|
||
onClick={handleManualSubmit}
|
||
className="px-3 py-2 rounded-lg text-xs font-semibold transition-all hover:opacity-90 active:scale-95 whitespace-nowrap"
|
||
style={{ background: "rgba(0,212,255,0.15)", color: "#00d4ff", border: "1px solid rgba(0,212,255,0.3)" }}
|
||
>
|
||
{lang === "zh" ? "确认" : "Confirm"}
|
||
</button>
|
||
</div>
|
||
{manualError && (
|
||
<p className="text-xs text-red-400">{manualError}</p>
|
||
)}
|
||
</div>
|
||
)}
|
||
</div>
|
||
</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 Connect"}
|
||
</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 Connect"}
|
||
</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>
|
||
|
||
{/* Connecting overlay */}
|
||
{connecting && (
|
||
<div
|
||
className="rounded-xl p-4 flex items-center gap-3"
|
||
style={{ background: "rgba(0,212,255,0.06)", border: "1px solid rgba(0,212,255,0.2)" }}
|
||
>
|
||
<svg className="animate-spin w-5 h-5 text-cyan-400 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>
|
||
<div>
|
||
<p className="text-sm font-semibold text-white/80">
|
||
{lang === "zh" ? "等待钱包授权..." : "Waiting for wallet authorization..."}
|
||
</p>
|
||
<p className="text-xs text-white/40 mt-0.5">
|
||
{lang === "zh" ? "请查看钱包弹窗并点击「连接」" : "Please check your wallet popup and click \"Connect\""}
|
||
</p>
|
||
</div>
|
||
</div>
|
||
)}
|
||
|
||
{/* Error panel */}
|
||
{!connecting && errorType && (
|
||
<ErrorHelpPanel
|
||
errorType={errorType}
|
||
walletName={errorWalletName}
|
||
lang={lang}
|
||
onRetry={handleRetry}
|
||
/>
|
||
)}
|
||
|
||
{/* Installed wallets */}
|
||
{!connecting && !errorType && installedWallets.length > 0 && (
|
||
<div className="space-y-2">
|
||
{installedWallets.map(wallet => (
|
||
<button
|
||
key={wallet.id}
|
||
onClick={() => handleConnect(wallet)}
|
||
disabled={!!connecting}
|
||
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: wallet.network === "tron" ? "rgba(255,0,19,0.08)" : "rgba(0,212,255,0.08)",
|
||
border: wallet.network === "tron" ? "1px solid rgba(255,0,19,0.3)" : "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={wallet.network === "tron"
|
||
? { background: "rgba(255,0,19,0.15)", color: "#FF4444" }
|
||
: { background: "rgba(0,212,255,0.15)", color: "#00d4ff" }}
|
||
>
|
||
{lang === "zh" ? "已安装" : "Installed"}
|
||
</span>
|
||
<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>
|
||
)}
|
||
|
||
{/* Show installed wallets even when there's an error (for retry) */}
|
||
{!connecting && errorType && installedWallets.length > 0 && (
|
||
<div className="space-y-2">
|
||
<p className="text-xs text-white/30 text-center">
|
||
{lang === "zh" ? "或选择其他钱包" : "Or try another wallet"}
|
||
</p>
|
||
{installedWallets.map(wallet => (
|
||
<button
|
||
key={wallet.id}
|
||
onClick={() => handleConnect(wallet)}
|
||
disabled={!!connecting}
|
||
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(255,255,255,0.04)",
|
||
border: "1px solid rgba(255,255,255,0.1)",
|
||
}}
|
||
>
|
||
<span className="flex-shrink-0">{wallet.icon}</span>
|
||
<span className="flex-1 text-left text-sm font-semibold text-white/70">{wallet.name}</span>
|
||
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="rgba(255,255,255,0.3)" strokeWidth="2" className="flex-shrink-0">
|
||
<path d="M5 12h14M12 5l7 7-7 7"/>
|
||
</svg>
|
||
</button>
|
||
))}
|
||
</div>
|
||
)}
|
||
|
||
{/* No wallets installed — desktop */}
|
||
{!connecting && 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"
|
||
? (showTron ? "未检测到 EVM 或 TRON 钱包" : "未检测到 EVM 钱包")
|
||
: (showTron ? "No EVM or TRON wallet detected" : "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>
|
||
{!showTron && (
|
||
<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 */}
|
||
{!connecting && notInstalledWallets.length > 0 && (
|
||
<div className="space-y-1.5">
|
||
<p className="text-xs text-white/25 uppercase tracking-wider">
|
||
{lang === "zh" ? "未安装" : "Not installed"}
|
||
</p>
|
||
{notInstalledWallets.map(wallet => (
|
||
<a
|
||
key={wallet.id}
|
||
href={wallet.installUrl}
|
||
target="_blank"
|
||
rel="noopener noreferrer"
|
||
className="w-full flex items-center gap-3 px-4 py-2.5 rounded-xl transition-all hover:opacity-80"
|
||
style={{
|
||
background: "rgba(255,255,255,0.02)",
|
||
border: "1px dashed rgba(255,255,255,0.1)",
|
||
}}
|
||
>
|
||
<span className="flex-shrink-0 opacity-50">{wallet.icon}</span>
|
||
<span className="flex-1 text-left text-sm text-white/40">{wallet.name}</span>
|
||
<span
|
||
className="text-xs px-2 py-0.5 rounded-full font-medium"
|
||
style={{ background: "rgba(255,255,255,0.05)", color: "rgba(255,255,255,0.3)" }}
|
||
>
|
||
{lang === "zh" ? "点击安装" : "Install"}
|
||
</span>
|
||
<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="rgba(255,255,255,0.2)" 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>
|
||
)}
|
||
|
||
{/* Manual address input */}
|
||
<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 开头)或 TRON 地址(T 开头)"
|
||
: "Enter EVM address (0x...) or TRON address (T...)"}
|
||
</p>
|
||
<div className="flex gap-2">
|
||
<input
|
||
type="text"
|
||
value={manualAddress}
|
||
onChange={e => { setManualAddress(e.target.value); setManualError(null); }}
|
||
placeholder={lang === "zh" ? "0x... 或 T..." : "0x... or T..."}
|
||
className="flex-1 px-3 py-2 rounded-lg text-xs font-mono text-white/80 outline-none focus:ring-1"
|
||
style={{
|
||
background: "rgba(255,255,255,0.06)",
|
||
border: manualError ? "1px solid rgba(255,80,80,0.5)" : "1px solid rgba(255,255,255,0.12)",
|
||
}}
|
||
onKeyDown={e => e.key === "Enter" && handleManualSubmit()}
|
||
/>
|
||
<button
|
||
onClick={handleManualSubmit}
|
||
className="px-3 py-2 rounded-lg text-xs font-semibold transition-all hover:opacity-90 active:scale-95 whitespace-nowrap"
|
||
style={{ background: "rgba(0,212,255,0.15)", color: "#00d4ff", border: "1px solid rgba(0,212,255,0.3)" }}
|
||
>
|
||
{lang === "zh" ? "确认" : "Confirm"}
|
||
</button>
|
||
</div>
|
||
{manualError && (
|
||
<p className="text-xs text-red-400">{manualError}</p>
|
||
)}
|
||
</div>
|
||
)}
|
||
</div>
|
||
</div>
|
||
);
|
||
}
|