Checkpoint: 修复钱包连接Modal问题:1) 将showWalletModal状态提升到Home主组件;2) Modal通过Portal渲染到document.body,脱离导航栏backdropFilter层叠上下文;3) EVMPurchasePanel内嵌WalletSelector替换为统一的Connect Wallet按钮,触发同一个顶层Modal;4) 停止旧预售合约0xc65e7a27...

This commit is contained in:
Manus 2026-03-10 02:12:35 -04:00
parent a7aa132b71
commit 1d0e293bdb
1 changed files with 82 additions and 124 deletions

View File

@ -4,6 +4,7 @@
// Colors: Amber Gold #f0b429 | Quantum Blue #00d4ff | Deep Black #0a0a0f
import { useState, useEffect, useCallback, useRef, useMemo } from "react";
import { createPortal } from "react-dom";
import { toast } from "sonner";
import { Link } from "wouter";
import { useWallet } from "@/hooks/useWallet";
@ -315,7 +316,7 @@ function TRC20Panel({ usdtAmount, lang, connectedAddress, onConnectWallet }: { u
}
// ─── EVM Purchase Panel ─────────────────────────────────────────────────────
function EVMPurchasePanel({ network, lang, wallet }: { network: "BSC" | "ETH"; lang: Lang; wallet: WalletHookReturn }) {
function EVMPurchasePanel({ network, lang, wallet, onOpenWalletModal }: { network: "BSC" | "ETH"; lang: Lang; wallet: WalletHookReturn; onOpenWalletModal?: () => void }) {
const { t } = useTranslation(lang);
const { purchaseState, buyWithUSDT, reset, calcTokens, getUsdtBalance } = usePresale(wallet, network);
const [usdtInput, setUsdtInput] = useState("100");
@ -361,22 +362,18 @@ function EVMPurchasePanel({ network, lang, wallet }: { network: "BSC" | "ETH"; l
return (
<div className="space-y-4">
<p className="text-sm text-white/60 text-center">{t("buy_connect_msg")}</p>
<WalletSelector
lang={lang}
connectedAddress={wallet.address ?? undefined}
onAddressDetected={async (addr, _network, rawProvider) => {
// KEY FIX: call connectWithProvider to sync wallet state immediately
// Do NOT rely on accountsChanged event — it only fires for window.ethereum listeners
if (rawProvider) {
await wallet.connectWithProvider(rawProvider, addr);
} else {
// Manual address entry — no provider available, set address-only state
await wallet.connectWithProvider({ request: async () => [] } as unknown as import("@/hooks/useWallet").EthProvider, addr);
}
toast.success(lang === "zh" ? `已连接: ${addr.slice(0, 6)}...${addr.slice(-4)}` : `Connected: ${addr.slice(0, 6)}...${addr.slice(-4)}`);
}}
compact
/>
<button
onClick={() => onOpenWalletModal?.()}
className="w-full py-3 rounded-xl text-base font-bold transition-all hover:opacity-90 flex items-center justify-center gap-2"
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="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.5">
<path d="M21 12V7H5a2 2 0 0 1 0-4h14v4"/>
<path d="M3 5v14a2 2 0 0 0 2 2h16v-5"/>
<path d="M18 12a2 2 0 0 0 0 4h4v-4z"/>
</svg>
{lang === "zh" ? "连接钱包" : "Connect Wallet"}
</button>
<div className="text-xs text-white/40 text-center">{t("buy_connect_hint")}</div>
</div>
);
@ -809,10 +806,9 @@ function ChatSupport({ lang }: { lang: Lang }) {
// ─── Navbar Wallet Button ─────────────────────────────────────────────────────
type WalletHookReturn = ReturnType<typeof useWallet>;
function NavWalletButton({ lang, wallet }: { lang: Lang; wallet: WalletHookReturn }) {
function NavWalletButton({ lang, wallet, showWalletModal, setShowWalletModal }: { lang: Lang; wallet: WalletHookReturn; showWalletModal: boolean; setShowWalletModal: (v: boolean) => void }) {
const { t } = useTranslation(lang);
const [showMenu, setShowMenu] = useState(false);
const [showWalletModal, setShowWalletModal] = useState(false);
const menuRef = useRef<HTMLDivElement>(null);
useEffect(() => {
@ -823,35 +819,10 @@ function NavWalletButton({ lang, wallet }: { lang: Lang; wallet: WalletHookRetur
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);
// Detect if running inside a wallet's in-app browser (window.ethereum is injected)
const isInWalletBrowser = typeof window !== "undefined" && !!((window as unknown as Record<string, unknown>).ethereum);
// Handle connect button click — show wallet selector modal
const handleConnectClick = async () => {
// On mobile AND inside a wallet's in-app browser: try direct connect first
// (window.ethereum is injected by the wallet app, so direct connect works)
if (isMobile && isInWalletBrowser) {
const result = await wallet.connect();
if (!result.success) {
// If direct connect failed, show modal as fallback
// Handle connect button click — always show wallet selector modal
// Both desktop and mobile users see the modal to choose their wallet
const handleConnectClick = () => {
setShowWalletModal(true);
}
return;
}
// On mobile browser (NOT in wallet app): show modal with DeepLink guide
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) {
@ -871,79 +842,6 @@ function NavWalletButton({ lang, wallet }: { lang: Lang; wallet: WalletHookRetur
{wallet.isConnecting ? t("nav_connecting") : t("nav_connect")}
</button>
{/* Wallet Connection Modal */}
{showWalletModal && (
<div
className="fixed inset-0 z-[100] flex items-end sm:items-center justify-center sm: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 sm:max-w-md rounded-t-2xl sm:rounded-2xl p-5 relative overflow-y-auto"
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)", maxHeight: "85vh" }}
>
{/* 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>
{!isMobile && (
<p className="text-xs text-white/40 mb-4">
{lang === "zh"
? "选择您的钱包进行连接,或手动输入地址"
: "Select your wallet to connect, or enter address manually"}
</p>
)}
{/* MetaMask initialization guide — desktop only */}
{!isMobile && (
<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>
)}
{isMobile && <div className="mb-3" />}
<WalletSelector
lang={lang}
compact={false}
showTron={false}
connectedAddress={undefined}
onAddressDetected={async (addr, _network, rawProvider) => {
// KEY FIX: use connectWithProvider() to directly sync wallet state
// This avoids calling connect() again which would use detectProvider()
// and might fail if the wallet injects to a different window property (e.g. OKX -> window.okxwallet)
if (rawProvider) {
await wallet.connectWithProvider(rawProvider, addr);
} else {
// Fallback for manual address entry (no provider)
const result = await wallet.connect();
if (!result.success) {
// Still mark as connected with address only
await wallet.connectWithProvider({ request: async () => [] } as unknown as import("@/hooks/useWallet").EthProvider, addr);
}
}
setShowWalletModal(false);
toast.success(lang === "zh" ? `钱包已连接: ${addr.slice(0, 6)}...${addr.slice(-4)}` : `Wallet connected: ${addr.slice(0, 6)}...${addr.slice(-4)}`);
}}
/>
</div>
</div>
)}
</>
);
}
@ -1042,6 +940,8 @@ export default function Home() {
// 钱包状态提升到顶层共享给NavWalletButton和EVMPurchasePanel
const wallet = useWallet();
// showWalletModal提升到顶层供NavWalletButton和EVMPurchasePanel共用
const [showWalletModal, setShowWalletModal] = useState(false);
const networks: NetworkTab[] = ["BSC", "ETH", "TRON"];
@ -1081,7 +981,7 @@ export default function Home() {
</span>
</Link>
<LangToggle lang={lang} setLang={setLang} />
<NavWalletButton lang={lang} wallet={wallet} />
<NavWalletButton lang={lang} wallet={wallet} showWalletModal={showWalletModal} setShowWalletModal={setShowWalletModal} />
</div>
</nav>
@ -1266,8 +1166,8 @@ export default function Home() {
</p>
</div>
)}
{activeNetwork === "BSC" && <EVMPurchasePanel network="BSC" lang={lang} wallet={wallet} />}
{activeNetwork === "ETH" && <EVMPurchasePanel network="ETH" lang={lang} wallet={wallet} />}
{activeNetwork === "BSC" && <EVMPurchasePanel network="BSC" lang={lang} wallet={wallet} onOpenWalletModal={() => setShowWalletModal(true)} />}
{activeNetwork === "ETH" && <EVMPurchasePanel network="ETH" lang={lang} wallet={wallet} onOpenWalletModal={() => setShowWalletModal(true)} />}
{activeNetwork === "TRON" && (
<div className="space-y-4">
<div className="space-y-2">
@ -1386,6 +1286,64 @@ export default function Home() {
{/* ── Chat Support Widget ── */}
<ChatSupport lang={lang} />
{/* ── Global Wallet Connection Modal (Portal) ── */}
{showWalletModal && createPortal(
<div
className="fixed inset-0 z-[9999] flex items-end sm:items-center justify-center sm: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 sm:max-w-md rounded-t-2xl sm:rounded-2xl p-5 relative overflow-y-auto"
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)", maxHeight: "85vh" }}
>
<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>
{!/Android|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(navigator.userAgent) && (
<>
<p className="text-xs text-white/40 mb-4">
{lang === "zh" ? "选择您的钱包进行连接,或手动输入地址" : "Select your wallet to connect, or enter address manually"}
</p>
<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}
compact={false}
showTron={false}
connectedAddress={undefined}
onAddressDetected={async (addr, _network, rawProvider) => {
if (rawProvider) {
await wallet.connectWithProvider(rawProvider, addr);
} else {
const result = await wallet.connect();
if (!result.success) {
await wallet.connectWithProvider({ request: async () => [] } as unknown as import("@/hooks/useWallet").EthProvider, addr);
}
}
setShowWalletModal(false);
toast.success(lang === "zh" ? `钱包已连接: ${addr.slice(0, 6)}...${addr.slice(-4)}` : `Wallet connected: ${addr.slice(0, 6)}...${addr.slice(-4)}`);
}}
/>
</div>
</div>
, document.body)}
<style>{`
@keyframes fadeInDown {
from { opacity: 0; transform: translateY(-10px); }