feat: 添加TronLink支持+手机端DeepLink引导
- WalletSelector v4: 添加TronLink桌面端检测(window.tronLink)和手机端DeepLink
- 手机端显示MetaMask/TrustWallet/OKX/TronLink App内置浏览器打开链接
- TRC20面板集成WalletSelector(showTron=true),统一连接体验
- useWallet改进: connect()返回{success,error},提供中文错误提示
- 导航栏连接钱包按钮: 手机端直接弹出钱包选择器,桌面端先尝试连接再弹窗
修复问题: 手机浏览器无法连接钱包(无扩展插件)
解决方案: DeepLink一键在钱包App内置浏览器中打开预售页面
This commit is contained in:
parent
158822556f
commit
8a77c4ad98
|
|
@ -1,6 +1,6 @@
|
||||||
// NAC XIC Presale — Wallet Selector Component
|
// NAC XIC Presale — Wallet Selector Component
|
||||||
// Detects installed EVM wallets and shows connect/install buttons for each
|
// Detects installed EVM wallets and shows connect/install buttons for each
|
||||||
// v2: improved detection timing, refresh button, manual address fallback
|
// v4: added TronLink support (desktop window.tronLink + mobile DeepLink)
|
||||||
|
|
||||||
import { useState, useEffect, useCallback } from "react";
|
import { useState, useEffect, useCallback } from "react";
|
||||||
|
|
||||||
|
|
@ -11,15 +11,18 @@ interface WalletInfo {
|
||||||
name: string;
|
name: string;
|
||||||
icon: React.ReactNode;
|
icon: React.ReactNode;
|
||||||
installUrl: string;
|
installUrl: string;
|
||||||
|
mobileDeepLink?: string; // DeepLink to open current page in wallet's in-app browser
|
||||||
isInstalled: () => boolean;
|
isInstalled: () => boolean;
|
||||||
connect: () => Promise<string | null>;
|
connect: () => Promise<string | null>;
|
||||||
|
network: "evm" | "tron"; // wallet network type
|
||||||
}
|
}
|
||||||
|
|
||||||
interface WalletSelectorProps {
|
interface WalletSelectorProps {
|
||||||
lang: Lang;
|
lang: Lang;
|
||||||
onAddressDetected: (address: string) => void;
|
onAddressDetected: (address: string, network?: "evm" | "tron") => void;
|
||||||
connectedAddress?: string;
|
connectedAddress?: string;
|
||||||
compact?: boolean; // compact mode for BSC/ETH panel
|
compact?: boolean; // compact mode for BSC/ETH panel
|
||||||
|
showTron?: boolean; // whether to show TRON wallets (for TRC20 panel)
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── Wallet Icons ──────────────────────────────────────────────────────────────
|
// ── Wallet Icons ──────────────────────────────────────────────────────────────
|
||||||
|
|
@ -80,6 +83,46 @@ const BitgetIcon = () => (
|
||||||
</svg>
|
</svg>
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// TronLink Icon — official TRON red color
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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;
|
||||||
|
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 ────────────────────────────────────────────────
|
// ── Provider detection helpers ────────────────────────────────────────────────
|
||||||
|
|
||||||
type EthProvider = {
|
type EthProvider = {
|
||||||
|
|
@ -96,6 +139,15 @@ type EthProvider = {
|
||||||
request: (args: { method: string; params?: unknown[] }) => Promise<unknown>;
|
request: (args: { method: string; params?: unknown[] }) => Promise<unknown>;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
type TronLinkProvider = {
|
||||||
|
ready: boolean;
|
||||||
|
tronWeb?: {
|
||||||
|
defaultAddress?: { base58?: string };
|
||||||
|
trx?: unknown;
|
||||||
|
};
|
||||||
|
request: (args: { method: string; params?: unknown[] }) => Promise<unknown>;
|
||||||
|
};
|
||||||
|
|
||||||
function getEth(): EthProvider | null {
|
function getEth(): EthProvider | null {
|
||||||
if (typeof window === "undefined") return null;
|
if (typeof window === "undefined") return null;
|
||||||
return (window as unknown as { ethereum?: EthProvider }).ethereum ?? null;
|
return (window as unknown as { ethereum?: EthProvider }).ethereum ?? null;
|
||||||
|
|
@ -112,6 +164,22 @@ function getBitget(): EthProvider | null {
|
||||||
return w.bitkeep?.ethereum ?? null;
|
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"] };
|
||||||
|
// TronLink injects window.tronLink; tronWeb is also available when connected
|
||||||
|
if (w.tronLink?.ready) return w.tronLink;
|
||||||
|
// Some versions only inject tronWeb
|
||||||
|
if (w.tronWeb) {
|
||||||
|
return {
|
||||||
|
ready: true,
|
||||||
|
tronWeb: w.tronWeb,
|
||||||
|
request: async () => null,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
// Find a specific provider from the providers array or direct injection
|
// Find a specific provider from the providers array or direct injection
|
||||||
function findProvider(predicate: (p: EthProvider) => boolean): EthProvider | null {
|
function findProvider(predicate: (p: EthProvider) => boolean): EthProvider | null {
|
||||||
const eth = getEth();
|
const eth = getEth();
|
||||||
|
|
@ -128,61 +196,79 @@ async function requestAccounts(provider: EthProvider): Promise<string | null> {
|
||||||
return accounts?.[0] ?? null;
|
return accounts?.[0] ?? null;
|
||||||
} catch (err: unknown) {
|
} catch (err: unknown) {
|
||||||
const error = err as { code?: number; message?: string };
|
const error = err as { code?: number; message?: string };
|
||||||
// User rejected
|
|
||||||
if (error?.code === 4001) throw new Error("user_rejected");
|
if (error?.code === 4001) throw new Error("user_rejected");
|
||||||
// MetaMask not initialized / locked
|
|
||||||
if (error?.code === -32002) throw new Error("wallet_pending");
|
if (error?.code === -32002) throw new Error("wallet_pending");
|
||||||
throw err;
|
throw err;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check if MetaMask is installed but not yet initialized (no accounts, no unlock)
|
async function requestTronAccounts(provider: TronLinkProvider): Promise<string | null> {
|
||||||
async function isWalletInitialized(provider: EthProvider): Promise<boolean> {
|
|
||||||
try {
|
try {
|
||||||
const accounts = await provider.request({ method: "eth_accounts" }) as string[];
|
// TronLink v1: use tronWeb.defaultAddress
|
||||||
// If we can get accounts (even empty array), wallet is initialized
|
if (provider.tronWeb?.defaultAddress?.base58) {
|
||||||
return true;
|
return provider.tronWeb.defaultAddress.base58;
|
||||||
} catch {
|
}
|
||||||
return false;
|
// TronLink v2+: use request method
|
||||||
|
const result = await provider.request({ method: "tron_requestAccounts" }) as { code?: number; message?: string };
|
||||||
|
if (result?.code === 200) {
|
||||||
|
// After approval, tronWeb.defaultAddress should be populated
|
||||||
|
const w = window as unknown as { tronWeb?: { defaultAddress?: { base58?: string } } };
|
||||||
|
return w.tronWeb?.defaultAddress?.base58 ?? null;
|
||||||
|
}
|
||||||
|
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 ────────────────────────────────────────────────────────
|
// ── Wallet definitions ────────────────────────────────────────────────────────
|
||||||
|
|
||||||
function buildWallets(): WalletInfo[] {
|
function buildWallets(showTron: boolean): WalletInfo[] {
|
||||||
return [
|
const currentUrl = typeof window !== "undefined" ? window.location.href : "https://pre-sale.newassetchain.io";
|
||||||
|
const urlWithoutProtocol = currentUrl.replace(/^https?:\/\//, "");
|
||||||
|
|
||||||
|
const evmWallets: WalletInfo[] = [
|
||||||
{
|
{
|
||||||
id: "metamask",
|
id: "metamask",
|
||||||
name: "MetaMask",
|
name: "MetaMask",
|
||||||
icon: <MetaMaskIcon />,
|
icon: <MetaMaskIcon />,
|
||||||
installUrl: "https://metamask.io/download/",
|
installUrl: "https://metamask.io/download/",
|
||||||
|
mobileDeepLink: `https://metamask.app.link/dapp/${urlWithoutProtocol}`,
|
||||||
isInstalled: () => !!findProvider(p => !!p.isMetaMask),
|
isInstalled: () => !!findProvider(p => !!p.isMetaMask),
|
||||||
connect: async () => {
|
connect: async () => {
|
||||||
const p = findProvider(p => !!p.isMetaMask) ?? getEth();
|
const p = findProvider(p => !!p.isMetaMask) ?? getEth();
|
||||||
return p ? requestAccounts(p) : null;
|
return p ? requestAccounts(p) : null;
|
||||||
},
|
},
|
||||||
|
network: "evm",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: "trust",
|
id: "trust",
|
||||||
name: "Trust Wallet",
|
name: "Trust Wallet",
|
||||||
icon: <TrustWalletIcon />,
|
icon: <TrustWalletIcon />,
|
||||||
installUrl: "https://trustwallet.com/download",
|
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)),
|
isInstalled: () => !!findProvider(p => !!(p.isTrust || p.isTrustWallet)),
|
||||||
connect: async () => {
|
connect: async () => {
|
||||||
const p = findProvider(p => !!(p.isTrust || p.isTrustWallet)) ?? getEth();
|
const p = findProvider(p => !!(p.isTrust || p.isTrustWallet)) ?? getEth();
|
||||||
return p ? requestAccounts(p) : null;
|
return p ? requestAccounts(p) : null;
|
||||||
},
|
},
|
||||||
|
network: "evm",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: "okx",
|
id: "okx",
|
||||||
name: "OKX Wallet",
|
name: "OKX Wallet",
|
||||||
icon: <OKXIcon />,
|
icon: <OKXIcon />,
|
||||||
installUrl: "https://www.okx.com/web3",
|
installUrl: "https://www.okx.com/web3",
|
||||||
|
mobileDeepLink: `okx://wallet/dapp/url?dappUrl=${encodeURIComponent(currentUrl)}`,
|
||||||
isInstalled: () => !!(getOKX() || findProvider(p => !!(p.isOKExWallet || p.isOkxWallet))),
|
isInstalled: () => !!(getOKX() || findProvider(p => !!(p.isOKExWallet || p.isOkxWallet))),
|
||||||
connect: async () => {
|
connect: async () => {
|
||||||
const p = getOKX() ?? findProvider(p => !!(p.isOKExWallet || p.isOkxWallet));
|
const p = getOKX() ?? findProvider(p => !!(p.isOKExWallet || p.isOkxWallet));
|
||||||
return p ? requestAccounts(p) : null;
|
return p ? requestAccounts(p) : null;
|
||||||
},
|
},
|
||||||
|
network: "evm",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: "coinbase",
|
id: "coinbase",
|
||||||
|
|
@ -194,6 +280,7 @@ function buildWallets(): WalletInfo[] {
|
||||||
const p = findProvider(p => !!p.isCoinbaseWallet) ?? getEth();
|
const p = findProvider(p => !!p.isCoinbaseWallet) ?? getEth();
|
||||||
return p ? requestAccounts(p) : null;
|
return p ? requestAccounts(p) : null;
|
||||||
},
|
},
|
||||||
|
network: "evm",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: "tokenpocket",
|
id: "tokenpocket",
|
||||||
|
|
@ -205,6 +292,7 @@ function buildWallets(): WalletInfo[] {
|
||||||
const p = findProvider(p => !!p.isTokenPocket) ?? getEth();
|
const p = findProvider(p => !!p.isTokenPocket) ?? getEth();
|
||||||
return p ? requestAccounts(p) : null;
|
return p ? requestAccounts(p) : null;
|
||||||
},
|
},
|
||||||
|
network: "evm",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: "bitget",
|
id: "bitget",
|
||||||
|
|
@ -216,8 +304,29 @@ function buildWallets(): WalletInfo[] {
|
||||||
const p = getBitget() ?? findProvider(p => !!(p.isBitkeep || p.isBitgetWallet));
|
const p = getBitget() ?? findProvider(p => !!(p.isBitkeep || p.isBitgetWallet));
|
||||||
return p ? requestAccounts(p) : null;
|
return p ? requestAccounts(p) : null;
|
||||||
},
|
},
|
||||||
|
network: "evm",
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
|
const tronWallets: WalletInfo[] = [
|
||||||
|
{
|
||||||
|
id: "tronlink",
|
||||||
|
name: "TronLink",
|
||||||
|
icon: <TronLinkIcon />,
|
||||||
|
installUrl: "https://www.tronlink.org/",
|
||||||
|
// TronLink mobile DeepLink — opens current URL in TronLink's built-in browser
|
||||||
|
mobileDeepLink: `tronlinkoutside://pull?param=${encodeURIComponent(JSON.stringify({ url: currentUrl, action: "open", protocol: "tronlink" }))}`,
|
||||||
|
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
|
// Validate Ethereum address format
|
||||||
|
|
@ -225,9 +334,157 @@ function isValidEthAddress(addr: string): boolean {
|
||||||
return /^0x[0-9a-fA-F]{40}$/.test(addr);
|
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);
|
||||||
|
}
|
||||||
|
|
||||||
|
function isValidAddress(addr: string): boolean {
|
||||||
|
return isValidEthAddress(addr) || isValidTronAddress(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 tronWallets = [
|
||||||
|
{
|
||||||
|
id: "tronlink",
|
||||||
|
name: "TronLink",
|
||||||
|
icon: <TronLinkIcon />,
|
||||||
|
deepLink: `tronlinkoutside://pull?param=${encodeURIComponent(JSON.stringify({ url: currentUrl, action: "open", protocol: "tronlink" }))}`,
|
||||||
|
installUrl: "https://www.tronlink.org/",
|
||||||
|
badge: "TRON",
|
||||||
|
badgeColor: "#FF0013",
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
const evmWallets = [
|
||||||
|
{
|
||||||
|
id: "metamask",
|
||||||
|
name: "MetaMask",
|
||||||
|
icon: <MetaMaskIcon />,
|
||||||
|
deepLink: `https://metamask.app.link/dapp/${urlWithoutProtocol}`,
|
||||||
|
installUrl: "https://metamask.io/download/",
|
||||||
|
badge: "EVM",
|
||||||
|
badgeColor: "#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",
|
||||||
|
badge: "EVM",
|
||||||
|
badgeColor: "#3375BB",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "okx",
|
||||||
|
name: "OKX Wallet",
|
||||||
|
icon: <OKXIcon />,
|
||||||
|
deepLink: `okx://wallet/dapp/url?dappUrl=${encodeURIComponent(currentUrl)}`,
|
||||||
|
installUrl: "https://www.okx.com/web3",
|
||||||
|
badge: "EVM",
|
||||||
|
badgeColor: "#00F0FF",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "tokenpocket",
|
||||||
|
name: "TokenPocket",
|
||||||
|
icon: <TokenPocketIcon />,
|
||||||
|
deepLink: `tpoutside://pull?param=${encodeURIComponent(JSON.stringify({ url: currentUrl }))}`,
|
||||||
|
installUrl: "https://www.tokenpocket.pro/en/download/app",
|
||||||
|
badge: "EVM/TRON",
|
||||||
|
badgeColor: "#2980FE",
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
const walletList = showTron ? [...tronWallets, ...evmWallets] : evmWallets;
|
||||||
|
|
||||||
|
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>
|
||||||
|
{walletList.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: `${wallet.badgeColor}22`, color: wallet.badgeColor, border: `1px solid ${wallet.badgeColor}44` }}
|
||||||
|
>
|
||||||
|
{wallet.badge}
|
||||||
|
</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 ──────────────────────────────────────────────────
|
// ── WalletSelector Component ──────────────────────────────────────────────────
|
||||||
|
|
||||||
export function WalletSelector({ lang, onAddressDetected, connectedAddress, compact = false }: WalletSelectorProps) {
|
export function WalletSelector({ lang, onAddressDetected, connectedAddress, compact = false, showTron = false }: WalletSelectorProps) {
|
||||||
const [wallets, setWallets] = useState<WalletInfo[]>([]);
|
const [wallets, setWallets] = useState<WalletInfo[]>([]);
|
||||||
const [connecting, setConnecting] = useState<string | null>(null);
|
const [connecting, setConnecting] = useState<string | null>(null);
|
||||||
const [error, setError] = useState<string | null>(null);
|
const [error, setError] = useState<string | null>(null);
|
||||||
|
|
@ -235,17 +492,19 @@ export function WalletSelector({ lang, onAddressDetected, connectedAddress, comp
|
||||||
const [showManual, setShowManual] = useState(false);
|
const [showManual, setShowManual] = useState(false);
|
||||||
const [manualAddress, setManualAddress] = useState("");
|
const [manualAddress, setManualAddress] = useState("");
|
||||||
const [manualError, setManualError] = useState<string | null>(null);
|
const [manualError, setManualError] = useState<string | null>(null);
|
||||||
|
const [isMobile] = useState(() => isMobileBrowser());
|
||||||
|
const [inWalletBrowser] = useState(() => isInWalletBrowser());
|
||||||
|
|
||||||
const detectWallets = useCallback(() => {
|
const detectWallets = useCallback(() => {
|
||||||
setDetecting(true);
|
setDetecting(true);
|
||||||
setError(null);
|
setError(null);
|
||||||
// Wait for wallet extensions to fully inject (up to 1500ms)
|
// Wait for wallet extensions to fully inject (up to 1500ms)
|
||||||
const timer = setTimeout(() => {
|
const timer = setTimeout(() => {
|
||||||
setWallets(buildWallets());
|
setWallets(buildWallets(showTron));
|
||||||
setDetecting(false);
|
setDetecting(false);
|
||||||
}, 1500);
|
}, 1500);
|
||||||
return () => clearTimeout(timer);
|
return () => clearTimeout(timer);
|
||||||
}, []);
|
}, [showTron]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const cleanup = detectWallets();
|
const cleanup = detectWallets();
|
||||||
|
|
@ -258,7 +517,7 @@ export function WalletSelector({ lang, onAddressDetected, connectedAddress, comp
|
||||||
try {
|
try {
|
||||||
const address = await wallet.connect();
|
const address = await wallet.connect();
|
||||||
if (address) {
|
if (address) {
|
||||||
onAddressDetected(address);
|
onAddressDetected(address, wallet.network);
|
||||||
} else {
|
} else {
|
||||||
setError(lang === "zh" ? "未获取到地址,请重试" : "No address returned, please try again");
|
setError(lang === "zh" ? "未获取到地址,请重试" : "No address returned, please try again");
|
||||||
}
|
}
|
||||||
|
|
@ -269,8 +528,8 @@ export function WalletSelector({ lang, onAddressDetected, connectedAddress, comp
|
||||||
} else if (error.message === "wallet_pending") {
|
} else if (error.message === "wallet_pending") {
|
||||||
setError(lang === "zh" ? "钱包请求处理中,请检查钱包弹窗" : "Wallet request pending, please check your wallet popup");
|
setError(lang === "zh" ? "钱包请求处理中,请检查钱包弹窗" : "Wallet request pending, please check your wallet popup");
|
||||||
} else if (error.message?.includes("not initialized") || error.message?.includes("setup")) {
|
} else if (error.message?.includes("not initialized") || error.message?.includes("setup")) {
|
||||||
setError(lang === "zh"
|
setError(lang === "zh"
|
||||||
? "请先完成钱包初始化设置,然后刷新页面重试"
|
? "请先完成钱包初始化设置,然后刷新页面重试"
|
||||||
: "Please complete wallet setup first, then refresh the page");
|
: "Please complete wallet setup first, then refresh the page");
|
||||||
} else {
|
} else {
|
||||||
setError(lang === "zh" ? "连接失败,请重试" : "Connection failed, please try again");
|
setError(lang === "zh" ? "连接失败,请重试" : "Connection failed, please try again");
|
||||||
|
|
@ -286,12 +545,19 @@ export function WalletSelector({ lang, onAddressDetected, connectedAddress, comp
|
||||||
setManualError(lang === "zh" ? "请输入钱包地址" : "Please enter wallet address");
|
setManualError(lang === "zh" ? "请输入钱包地址" : "Please enter wallet address");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (!isValidEthAddress(addr)) {
|
if (isValidEthAddress(addr)) {
|
||||||
setManualError(lang === "zh" ? "地址格式无效,请输入正确的以太坊地址(0x开头,42位)" : "Invalid address format. Must be 0x followed by 40 hex characters");
|
setManualError(null);
|
||||||
|
onAddressDetected(addr, "evm");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
setManualError(null);
|
if (isValidTronAddress(addr)) {
|
||||||
onAddressDetected(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 installedWallets = wallets.filter(w => w.isInstalled());
|
||||||
|
|
@ -315,6 +581,91 @@ export function WalletSelector({ lang, onAddressDetected, connectedAddress, comp
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ── 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} 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 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 (
|
return (
|
||||||
<div className="space-y-3">
|
<div className="space-y-3">
|
||||||
<div className="flex items-center justify-between">
|
<div className="flex items-center justify-between">
|
||||||
|
|
@ -336,27 +687,14 @@ export function WalletSelector({ lang, onAddressDetected, connectedAddress, comp
|
||||||
<path d="M23 4v6h-6M1 20v-6h6"/>
|
<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"/>
|
<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>
|
</svg>
|
||||||
{detecting
|
{detecting
|
||||||
? (lang === "zh" ? "检测中..." : "Detecting...")
|
? (lang === "zh" ? "检测中..." : "Detecting...")
|
||||||
: (lang === "zh" ? "刷新" : "Refresh")}
|
: (lang === "zh" ? "刷新" : "Refresh")}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Loading state */}
|
|
||||||
{detecting && (
|
|
||||||
<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>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{/* Installed wallets */}
|
{/* Installed wallets */}
|
||||||
{!detecting && installedWallets.length > 0 && (
|
{installedWallets.length > 0 && (
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
{installedWallets.map(wallet => (
|
{installedWallets.map(wallet => (
|
||||||
<button
|
<button
|
||||||
|
|
@ -365,15 +703,17 @@ export function WalletSelector({ lang, onAddressDetected, connectedAddress, comp
|
||||||
disabled={connecting === wallet.id}
|
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]"
|
className="w-full flex items-center gap-3 px-4 py-3 rounded-xl transition-all hover:opacity-90 active:scale-[0.98]"
|
||||||
style={{
|
style={{
|
||||||
background: "rgba(0,212,255,0.08)",
|
background: wallet.network === "tron" ? "rgba(255,0,19,0.08)" : "rgba(0,212,255,0.08)",
|
||||||
border: "1px solid rgba(0,212,255,0.3)",
|
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-shrink-0">{wallet.icon}</span>
|
||||||
<span className="flex-1 text-left text-sm font-semibold text-white">{wallet.name}</span>
|
<span className="flex-1 text-left text-sm font-semibold text-white">{wallet.name}</span>
|
||||||
<span
|
<span
|
||||||
className="text-xs px-2 py-0.5 rounded-full font-medium"
|
className="text-xs px-2 py-0.5 rounded-full font-medium"
|
||||||
style={{ background: "rgba(0,212,255,0.15)", color: "#00d4ff" }}
|
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"}
|
{lang === "zh" ? "已安装" : "Installed"}
|
||||||
</span>
|
</span>
|
||||||
|
|
@ -392,30 +732,41 @@ export function WalletSelector({ lang, onAddressDetected, connectedAddress, comp
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* No wallets installed */}
|
{/* No wallets installed — desktop */}
|
||||||
{!detecting && installedWallets.length === 0 && (
|
{installedWallets.length === 0 && (
|
||||||
<div
|
<div
|
||||||
className="rounded-xl p-4 text-center"
|
className="rounded-xl p-4 text-center"
|
||||||
style={{ background: "rgba(255,255,255,0.04)", border: "1px dashed rgba(255,255,255,0.15)" }}
|
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">
|
<p className="text-sm text-white/50 mb-1">
|
||||||
{lang === "zh" ? "未检测到 EVM 钱包" : "No EVM wallet detected"}
|
{lang === "zh"
|
||||||
|
? (showTron ? "未检测到 EVM 或 TRON 钱包" : "未检测到 EVM 钱包")
|
||||||
|
: (showTron ? "No EVM or TRON wallet detected" : "No EVM wallet detected")}
|
||||||
</p>
|
</p>
|
||||||
<p className="text-xs text-white/30 mb-3">
|
<p className="text-xs text-white/30 mb-3">
|
||||||
{lang === "zh"
|
{lang === "zh"
|
||||||
? "请安装以下任一钱包,完成设置后点击上方「刷新」按钮"
|
? "请安装以下任一钱包,完成设置后点击上方「刷新」按钮"
|
||||||
: "Install any wallet below, then click Refresh above after setup"}
|
: "Install any wallet below, then click Refresh above after setup"}
|
||||||
</p>
|
</p>
|
||||||
<p className="text-xs text-amber-400/70">
|
{showTron && (
|
||||||
{lang === "zh"
|
<p className="text-xs text-amber-400/70">
|
||||||
? "💡 已安装MetaMask?请先完成钱包初始化(创建或导入钱包),再点击刷新"
|
{lang === "zh"
|
||||||
: "💡 Have MetaMask? Complete wallet setup (create or import) first, then click Refresh"}
|
? "💡 TRC20 支付请安装 TronLink;BSC/ETH 支付请安装 MetaMask"
|
||||||
</p>
|
: "💡 For TRC20 install TronLink; for BSC/ETH install MetaMask"}
|
||||||
|
</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>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Not-installed wallets — show install links */}
|
{/* Not-installed wallets — show install links */}
|
||||||
{!detecting && !compact && notInstalledWallets.length > 0 && (
|
{!compact && notInstalledWallets.length > 0 && (
|
||||||
<div className="space-y-1">
|
<div className="space-y-1">
|
||||||
<p className="text-xs text-white/30 mt-2">
|
<p className="text-xs text-white/30 mt-2">
|
||||||
{lang === "zh" ? "未安装(点击安装)" : "Not installed (click to install)"}
|
{lang === "zh" ? "未安装(点击安装)" : "Not installed (click to install)"}
|
||||||
|
|
@ -442,7 +793,7 @@ export function WalletSelector({ lang, onAddressDetected, connectedAddress, comp
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* In compact mode, show install links inline */}
|
{/* In compact mode, show install links inline */}
|
||||||
{!detecting && compact && notInstalledWallets.length > 0 && installedWallets.length === 0 && (
|
{compact && notInstalledWallets.length > 0 && installedWallets.length === 0 && (
|
||||||
<div className="flex flex-wrap gap-2">
|
<div className="flex flex-wrap gap-2">
|
||||||
{notInstalledWallets.slice(0, 4).map(wallet => (
|
{notInstalledWallets.slice(0, 4).map(wallet => (
|
||||||
<a
|
<a
|
||||||
|
|
@ -478,7 +829,7 @@ export function WalletSelector({ lang, onAddressDetected, connectedAddress, comp
|
||||||
<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="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"/>
|
<path d="M18.5 2.5a2.121 2.121 0 0 1 3 3L12 15l-4 1 1-4 9.5-9.5z"/>
|
||||||
</svg>
|
</svg>
|
||||||
{showManual
|
{showManual
|
||||||
? (lang === "zh" ? "收起手动输入" : "Hide manual input")
|
? (lang === "zh" ? "收起手动输入" : "Hide manual input")
|
||||||
: (lang === "zh" ? "手动输入钱包地址" : "Enter address manually")}
|
: (lang === "zh" ? "手动输入钱包地址" : "Enter address manually")}
|
||||||
</button>
|
</button>
|
||||||
|
|
@ -486,16 +837,16 @@ export function WalletSelector({ lang, onAddressDetected, connectedAddress, comp
|
||||||
{showManual && (
|
{showManual && (
|
||||||
<div className="mt-2 space-y-2">
|
<div className="mt-2 space-y-2">
|
||||||
<p className="text-xs text-white/40 text-center">
|
<p className="text-xs text-white/40 text-center">
|
||||||
{lang === "zh"
|
{lang === "zh"
|
||||||
? "直接输入您的 EVM 钱包地址(0x 开头)"
|
? "输入 EVM 地址(0x 开头)或 TRON 地址(T 开头)"
|
||||||
: "Enter your EVM wallet address (starts with 0x)"}
|
: "Enter EVM address (0x...) or TRON address (T...)"}
|
||||||
</p>
|
</p>
|
||||||
<div className="flex gap-2">
|
<div className="flex gap-2">
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
value={manualAddress}
|
value={manualAddress}
|
||||||
onChange={e => { setManualAddress(e.target.value); setManualError(null); }}
|
onChange={e => { setManualAddress(e.target.value); setManualError(null); }}
|
||||||
placeholder={lang === "zh" ? "0x..." : "0x..."}
|
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"
|
className="flex-1 px-3 py-2 rounded-lg text-xs font-mono text-white/80 outline-none focus:ring-1"
|
||||||
style={{
|
style={{
|
||||||
background: "rgba(255,255,255,0.06)",
|
background: "rgba(255,255,255,0.06)",
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
// NAC XIC Presale — Wallet Connection Hook
|
// NAC XIC Presale — Wallet Connection Hook
|
||||||
// Supports MetaMask, Trust Wallet, OKX Wallet, Coinbase Wallet, and all EVM-compatible wallets
|
// Supports MetaMask, Trust Wallet, OKX Wallet, Coinbase Wallet, and all EVM-compatible wallets
|
||||||
// Robust auto-detect with retry, multi-provider support, and graceful fallback
|
// v3: improved error handling, MetaMask initialization detection, toast notifications
|
||||||
|
|
||||||
import { useState, useEffect, useCallback, useRef } from "react";
|
import { useState, useEffect, useCallback, useRef } from "react";
|
||||||
import { BrowserProvider, JsonRpcSigner, Eip1193Provider } from "ethers";
|
import { BrowserProvider, JsonRpcSigner, Eip1193Provider } from "ethers";
|
||||||
|
|
@ -31,12 +31,17 @@ const INITIAL_STATE: WalletState = {
|
||||||
};
|
};
|
||||||
|
|
||||||
// Detect the best available EVM provider across all major wallets
|
// Detect the best available EVM provider across all major wallets
|
||||||
function detectProvider(): Eip1193Provider | null {
|
export function detectProvider(): Eip1193Provider | null {
|
||||||
if (typeof window === "undefined") return 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 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;
|
const eth = w.ethereum as (Eip1193Provider & {
|
||||||
|
providers?: Eip1193Provider[];
|
||||||
|
isMetaMask?: boolean;
|
||||||
|
isTrust?: boolean;
|
||||||
|
isOKExWallet?: boolean;
|
||||||
|
isCoinbaseWallet?: boolean;
|
||||||
|
}) | undefined;
|
||||||
|
|
||||||
if (!eth) {
|
if (!eth) {
|
||||||
// Fallback: check wallet-specific globals
|
// Fallback: check wallet-specific globals
|
||||||
|
|
@ -47,7 +52,6 @@ function detectProvider(): Eip1193Provider | null {
|
||||||
|
|
||||||
// If multiple providers are injected (common when multiple extensions installed)
|
// If multiple providers are injected (common when multiple extensions installed)
|
||||||
if (eth.providers && Array.isArray(eth.providers) && eth.providers.length > 0) {
|
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);
|
const metamask = eth.providers.find((p: Eip1193Provider & { isMetaMask?: boolean }) => p.isMetaMask);
|
||||||
return metamask ?? eth.providers[0];
|
return metamask ?? eth.providers[0];
|
||||||
}
|
}
|
||||||
|
|
@ -55,6 +59,26 @@ function detectProvider(): Eip1193Provider | null {
|
||||||
return eth;
|
return eth;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Check if MetaMask is installed but not yet initialized (no wallet created/imported)
|
||||||
|
export async function checkWalletReady(rawProvider: Eip1193Provider): Promise<{ ready: boolean; reason?: string }> {
|
||||||
|
try {
|
||||||
|
// eth_accounts is silent — if it returns empty array, wallet is installed but locked or not initialized
|
||||||
|
const accounts = await (rawProvider as { request: (args: { method: string }) => Promise<string[]> }).request({
|
||||||
|
method: "eth_accounts",
|
||||||
|
});
|
||||||
|
// If we get here, the wallet is at least initialized (even if locked / no accounts)
|
||||||
|
return { ready: true };
|
||||||
|
} catch (err: unknown) {
|
||||||
|
const error = err as { code?: number; message?: string };
|
||||||
|
// -32002: Request already pending (MetaMask not initialized or another request pending)
|
||||||
|
if (error?.code === -32002) {
|
||||||
|
return { ready: false, reason: "pending" };
|
||||||
|
}
|
||||||
|
// Any other error — treat as not ready
|
||||||
|
return { ready: false, reason: error?.message || "unknown" };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Build wallet state from a provider and accounts
|
// Build wallet state from a provider and accounts
|
||||||
async function buildWalletState(
|
async function buildWalletState(
|
||||||
rawProvider: Eip1193Provider,
|
rawProvider: Eip1193Provider,
|
||||||
|
|
@ -68,7 +92,6 @@ async function buildWalletState(
|
||||||
const network = await provider.getNetwork();
|
const network = await provider.getNetwork();
|
||||||
chainId = Number(network.chainId);
|
chainId = Number(network.chainId);
|
||||||
} catch {
|
} catch {
|
||||||
// Some wallets don't support getNetwork immediately — try eth_chainId directly
|
|
||||||
try {
|
try {
|
||||||
const chainHex = await (rawProvider as { request: (args: { method: string }) => Promise<string> }).request({ method: "eth_chainId" });
|
const chainHex = await (rawProvider as { request: (args: { method: string }) => Promise<string> }).request({ method: "eth_chainId" });
|
||||||
chainId = parseInt(chainHex, 16);
|
chainId = parseInt(chainHex, 16);
|
||||||
|
|
@ -80,7 +103,6 @@ async function buildWalletState(
|
||||||
try {
|
try {
|
||||||
signer = await provider.getSigner();
|
signer = await provider.getSigner();
|
||||||
} catch {
|
} catch {
|
||||||
// getSigner may fail on some wallets before full connection — that's OK
|
|
||||||
signer = null;
|
signer = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -110,25 +132,58 @@ export function useWallet() {
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
// ── Connect (explicit user action) ─────────────────────────────────────────
|
// ── Connect (explicit user action) ─────────────────────────────────────────
|
||||||
const connect = useCallback(async () => {
|
const connect = useCallback(async (): Promise<{ success: boolean; error?: string }> => {
|
||||||
const rawProvider = detectProvider();
|
const rawProvider = detectProvider();
|
||||||
|
|
||||||
if (!rawProvider) {
|
if (!rawProvider) {
|
||||||
setState(s => ({ ...s, error: "请安装 MetaMask 或其他 EVM 兼容钱包 / Please install MetaMask or a compatible wallet." }));
|
const msg = "未检测到钱包插件。请安装 MetaMask 或其他 EVM 兼容钱包后刷新页面。";
|
||||||
return;
|
if (mountedRef.current) setState(s => ({ ...s, error: msg }));
|
||||||
|
return { success: false, error: msg };
|
||||||
}
|
}
|
||||||
|
|
||||||
setState(s => ({ ...s, isConnecting: true, error: null }));
|
setState(s => ({ ...s, isConnecting: true, error: null }));
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// Request accounts — this triggers the wallet popup
|
// Request accounts — this triggers the wallet popup
|
||||||
const accounts = await (rawProvider as { request: (args: { method: string; params?: unknown[] }) => Promise<string[]> }).request({
|
const accounts = await (rawProvider as {
|
||||||
|
request: (args: { method: string; params?: unknown[] }) => Promise<string[]>
|
||||||
|
}).request({
|
||||||
method: "eth_requestAccounts",
|
method: "eth_requestAccounts",
|
||||||
params: [],
|
params: [],
|
||||||
});
|
});
|
||||||
if (!accounts || accounts.length === 0) throw new Error("No accounts returned");
|
|
||||||
|
if (!accounts || accounts.length === 0) {
|
||||||
|
throw new Error("no_accounts");
|
||||||
|
}
|
||||||
|
|
||||||
const partial = await buildWalletState(rawProvider, accounts[0]);
|
const partial = await buildWalletState(rawProvider, accounts[0]);
|
||||||
if (mountedRef.current) setState({ ...INITIAL_STATE, ...partial });
|
if (mountedRef.current) setState({ ...INITIAL_STATE, ...partial });
|
||||||
|
return { success: true };
|
||||||
|
|
||||||
} catch (err: unknown) {
|
} catch (err: unknown) {
|
||||||
const msg = (err as Error).message || "Failed to connect wallet";
|
const error = err as { code?: number; message?: string };
|
||||||
|
let msg: string;
|
||||||
|
|
||||||
|
if (error?.code === 4001) {
|
||||||
|
// User rejected
|
||||||
|
msg = "已取消连接 / Connection cancelled";
|
||||||
|
} else if (error?.code === -32002) {
|
||||||
|
// MetaMask has a pending request — usually means it's not initialized or popup is already open
|
||||||
|
msg = "钱包请求处理中,请检查 MetaMask 弹窗。如未弹出,请先完成 MetaMask 初始化设置(创建或导入钱包),然后刷新页面重试。";
|
||||||
|
} else if (error?.message === "no_accounts") {
|
||||||
|
msg = "未获取到账户,请确认钱包已解锁并授权此网站。";
|
||||||
|
} else if (
|
||||||
|
error?.message?.toLowerCase().includes("not initialized") ||
|
||||||
|
error?.message?.toLowerCase().includes("setup") ||
|
||||||
|
error?.message?.toLowerCase().includes("onboarding")
|
||||||
|
) {
|
||||||
|
msg = "MetaMask 尚未完成初始化。请先打开 MetaMask 扩展,创建或导入钱包,然后刷新页面重试。";
|
||||||
|
} else {
|
||||||
|
msg = `连接失败: ${error?.message || "未知错误"}。请刷新页面重试。`;
|
||||||
|
}
|
||||||
|
|
||||||
if (mountedRef.current) setState(s => ({ ...s, isConnecting: false, error: msg }));
|
if (mountedRef.current) setState(s => ({ ...s, isConnecting: false, error: msg }));
|
||||||
|
return { success: false, error: msg };
|
||||||
}
|
}
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
|
@ -171,7 +226,6 @@ export function useWallet() {
|
||||||
|
|
||||||
const rawProvider = detectProvider();
|
const rawProvider = detectProvider();
|
||||||
if (!rawProvider) {
|
if (!rawProvider) {
|
||||||
// Wallet extension may not be injected yet — retry up to 3 times
|
|
||||||
if (attempt < 3) {
|
if (attempt < 3) {
|
||||||
retryRef.current = setTimeout(() => tryAutoDetect(attempt + 1), 800 * attempt);
|
retryRef.current = setTimeout(() => tryAutoDetect(attempt + 1), 800 * attempt);
|
||||||
}
|
}
|
||||||
|
|
@ -189,7 +243,6 @@ export function useWallet() {
|
||||||
setState({ ...INITIAL_STATE, ...partial });
|
setState({ ...INITIAL_STATE, ...partial });
|
||||||
}
|
}
|
||||||
} else if (attempt < 3) {
|
} else if (attempt < 3) {
|
||||||
// Accounts empty — wallet might not have finished loading, retry
|
|
||||||
retryRef.current = setTimeout(() => tryAutoDetect(attempt + 1), 1000 * attempt);
|
retryRef.current = setTimeout(() => tryAutoDetect(attempt + 1), 1000 * attempt);
|
||||||
}
|
}
|
||||||
} catch {
|
} catch {
|
||||||
|
|
@ -197,7 +250,6 @@ export function useWallet() {
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// Small initial delay to let wallet extensions inject themselves
|
|
||||||
retryRef.current = setTimeout(() => tryAutoDetect(1), 300);
|
retryRef.current = setTimeout(() => tryAutoDetect(1), 300);
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
|
|
@ -223,7 +275,6 @@ export function useWallet() {
|
||||||
if (!accs || accs.length === 0) {
|
if (!accs || accs.length === 0) {
|
||||||
setState(INITIAL_STATE);
|
setState(INITIAL_STATE);
|
||||||
} else {
|
} else {
|
||||||
// Re-build full state with new address
|
|
||||||
try {
|
try {
|
||||||
const partial = await buildWalletState(rawProvider, accs[0]);
|
const partial = await buildWalletState(rawProvider, accs[0]);
|
||||||
if (mountedRef.current) setState({ ...INITIAL_STATE, ...partial });
|
if (mountedRef.current) setState({ ...INITIAL_STATE, ...partial });
|
||||||
|
|
@ -242,7 +293,6 @@ export function useWallet() {
|
||||||
|
|
||||||
const handleChainChanged = async () => {
|
const handleChainChanged = async () => {
|
||||||
if (!mountedRef.current) return;
|
if (!mountedRef.current) return;
|
||||||
// Re-fetch network info instead of reloading the page
|
|
||||||
try {
|
try {
|
||||||
const provider = new BrowserProvider(rawProvider);
|
const provider = new BrowserProvider(rawProvider);
|
||||||
const network = await provider.getNetwork();
|
const network = await provider.getNetwork();
|
||||||
|
|
@ -257,7 +307,6 @@ export function useWallet() {
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
} catch {
|
} catch {
|
||||||
// If we can't get network info, reload as last resort
|
|
||||||
window.location.reload();
|
window.location.reload();
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -112,48 +112,8 @@ function TRC20Panel({ usdtAmount, lang, connectedAddress, onConnectWallet }: { u
|
||||||
const [evmAddress, setEvmAddress] = useState(connectedAddress || "");
|
const [evmAddress, setEvmAddress] = useState(connectedAddress || "");
|
||||||
const [evmAddrError, setEvmAddrError] = useState("");
|
const [evmAddrError, setEvmAddrError] = useState("");
|
||||||
const [submitted, setSubmitted] = useState(false);
|
const [submitted, setSubmitted] = useState(false);
|
||||||
// TronLink detection state
|
// TronLink detection state — now handled by WalletSelector(showTron=true)
|
||||||
const [tronAddress, setTronAddress] = useState<string | null>(null);
|
const [tronAddress, setTronAddress] = useState<string | null>(null);
|
||||||
const [isTronConnecting, setIsTronConnecting] = useState(false);
|
|
||||||
const hasTronLink = typeof window !== "undefined" && (!!(window as unknown as Record<string, unknown>).tronWeb || !!(window as unknown as Record<string, unknown>).tronLink);
|
|
||||||
|
|
||||||
// Auto-detect TronLink on mount
|
|
||||||
useEffect(() => {
|
|
||||||
const detectTron = async () => {
|
|
||||||
// Wait briefly for TronLink to inject
|
|
||||||
await new Promise(resolve => setTimeout(resolve, 500));
|
|
||||||
const tronWeb = (window as unknown as Record<string, unknown>).tronWeb as { defaultAddress?: { base58?: string }; ready?: boolean } | undefined;
|
|
||||||
if (tronWeb && tronWeb.ready && tronWeb.defaultAddress?.base58) {
|
|
||||||
setTronAddress(tronWeb.defaultAddress.base58);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
detectTron();
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
// Connect TronLink wallet
|
|
||||||
const handleConnectTronLink = async () => {
|
|
||||||
setIsTronConnecting(true);
|
|
||||||
try {
|
|
||||||
const tronLink = (window as unknown as Record<string, unknown>).tronLink as { request?: (args: { method: string }) => Promise<{ code: number }> } | undefined;
|
|
||||||
const tronWeb = (window as unknown as Record<string, unknown>).tronWeb as { defaultAddress?: { base58?: string }; ready?: boolean } | undefined;
|
|
||||||
if (tronLink?.request) {
|
|
||||||
const result = await tronLink.request({ method: 'tron_requestAccounts' });
|
|
||||||
if (result?.code === 200 && tronWeb?.defaultAddress?.base58) {
|
|
||||||
setTronAddress(tronWeb.defaultAddress.base58);
|
|
||||||
toast.success(lang === "zh" ? "TronLink已连接!" : "TronLink connected!");
|
|
||||||
}
|
|
||||||
} else if (tronWeb?.ready && tronWeb.defaultAddress?.base58) {
|
|
||||||
setTronAddress(tronWeb.defaultAddress.base58);
|
|
||||||
toast.success(lang === "zh" ? "TronLink已检测到!" : "TronLink detected!");
|
|
||||||
} else {
|
|
||||||
toast.error(lang === "zh" ? "请安装TronLink钱包扩展" : "Please install TronLink wallet extension");
|
|
||||||
}
|
|
||||||
} catch {
|
|
||||||
toast.error(lang === "zh" ? "连接TronLink失败" : "Failed to connect TronLink");
|
|
||||||
} finally {
|
|
||||||
setIsTronConnecting(false);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// Auto-fill EVM address whenever wallet connects or address changes (unless user already submitted)
|
// Auto-fill EVM address whenever wallet connects or address changes (unless user already submitted)
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
|
@ -252,7 +212,7 @@ function TRC20Panel({ usdtAmount, lang, connectedAddress, onConnectWallet }: { u
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* TronLink Wallet Detection */}
|
{/* TronLink Wallet Detection — using unified WalletSelector with showTron=true */}
|
||||||
<div className="rounded-xl p-4 space-y-3" style={{ background: "rgba(255,0,19,0.06)", border: "1px solid rgba(255,0,19,0.25)" }}>
|
<div className="rounded-xl p-4 space-y-3" style={{ background: "rgba(255,0,19,0.06)", border: "1px solid rgba(255,0,19,0.25)" }}>
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<svg width="18" height="18" viewBox="0 0 24 24" fill="none">
|
<svg width="18" height="18" viewBox="0 0 24 24" fill="none">
|
||||||
|
|
@ -261,7 +221,7 @@ function TRC20Panel({ usdtAmount, lang, connectedAddress, onConnectWallet }: { u
|
||||||
<path d="M12 8L16 10.5V13.5L12 16L8 13.5V10.5L12 8Z" fill="#FF0013"/>
|
<path d="M12 8L16 10.5V13.5L12 16L8 13.5V10.5L12 8Z" fill="#FF0013"/>
|
||||||
</svg>
|
</svg>
|
||||||
<p className="text-sm font-semibold" style={{ color: "#ff6b6b" }}>
|
<p className="text-sm font-semibold" style={{ color: "#ff6b6b" }}>
|
||||||
{lang === "zh" ? "TronLink 钱包(可选)" : "TronLink Wallet (Optional)"}
|
{lang === "zh" ? "连接 TronLink 钱包(可选)" : "Connect TronLink Wallet (Optional)"}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
{tronAddress ? (
|
{tronAddress ? (
|
||||||
|
|
@ -283,45 +243,28 @@ function TRC20Panel({ usdtAmount, lang, connectedAddress, onConnectWallet }: { u
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
<p className="text-xs text-white/50">
|
<p className="text-xs text-white/50 mb-2">
|
||||||
{lang === "zh"
|
{lang === "zh"
|
||||||
? "如果您使用 TronLink 钱包,可以连接后自动验证您的 TRON 地址。"
|
? "连接 TronLink 可自动验证您的 TRON 地址。手机用户可通过 TronLink App 内置浏览器打开本页面。"
|
||||||
: "If you use TronLink wallet, connect to auto-verify your TRON address."}
|
: "Connect TronLink to auto-verify your TRON address. Mobile users can open this page in TronLink App's built-in browser."}
|
||||||
</p>
|
</p>
|
||||||
{hasTronLink ? (
|
<WalletSelector
|
||||||
<button
|
lang={lang}
|
||||||
onClick={handleConnectTronLink}
|
showTron={true}
|
||||||
disabled={isTronConnecting}
|
compact={true}
|
||||||
className="w-full py-2.5 rounded-xl text-sm font-semibold flex items-center justify-center gap-2 transition-all hover:opacity-90"
|
onAddressDetected={(addr, network) => {
|
||||||
style={{
|
if (network === "tron") {
|
||||||
background: "rgba(255,0,19,0.15)",
|
setTronAddress(addr);
|
||||||
border: "1px solid rgba(255,0,19,0.4)",
|
toast.success(lang === "zh" ? "TronLink 已连接!" : "TronLink connected!");
|
||||||
color: "#ff6b6b",
|
} else {
|
||||||
}}
|
// EVM address detected in TRC20 panel — use as XIC receiving address
|
||||||
>
|
setEvmAddress(addr);
|
||||||
<svg width="16" height="16" viewBox="0 0 24 24" fill="none">
|
setEvmAddrError("");
|
||||||
<circle cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="2"/>
|
toast.success(lang === "zh" ? "XIC接收地址已自动填充!" : "XIC receiving address auto-filled!");
|
||||||
<path d="M12 6v6l4 2" stroke="currentColor" strokeWidth="2" strokeLinecap="round"/>
|
if (onConnectWallet) onConnectWallet();
|
||||||
</svg>
|
}
|
||||||
{isTronConnecting
|
}}
|
||||||
? (lang === "zh" ? "连接中..." : "Connecting...")
|
/>
|
||||||
: (lang === "zh" ? "连接 TronLink 自动验证" : "Connect TronLink to verify")}
|
|
||||||
</button>
|
|
||||||
) : (
|
|
||||||
<a
|
|
||||||
href="https://www.tronlink.org/"
|
|
||||||
target="_blank"
|
|
||||||
rel="noopener noreferrer"
|
|
||||||
className="w-full py-2.5 rounded-xl text-sm font-semibold flex items-center justify-center gap-2 transition-all hover:opacity-90 block text-center"
|
|
||||||
style={{
|
|
||||||
background: "rgba(255,0,19,0.08)",
|
|
||||||
border: "1px solid rgba(255,0,19,0.25)",
|
|
||||||
color: "rgba(255,107,107,0.7)",
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{lang === "zh" ? "安装 TronLink 钱包 →" : "Install TronLink Wallet →"}
|
|
||||||
</a>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -421,8 +364,8 @@ function EVMPurchasePanel({ network, lang, wallet }: { network: "BSC" | "ETH"; l
|
||||||
lang={lang}
|
lang={lang}
|
||||||
connectedAddress={wallet.address ?? undefined}
|
connectedAddress={wallet.address ?? undefined}
|
||||||
onAddressDetected={(addr) => {
|
onAddressDetected={(addr) => {
|
||||||
// Address detected — wallet is now connected, trigger wallet.connect to sync state
|
// WalletSelector already called eth_requestAccounts and got the address
|
||||||
wallet.connect();
|
// Just show success toast; wallet state will auto-update via accountsChanged event
|
||||||
toast.success(lang === "zh" ? `已连接: ${addr.slice(0, 6)}...${addr.slice(-4)}` : `Connected: ${addr.slice(0, 6)}...${addr.slice(-4)}`);
|
toast.success(lang === "zh" ? `已连接: ${addr.slice(0, 6)}...${addr.slice(-4)}` : `Connected: ${addr.slice(0, 6)}...${addr.slice(-4)}`);
|
||||||
}}
|
}}
|
||||||
compact
|
compact
|
||||||
|
|
@ -814,6 +757,7 @@ type WalletHookReturn = ReturnType<typeof useWallet>;
|
||||||
function NavWalletButton({ lang, wallet }: { lang: Lang; wallet: WalletHookReturn }) {
|
function NavWalletButton({ lang, wallet }: { lang: Lang; wallet: WalletHookReturn }) {
|
||||||
const { t } = useTranslation(lang);
|
const { t } = useTranslation(lang);
|
||||||
const [showMenu, setShowMenu] = useState(false);
|
const [showMenu, setShowMenu] = useState(false);
|
||||||
|
const [showWalletModal, setShowWalletModal] = useState(false);
|
||||||
const menuRef = useRef<HTMLDivElement>(null);
|
const menuRef = useRef<HTMLDivElement>(null);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
|
@ -824,21 +768,105 @@ function NavWalletButton({ lang, wallet }: { lang: Lang; wallet: WalletHookRetur
|
||||||
return () => document.removeEventListener("mousedown", handleClick);
|
return () => document.removeEventListener("mousedown", handleClick);
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
// Detect mobile browser
|
||||||
|
const isMobile = typeof window !== "undefined" && /Android|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(navigator.userAgent);
|
||||||
|
|
||||||
|
// Handle connect button click — show wallet selector modal
|
||||||
|
const handleConnectClick = async () => {
|
||||||
|
// On mobile browsers, skip direct connect attempt and show modal immediately
|
||||||
|
// (mobile browsers don't support wallet extensions)
|
||||||
|
if (isMobile) {
|
||||||
|
setShowWalletModal(true);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// On desktop: first try direct connect (works if wallet is already set up and locked)
|
||||||
|
const result = await wallet.connect();
|
||||||
|
if (!result.success && result.error) {
|
||||||
|
// If direct connect failed, show the wallet selector modal for guided setup
|
||||||
|
setShowWalletModal(true);
|
||||||
|
toast.error(result.error, { duration: 6000 });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
if (!wallet.isConnected) {
|
if (!wallet.isConnected) {
|
||||||
return (
|
return (
|
||||||
<button
|
<>
|
||||||
onClick={wallet.connect}
|
<button
|
||||||
disabled={wallet.isConnecting}
|
onClick={handleConnectClick}
|
||||||
className="flex items-center gap-2 px-4 py-2 rounded-xl text-sm font-bold transition-all hover:opacity-90"
|
disabled={wallet.isConnecting}
|
||||||
style={{ background: "linear-gradient(135deg, rgba(240,180,41,0.9) 0%, rgba(255,215,0,0.9) 100%)", color: "#0a0a0f", fontFamily: "'Space Grotesk', sans-serif", boxShadow: "0 0 16px rgba(240,180,41,0.3)" }}
|
className="flex items-center gap-2 px-4 py-2 rounded-xl text-sm font-bold transition-all hover:opacity-90"
|
||||||
>
|
style={{ background: "linear-gradient(135deg, rgba(240,180,41,0.9) 0%, rgba(255,215,0,0.9) 100%)", color: "#0a0a0f", fontFamily: "'Space Grotesk', sans-serif", boxShadow: "0 0 16px rgba(240,180,41,0.3)" }}
|
||||||
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.5">
|
>
|
||||||
<path d="M21 12V7H5a2 2 0 0 1 0-4h14v4"/>
|
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.5">
|
||||||
<path d="M3 5v14a2 2 0 0 0 2 2h16v-5"/>
|
<path d="M21 12V7H5a2 2 0 0 1 0-4h14v4"/>
|
||||||
<path d="M18 12a2 2 0 0 0 0 4h4v-4z"/>
|
<path d="M3 5v14a2 2 0 0 0 2 2h16v-5"/>
|
||||||
</svg>
|
<path d="M18 12a2 2 0 0 0 0 4h4v-4z"/>
|
||||||
{wallet.isConnecting ? t("nav_connecting") : t("nav_connect")}
|
</svg>
|
||||||
</button>
|
{wallet.isConnecting ? t("nav_connecting") : t("nav_connect")}
|
||||||
|
</button>
|
||||||
|
|
||||||
|
{/* Wallet Connection Modal */}
|
||||||
|
{showWalletModal && (
|
||||||
|
<div
|
||||||
|
className="fixed inset-0 z-[100] flex items-center justify-center p-4"
|
||||||
|
style={{ background: "rgba(0,0,0,0.85)", backdropFilter: "blur(8px)" }}
|
||||||
|
onClick={(e) => { if (e.target === e.currentTarget) setShowWalletModal(false); }}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
className="w-full max-w-md rounded-2xl p-6 relative"
|
||||||
|
style={{ background: "rgba(10,10,20,0.98)", border: "1px solid rgba(240,180,41,0.3)", boxShadow: "0 0 40px rgba(240,180,41,0.15)" }}
|
||||||
|
>
|
||||||
|
{/* Close button */}
|
||||||
|
<button
|
||||||
|
onClick={() => setShowWalletModal(false)}
|
||||||
|
className="absolute top-4 right-4 text-white/40 hover:text-white/80 transition-colors"
|
||||||
|
>
|
||||||
|
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
|
||||||
|
<path d="M18 6L6 18M6 6l12 12"/>
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<h3 className="text-lg font-bold text-white mb-1" style={{ fontFamily: "'Space Grotesk', sans-serif" }}>
|
||||||
|
{lang === "zh" ? "连接钱包" : "Connect Wallet"}
|
||||||
|
</h3>
|
||||||
|
<p className="text-xs text-white/40 mb-4">
|
||||||
|
{lang === "zh"
|
||||||
|
? "选择您的钱包进行连接,或手动输入地址"
|
||||||
|
: "Select your wallet to connect, or enter address manually"}
|
||||||
|
</p>
|
||||||
|
|
||||||
|
{/* MetaMask initialization guide */}
|
||||||
|
<div
|
||||||
|
className="rounded-xl p-3 mb-4"
|
||||||
|
style={{ background: "rgba(240,180,41,0.06)", border: "1px solid rgba(240,180,41,0.2)" }}
|
||||||
|
>
|
||||||
|
<p className="text-xs text-amber-300/80 leading-relaxed">
|
||||||
|
{lang === "zh"
|
||||||
|
? "💡 首次使用 MetaMask?请先打开 MetaMask 扩展完成初始化(创建或导入钱包),完成后点击下方「刷新」按钮重新检测。"
|
||||||
|
: "💡 First time using MetaMask? Open the MetaMask extension and complete setup (create or import a wallet), then click Refresh below to re-detect."}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<WalletSelector
|
||||||
|
lang={lang}
|
||||||
|
connectedAddress={wallet.address ?? undefined}
|
||||||
|
onAddressDetected={async (addr) => {
|
||||||
|
// After address detected from WalletSelector, sync wallet state
|
||||||
|
const result = await wallet.connect();
|
||||||
|
if (result.success) {
|
||||||
|
setShowWalletModal(false);
|
||||||
|
toast.success(lang === "zh" ? `钱包已连接: ${addr.slice(0, 6)}...${addr.slice(-4)}` : `Wallet connected: ${addr.slice(0, 6)}...${addr.slice(-4)}`);
|
||||||
|
} else {
|
||||||
|
// Even if connect() failed, we have the address — close modal
|
||||||
|
setShowWalletModal(false);
|
||||||
|
toast.success(lang === "zh" ? `地址已确认: ${addr.slice(0, 6)}...${addr.slice(-4)}` : `Address confirmed: ${addr.slice(0, 6)}...${addr.slice(-4)}`);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue