v12: WalletSelector重写 - 增加错误诊断、MetaMask权限重置引导、连接状态优化
主要改进: - ErrorHelpPanel: 分类错误处理(user_rejected/wallet_pending/not_initialized/unknown) - MetaMask 4001错误时显示5步权限重置操作指南 - 连接中状态显示'等待钱包授权...'提示 - 错误后保留重试按钮和其他可用钱包选项 - 增加eth_accounts静默检查(先检查是否已连接) - Bridge: 确认所有链USDT->XIC路由(BSC/ETH/Polygon/Arbitrum/Avalanche)
This commit is contained in:
parent
4bdb118cb2
commit
1576303898
|
|
@ -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
|
||||||
// v4: added TronLink support (desktop window.tronLink + mobile DeepLink)
|
// v5: improved error handling, MetaMask permission reset guide, connection diagnostics
|
||||||
|
|
||||||
import { useState, useEffect, useCallback } from "react";
|
import { useState, useEffect, useCallback } from "react";
|
||||||
import type { EthProvider } from "@/hooks/useWallet";
|
import type { EthProvider } from "@/hooks/useWallet";
|
||||||
|
|
@ -12,19 +12,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
|
mobileDeepLink?: string;
|
||||||
isInstalled: () => boolean;
|
isInstalled: () => boolean;
|
||||||
connect: () => Promise<{ address: string; rawProvider: EthProvider } | null>;
|
connect: () => Promise<{ address: string; rawProvider: EthProvider } | null>;
|
||||||
network: "evm" | "tron"; // wallet network type
|
network: "evm" | "tron";
|
||||||
}
|
}
|
||||||
|
|
||||||
interface WalletSelectorProps {
|
interface WalletSelectorProps {
|
||||||
lang: Lang;
|
lang: Lang;
|
||||||
// rawProvider is passed so parent can call wallet.connectWithProvider() directly
|
|
||||||
onAddressDetected: (address: string, network?: "evm" | "tron", rawProvider?: EthProvider) => void;
|
onAddressDetected: (address: string, network?: "evm" | "tron", rawProvider?: EthProvider) => void;
|
||||||
connectedAddress?: string;
|
connectedAddress?: string;
|
||||||
compact?: boolean; // compact mode for BSC/ETH panel
|
compact?: boolean;
|
||||||
showTron?: boolean; // whether to show TRON wallets (for TRC20 panel)
|
showTron?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── Wallet Icons ──────────────────────────────────────────────────────────────
|
// ── Wallet Icons ──────────────────────────────────────────────────────────────
|
||||||
|
|
@ -85,7 +84,6 @@ const BitgetIcon = () => (
|
||||||
</svg>
|
</svg>
|
||||||
);
|
);
|
||||||
|
|
||||||
// TronLink Icon — official TRON red color
|
|
||||||
const TronLinkIcon = () => (
|
const TronLinkIcon = () => (
|
||||||
<svg width="24" height="24" viewBox="0 0 24 24" fill="none">
|
<svg width="24" height="24" viewBox="0 0 24 24" fill="none">
|
||||||
<circle cx="12" cy="12" r="12" fill="#FF0013"/>
|
<circle cx="12" cy="12" r="12" fill="#FF0013"/>
|
||||||
|
|
@ -102,7 +100,6 @@ function isMobileBrowser(): boolean {
|
||||||
return /Android|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(navigator.userAgent);
|
return /Android|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(navigator.userAgent);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check if running inside a wallet's in-app browser
|
|
||||||
function isInWalletBrowser(): boolean {
|
function isInWalletBrowser(): boolean {
|
||||||
if (typeof window === "undefined") return false;
|
if (typeof window === "undefined") return false;
|
||||||
const ua = navigator.userAgent.toLowerCase();
|
const ua = navigator.userAgent.toLowerCase();
|
||||||
|
|
@ -126,7 +123,6 @@ function isInWalletBrowser(): boolean {
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── Provider detection helpers ────────────────────────────────────────────────
|
// ── Provider detection helpers ────────────────────────────────────────────────
|
||||||
// Note: EthProvider type is imported from useWallet.ts
|
|
||||||
|
|
||||||
type TronLinkProvider = {
|
type TronLinkProvider = {
|
||||||
ready: boolean;
|
ready: boolean;
|
||||||
|
|
@ -156,9 +152,7 @@ function getBitget(): EthProvider | null {
|
||||||
function getTronLink(): TronLinkProvider | null {
|
function getTronLink(): TronLinkProvider | null {
|
||||||
if (typeof window === "undefined") return null;
|
if (typeof window === "undefined") return null;
|
||||||
const w = window as unknown as { tronLink?: TronLinkProvider; tronWeb?: TronLinkProvider["tronWeb"] };
|
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;
|
if (w.tronLink?.ready) return w.tronLink;
|
||||||
// Some versions only inject tronWeb
|
|
||||||
if (w.tronWeb) {
|
if (w.tronWeb) {
|
||||||
return {
|
return {
|
||||||
ready: true,
|
ready: true,
|
||||||
|
|
@ -169,7 +163,6 @@ function getTronLink(): TronLinkProvider | null {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 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();
|
||||||
if (!eth) return null;
|
if (!eth) return null;
|
||||||
|
|
@ -179,7 +172,18 @@ function findProvider(predicate: (p: EthProvider) => boolean): EthProvider | nul
|
||||||
return predicate(eth) ? eth : null;
|
return predicate(eth) ? eth : null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ── Improved requestAccounts with better error classification ─────────────────
|
||||||
async function requestAccounts(provider: EthProvider): Promise<{ address: string; rawProvider: EthProvider } | null> {
|
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 {
|
try {
|
||||||
const accounts = await provider.request({ method: "eth_requestAccounts" }) as string[];
|
const accounts = await provider.request({ method: "eth_requestAccounts" }) as string[];
|
||||||
const address = accounts?.[0] ?? null;
|
const address = accounts?.[0] ?? null;
|
||||||
|
|
@ -187,22 +191,26 @@ async function requestAccounts(provider: EthProvider): Promise<{ address: string
|
||||||
return { address, rawProvider: provider };
|
return { address, rawProvider: provider };
|
||||||
} catch (err: unknown) {
|
} catch (err: unknown) {
|
||||||
const error = err as { code?: number; message?: string };
|
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 === 4001) throw new Error("user_rejected");
|
||||||
if (error?.code === -32002) throw new Error("wallet_pending");
|
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;
|
throw err;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function requestTronAccounts(provider: TronLinkProvider): Promise<{ address: string; rawProvider: EthProvider } | null> {
|
async function requestTronAccounts(provider: TronLinkProvider): Promise<{ address: string; rawProvider: EthProvider } | null> {
|
||||||
try {
|
try {
|
||||||
// TronLink v1: use tronWeb.defaultAddress
|
|
||||||
if (provider.tronWeb?.defaultAddress?.base58) {
|
if (provider.tronWeb?.defaultAddress?.base58) {
|
||||||
return { address: provider.tronWeb.defaultAddress.base58, rawProvider: provider as unknown as EthProvider };
|
return { address: provider.tronWeb.defaultAddress.base58, rawProvider: provider as unknown as EthProvider };
|
||||||
}
|
}
|
||||||
// TronLink v2+: use request method
|
|
||||||
const result = await provider.request({ method: "tron_requestAccounts" }) as { code?: number; message?: string };
|
const result = await provider.request({ method: "tron_requestAccounts" }) as { code?: number; message?: string };
|
||||||
if (result?.code === 200) {
|
if (result?.code === 200) {
|
||||||
// After approval, tronWeb.defaultAddress should be populated
|
|
||||||
const w = window as unknown as { tronWeb?: { defaultAddress?: { base58?: string } } };
|
const w = window as unknown as { tronWeb?: { defaultAddress?: { base58?: string } } };
|
||||||
const address = w.tronWeb?.defaultAddress?.base58 ?? null;
|
const address = w.tronWeb?.defaultAddress?.base58 ?? null;
|
||||||
if (!address) return null;
|
if (!address) return null;
|
||||||
|
|
@ -307,7 +315,6 @@ function buildWallets(showTron: boolean): WalletInfo[] {
|
||||||
name: "TronLink",
|
name: "TronLink",
|
||||||
icon: <TronLinkIcon />,
|
icon: <TronLinkIcon />,
|
||||||
installUrl: "https://www.tronlink.org/",
|
installUrl: "https://www.tronlink.org/",
|
||||||
// TronLink mobile DeepLink — opens current URL in TronLink's built-in browser
|
|
||||||
mobileDeepLink: `tronlinkoutside://pull.activity?param=${encodeURIComponent(JSON.stringify({ url: currentUrl, action: "open", protocol: "tronlink", version: "1.0" }))}`,
|
mobileDeepLink: `tronlinkoutside://pull.activity?param=${encodeURIComponent(JSON.stringify({ url: currentUrl, action: "open", protocol: "tronlink", version: "1.0" }))}`,
|
||||||
isInstalled: () => !!getTronLink(),
|
isInstalled: () => !!getTronLink(),
|
||||||
connect: async () => {
|
connect: async () => {
|
||||||
|
|
@ -332,10 +339,6 @@ function isValidTronAddress(addr: string): boolean {
|
||||||
return /^T[1-9A-HJ-NP-Za-km-z]{33}$/.test(addr);
|
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 ─────────────────────────────────────────────────────
|
// ── Mobile DeepLink Panel ─────────────────────────────────────────────────────
|
||||||
|
|
||||||
function MobileDeepLinkPanel({ lang, showTron }: { lang: Lang; showTron: boolean }) {
|
function MobileDeepLinkPanel({ lang, showTron }: { lang: Lang; showTron: boolean }) {
|
||||||
|
|
@ -343,7 +346,6 @@ function MobileDeepLinkPanel({ lang, showTron }: { lang: Lang; showTron: boolean
|
||||||
const urlWithoutProtocol = currentUrl.replace(/^https?:\/\//, "");
|
const urlWithoutProtocol = currentUrl.replace(/^https?:\/\//, "");
|
||||||
const [showMore, setShowMore] = useState(false);
|
const [showMore, setShowMore] = useState(false);
|
||||||
|
|
||||||
// MetaMask is always the primary wallet (most widely used)
|
|
||||||
const metamaskDeepLink = `https://metamask.app.link/dapp/${urlWithoutProtocol}`;
|
const metamaskDeepLink = `https://metamask.app.link/dapp/${urlWithoutProtocol}`;
|
||||||
|
|
||||||
const otherWallets = [
|
const otherWallets = [
|
||||||
|
|
@ -383,7 +385,6 @@ function MobileDeepLinkPanel({ lang, showTron }: { lang: Lang; showTron: boolean
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="space-y-3">
|
<div className="space-y-3">
|
||||||
{/* Primary: MetaMask — large prominent button */}
|
|
||||||
<a
|
<a
|
||||||
href={metamaskDeepLink}
|
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"
|
className="w-full flex items-center gap-3 px-4 py-4 rounded-2xl transition-all hover:opacity-90 active:scale-[0.98] block"
|
||||||
|
|
@ -407,7 +408,6 @@ function MobileDeepLinkPanel({ lang, showTron }: { lang: Lang; showTron: boolean
|
||||||
</svg>
|
</svg>
|
||||||
</a>
|
</a>
|
||||||
|
|
||||||
{/* Other wallets — collapsed by default */}
|
|
||||||
<button
|
<button
|
||||||
onClick={() => setShowMore(v => !v)}
|
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"
|
className="w-full text-xs text-white/35 hover:text-white/55 transition-colors py-1 flex items-center justify-center gap-1"
|
||||||
|
|
@ -454,22 +454,182 @@ function MobileDeepLinkPanel({ lang, showTron }: { lang: Lang; showTron: boolean
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ── 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 ──────────────────────────────────────────────────
|
// ── WalletSelector Component ──────────────────────────────────────────────────
|
||||||
|
|
||||||
export function WalletSelector({ lang, onAddressDetected, connectedAddress, compact = false, showTron = 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 [errorType, setErrorType] = useState<"user_rejected" | "wallet_pending" | "not_initialized" | "unknown" | null>(null);
|
||||||
|
const [errorWalletName, setErrorWalletName] = useState<string>("");
|
||||||
const [detecting, setDetecting] = useState(true);
|
const [detecting, setDetecting] = useState(true);
|
||||||
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 [isMobile] = useState(() => isMobileBrowser());
|
||||||
const [inWalletBrowser] = useState(() => isInWalletBrowser());
|
const [inWalletBrowser] = useState(() => isInWalletBrowser());
|
||||||
|
const [lastConnectedWallet, setLastConnectedWallet] = useState<WalletInfo | null>(null);
|
||||||
|
|
||||||
const detectWallets = useCallback(() => {
|
const detectWallets = useCallback(() => {
|
||||||
setDetecting(true);
|
setDetecting(true);
|
||||||
setError(null);
|
setErrorType(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(showTron));
|
setWallets(buildWallets(showTron));
|
||||||
|
|
@ -485,33 +645,40 @@ export function WalletSelector({ lang, onAddressDetected, connectedAddress, comp
|
||||||
|
|
||||||
const handleConnect = async (wallet: WalletInfo) => {
|
const handleConnect = async (wallet: WalletInfo) => {
|
||||||
setConnecting(wallet.id);
|
setConnecting(wallet.id);
|
||||||
setError(null);
|
setErrorType(null);
|
||||||
|
setLastConnectedWallet(wallet);
|
||||||
try {
|
try {
|
||||||
const result = await wallet.connect();
|
const result = await wallet.connect();
|
||||||
if (result) {
|
if (result) {
|
||||||
// Pass rawProvider so parent can call wallet.connectWithProvider() directly
|
|
||||||
onAddressDetected(result.address, wallet.network, result.rawProvider);
|
onAddressDetected(result.address, wallet.network, result.rawProvider);
|
||||||
} else {
|
} else {
|
||||||
setError(lang === "zh" ? "未获取到地址,请重试" : "No address returned, please try again");
|
setErrorType("unknown");
|
||||||
|
setErrorWalletName(wallet.name);
|
||||||
}
|
}
|
||||||
} catch (err: unknown) {
|
} catch (err: unknown) {
|
||||||
const error = err as Error;
|
const error = err as Error;
|
||||||
|
setErrorWalletName(wallet.name);
|
||||||
if (error.message === "user_rejected") {
|
if (error.message === "user_rejected") {
|
||||||
setError(lang === "zh" ? "已取消连接" : "Connection cancelled");
|
setErrorType("user_rejected");
|
||||||
} else if (error.message === "wallet_pending") {
|
} else if (error.message === "wallet_pending") {
|
||||||
setError(lang === "zh" ? "钱包请求处理中,请检查钱包弹窗" : "Wallet request pending, please check your wallet popup");
|
setErrorType("wallet_pending");
|
||||||
} 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"
|
setErrorType("not_initialized");
|
||||||
? "请先完成钱包初始化设置,然后刷新页面重试"
|
|
||||||
: "Please complete wallet setup first, then refresh the page");
|
|
||||||
} else {
|
} else {
|
||||||
setError(lang === "zh" ? "连接失败,请重试" : "Connection failed, please try again");
|
setErrorType("unknown");
|
||||||
}
|
}
|
||||||
} finally {
|
} finally {
|
||||||
setConnecting(null);
|
setConnecting(null);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const handleRetry = useCallback(() => {
|
||||||
|
setErrorType(null);
|
||||||
|
if (lastConnectedWallet) {
|
||||||
|
handleConnect(lastConnectedWallet);
|
||||||
|
}
|
||||||
|
}, [lastConnectedWallet]); // eslint-disable-line react-hooks/exhaustive-deps
|
||||||
|
|
||||||
const handleManualSubmit = () => {
|
const handleManualSubmit = () => {
|
||||||
const addr = manualAddress.trim();
|
const addr = manualAddress.trim();
|
||||||
if (!addr) {
|
if (!addr) {
|
||||||
|
|
@ -555,8 +722,6 @@ export function WalletSelector({ lang, onAddressDetected, connectedAddress, comp
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── Mobile browser (not in wallet app) — show DeepLink guide ──────────────
|
// ── Mobile browser (not in wallet app) — show DeepLink guide ──────────────
|
||||||
// On mobile browsers, ALWAYS show DeepLink guide regardless of whether wallets are "detected"
|
|
||||||
// because window.ethereum/okxwallet is NOT available in mobile browsers (only in wallet app's built-in browser)
|
|
||||||
if (isMobile && !inWalletBrowser && !detecting) {
|
if (isMobile && !inWalletBrowser && !detecting) {
|
||||||
return (
|
return (
|
||||||
<div className="space-y-3">
|
<div className="space-y-3">
|
||||||
|
|
@ -613,7 +778,6 @@ export function WalletSelector({ lang, onAddressDetected, connectedAddress, comp
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── Loading state ─────────────────────────────────────────────────────────
|
// ── Loading state ─────────────────────────────────────────────────────────
|
||||||
|
|
@ -622,7 +786,7 @@ export function WalletSelector({ lang, onAddressDetected, connectedAddress, comp
|
||||||
<div className="space-y-3">
|
<div className="space-y-3">
|
||||||
<div className="flex items-center justify-between">
|
<div className="flex items-center justify-between">
|
||||||
<p className="text-xs font-semibold text-white/60 uppercase tracking-wider">
|
<p className="text-xs font-semibold text-white/60 uppercase tracking-wider">
|
||||||
{lang === "zh" ? "选择钱包自动填充地址" : "Select wallet to auto-fill address"}
|
{lang === "zh" ? "选择钱包连接" : "Select Wallet to Connect"}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center justify-center py-4 gap-2">
|
<div className="flex items-center justify-center py-4 gap-2">
|
||||||
|
|
@ -642,7 +806,7 @@ export function WalletSelector({ lang, onAddressDetected, connectedAddress, comp
|
||||||
<div className="space-y-3">
|
<div className="space-y-3">
|
||||||
<div className="flex items-center justify-between">
|
<div className="flex items-center justify-between">
|
||||||
<p className="text-xs font-semibold text-white/60 uppercase tracking-wider">
|
<p className="text-xs font-semibold text-white/60 uppercase tracking-wider">
|
||||||
{lang === "zh" ? "选择钱包自动填充地址" : "Select wallet to auto-fill address"}
|
{lang === "zh" ? "选择钱包连接" : "Select Wallet to Connect"}
|
||||||
</p>
|
</p>
|
||||||
{/* Refresh detection button */}
|
{/* Refresh detection button */}
|
||||||
<button
|
<button
|
||||||
|
|
@ -665,7 +829,7 @@ export function WalletSelector({ lang, onAddressDetected, connectedAddress, comp
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Connecting overlay — shown when any wallet is connecting */}
|
{/* Connecting overlay */}
|
||||||
{connecting && (
|
{connecting && (
|
||||||
<div
|
<div
|
||||||
className="rounded-xl p-4 flex items-center gap-3"
|
className="rounded-xl p-4 flex items-center gap-3"
|
||||||
|
|
@ -677,16 +841,27 @@ export function WalletSelector({ lang, onAddressDetected, connectedAddress, comp
|
||||||
</svg>
|
</svg>
|
||||||
<div>
|
<div>
|
||||||
<p className="text-sm font-semibold text-white/80">
|
<p className="text-sm font-semibold text-white/80">
|
||||||
{lang === "zh" ? "正在连接钱包..." : "Connecting wallet..."}
|
{lang === "zh" ? "等待钱包授权..." : "Waiting for wallet authorization..."}
|
||||||
</p>
|
</p>
|
||||||
<p className="text-xs text-white/40 mt-0.5">
|
<p className="text-xs text-white/40 mt-0.5">
|
||||||
{lang === "zh" ? "请在钱包弹窗中确认连接请求" : "Please confirm the connection request in your wallet popup"}
|
{lang === "zh" ? "请查看钱包弹窗并点击「连接」" : "Please check your wallet popup and click \"Connect\""}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
{/* Installed wallets — hidden while connecting to prevent accidental double-clicks */}
|
|
||||||
{!connecting && installedWallets.length > 0 && (
|
{/* 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">
|
<div className="space-y-2">
|
||||||
{installedWallets.map(wallet => (
|
{installedWallets.map(wallet => (
|
||||||
<button
|
<button
|
||||||
|
|
@ -709,16 +884,36 @@ export function WalletSelector({ lang, onAddressDetected, connectedAddress, comp
|
||||||
>
|
>
|
||||||
{lang === "zh" ? "已安装" : "Installed"}
|
{lang === "zh" ? "已安装" : "Installed"}
|
||||||
</span>
|
</span>
|
||||||
{connecting === wallet.id ? (
|
<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">
|
||||||
<svg className="animate-spin w-4 h-4 text-white/60 flex-shrink-0" fill="none" viewBox="0 0 24 24">
|
<path d="M5 12h14M12 5l7 7-7 7"/>
|
||||||
<circle className="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="4"/>
|
</svg>
|
||||||
<path className="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4z"/>
|
</button>
|
||||||
</svg>
|
))}
|
||||||
) : (
|
</div>
|
||||||
<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>
|
{/* 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>
|
</button>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -740,13 +935,6 @@ export function WalletSelector({ lang, onAddressDetected, connectedAddress, comp
|
||||||
? "请安装以下任一钱包,完成设置后点击上方「刷新」按钮"
|
? "请安装以下任一钱包,完成设置后点击上方「刷新」按钮"
|
||||||
: "Install any wallet below, then click Refresh above after setup"}
|
: "Install any wallet below, then click Refresh above after setup"}
|
||||||
</p>
|
</p>
|
||||||
{showTron && (
|
|
||||||
<p className="text-xs text-amber-400/70">
|
|
||||||
{lang === "zh"
|
|
||||||
? "💡 TRC20 支付请安装 TronLink;BSC/ETH 支付请安装 MetaMask"
|
|
||||||
: "💡 For TRC20 install TronLink; for BSC/ETH install MetaMask"}
|
|
||||||
</p>
|
|
||||||
)}
|
|
||||||
{!showTron && (
|
{!showTron && (
|
||||||
<p className="text-xs text-amber-400/70">
|
<p className="text-xs text-amber-400/70">
|
||||||
{lang === "zh"
|
{lang === "zh"
|
||||||
|
|
@ -757,62 +945,43 @@ export function WalletSelector({ lang, onAddressDetected, connectedAddress, comp
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Not-installed wallets — only show when NO wallet is installed and not connecting */}
|
{/* Not installed wallets */}
|
||||||
{!connecting && !compact && notInstalledWallets.length > 0 && installedWallets.length === 0 && (
|
{!connecting && notInstalledWallets.length > 0 && (
|
||||||
<div className="space-y-1">
|
<div className="space-y-1.5">
|
||||||
<p className="text-xs text-white/30 mt-2">
|
<p className="text-xs text-white/25 uppercase tracking-wider">
|
||||||
{lang === "zh" ? "未安装(点击安装)" : "Not installed (click to install)"}
|
{lang === "zh" ? "未安装" : "Not installed"}
|
||||||
</p>
|
</p>
|
||||||
<div className="grid grid-cols-3 gap-2">
|
{notInstalledWallets.map(wallet => (
|
||||||
{notInstalledWallets.map(wallet => (
|
|
||||||
<a
|
|
||||||
key={wallet.id}
|
|
||||||
href={wallet.installUrl}
|
|
||||||
target="_blank"
|
|
||||||
rel="noopener noreferrer"
|
|
||||||
className="flex flex-col items-center gap-1.5 p-2.5 rounded-xl transition-all hover:opacity-80"
|
|
||||||
style={{
|
|
||||||
background: "rgba(255,255,255,0.04)",
|
|
||||||
border: "1px solid rgba(255,255,255,0.08)",
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<span className="opacity-40">{wallet.icon}</span>
|
|
||||||
<span className="text-xs text-white/30 text-center leading-tight">{wallet.name}</span>
|
|
||||||
</a>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{/* In compact mode, show install links inline */}
|
|
||||||
{!connecting && compact && notInstalledWallets.length > 0 && installedWallets.length === 0 && (
|
|
||||||
<div className="flex flex-wrap gap-2">
|
|
||||||
{notInstalledWallets.slice(0, 4).map(wallet => (
|
|
||||||
<a
|
<a
|
||||||
key={wallet.id}
|
key={wallet.id}
|
||||||
href={wallet.installUrl}
|
href={wallet.installUrl}
|
||||||
target="_blank"
|
target="_blank"
|
||||||
rel="noopener noreferrer"
|
rel="noopener noreferrer"
|
||||||
className="flex items-center gap-1.5 px-3 py-1.5 rounded-lg text-xs transition-all hover:opacity-80"
|
className="w-full flex items-center gap-3 px-4 py-2.5 rounded-xl transition-all hover:opacity-80"
|
||||||
style={{
|
style={{
|
||||||
background: "rgba(255,255,255,0.05)",
|
background: "rgba(255,255,255,0.02)",
|
||||||
border: "1px solid rgba(255,255,255,0.1)",
|
border: "1px dashed rgba(255,255,255,0.1)",
|
||||||
color: "rgba(255,255,255,0.4)",
|
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<span className="opacity-50">{wallet.icon}</span>
|
<span className="flex-shrink-0 opacity-50">{wallet.icon}</span>
|
||||||
{lang === "zh" ? `安装 ${wallet.name}` : `Install ${wallet.name}`}
|
<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>
|
</a>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{error && !connecting && (
|
{/* Manual address input */}
|
||||||
<p className="text-xs text-red-400 text-center">{error}</p>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{/* Manual address input — divider — hidden while connecting */}
|
|
||||||
{!connecting && (
|
|
||||||
<div className="pt-1">
|
<div className="pt-1">
|
||||||
<button
|
<button
|
||||||
onClick={() => { setShowManual(!showManual); setManualError(null); }}
|
onClick={() => { setShowManual(!showManual); setManualError(null); }}
|
||||||
|
|
@ -861,7 +1030,6 @@ export function WalletSelector({ lang, onAddressDetected, connectedAddress, comp
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
11
todo.md
11
todo.md
|
|
@ -141,3 +141,14 @@
|
||||||
- [ ] 修复用户取消钱包弹窗后状态不重置(error code 4001/4100处理)
|
- [ ] 修复用户取消钱包弹窗后状态不重置(error code 4001/4100处理)
|
||||||
- [ ] 修复连接成功后回调不触发(accounts事件监听改为直接返回值处理)
|
- [ ] 修复连接成功后回调不触发(accounts事件监听改为直接返回值处理)
|
||||||
- [ ] 确保每次点击钱包按钮都能重新触发钱包弹窗
|
- [ ] 确保每次点击钱包按钮都能重新触发钱包弹窗
|
||||||
|
|
||||||
|
## v12 Bridge跨链桥完善 + 钱包连接深度修复
|
||||||
|
|
||||||
|
- [ ] WalletSelector v5:ErrorHelpPanel组件(分类错误处理+MetaMask权限重置5步指南)
|
||||||
|
- [ ] WalletSelector v5:连接中状态改为"等待钱包授权"提示
|
||||||
|
- [ ] WalletSelector v5:错误后显示"重试"按钮和其他可用钱包
|
||||||
|
- [ ] Bridge页面:确认所有链(BSC/ETH/Polygon/Arbitrum/Avalanche)USDT→XIC路由逻辑
|
||||||
|
- [ ] Bridge页面:Gas费说明(每条链原生代币:BNB/ETH/MATIC/ETH/AVAX)
|
||||||
|
- [ ] 构建v12并部署到AI服务器(43.224.155.27)
|
||||||
|
- [ ] 同步代码到备份Git库(git.newassetchain.io)
|
||||||
|
- [ ] 记录部署日志
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue