diff --git a/client/src/components/WalletSelector.tsx b/client/src/components/WalletSelector.tsx index e891869..aed19b0 100644 --- a/client/src/components/WalletSelector.tsx +++ b/client/src/components/WalletSelector.tsx @@ -1,26 +1,44 @@ // NAC XIC Presale — Wallet Selector Component // Detects installed EVM wallets and shows connect/install buttons for each -// v3: added mobile detection, DeepLink support for MetaMask/Trust/OKX App +// v5: connect() returns { address, provider } so caller can use the correct wallet provider import { useState, useEffect, useCallback } from "react"; +import { Eip1193Provider } from "ethers"; type Lang = "zh" | "en"; +type EthProvider = Eip1193Provider & { + isMetaMask?: boolean; + isTrust?: boolean; + isTrustWallet?: boolean; + isOKExWallet?: boolean; + isOkxWallet?: boolean; + isCoinbaseWallet?: boolean; + isTokenPocket?: boolean; + isBitkeep?: boolean; + isBitgetWallet?: boolean; + isRabby?: boolean; + isSafePal?: boolean; + isImToken?: boolean; + isPhantom?: boolean; + providers?: EthProvider[]; +}; + interface WalletInfo { id: string; name: string; icon: React.ReactNode; installUrl: string; - mobileDeepLink?: string; // DeepLink to open current page in wallet's in-app browser + mobileDeepLink?: string; isInstalled: () => boolean; - connect: () => Promise; + connect: () => Promise<{ address: string; provider: EthProvider } | null>; } interface WalletSelectorProps { lang: Lang; - onAddressDetected: (address: string) => void; + onAddressDetected: (address: string, provider: EthProvider) => void; connectedAddress?: string; - compact?: boolean; // compact mode for BSC/ETH panel + compact?: boolean; } // ── Wallet Icons ────────────────────────────────────────────────────────────── @@ -81,6 +99,40 @@ const BitgetIcon = () => ( ); +const RabbyIcon = () => ( + + + + + + +); + +const SafePalIcon = () => ( + + + + + +); + +const ImTokenIcon = () => ( + + + + + +); + +const PhantomIcon = () => ( + + + + + + +); + // ── Mobile detection ────────────────────────────────────────────────────────── function isMobileBrowser(): boolean { @@ -88,50 +140,35 @@ function isMobileBrowser(): boolean { 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; - const eth = w.ethereum as { isMetaMask?: boolean; isTrust?: boolean; isTrustWallet?: boolean; isOKExWallet?: boolean; isOkxWallet?: boolean } | undefined; + const eth = w.ethereum as EthProvider | undefined; return !!( eth?.isMetaMask || eth?.isTrust || eth?.isTrustWallet || eth?.isOKExWallet || eth?.isOkxWallet || + eth?.isTokenPocket || + eth?.isBitkeep || + eth?.isBitgetWallet || + eth?.isRabby || + eth?.isSafePal || + eth?.isImToken || ua.includes("metamask") || ua.includes("trust") || ua.includes("okex") || ua.includes("tokenpocket") || - ua.includes("bitkeep") + ua.includes("bitkeep") || + ua.includes("imtoken") || + ua.includes("safepal") ); } -// Build DeepLink URL for opening current page in wallet's in-app browser -function buildDeepLink(walletScheme: string): string { - const currentUrl = typeof window !== "undefined" ? window.location.href : "https://pre-sale.newassetchain.io"; - // Remove protocol from URL for deeplink - const urlWithoutProtocol = currentUrl.replace(/^https?:\/\//, ""); - return `${walletScheme}${urlWithoutProtocol}`; -} - // ── Provider detection helpers ──────────────────────────────────────────────── -type EthProvider = { - isMetaMask?: boolean; - isTrust?: boolean; - isTrustWallet?: boolean; - isOKExWallet?: boolean; - isOkxWallet?: boolean; - isCoinbaseWallet?: boolean; - isTokenPocket?: boolean; - isBitkeep?: boolean; - isBitgetWallet?: boolean; - providers?: EthProvider[]; - request: (args: { method: string; params?: unknown[] }) => Promise; -}; - function getEth(): EthProvider | null { if (typeof window === "undefined") return null; return (window as unknown as { ethereum?: EthProvider }).ethereum ?? null; @@ -148,6 +185,18 @@ function getBitget(): EthProvider | null { return w.bitkeep?.ethereum ?? null; } +function getSafePal(): EthProvider | null { + if (typeof window === "undefined") return null; + const w = window as unknown as { safepalProvider?: EthProvider }; + return w.safepalProvider ?? null; +} + +function getImToken(): EthProvider | null { + if (typeof window === "undefined") return null; + const w = window as unknown as { imToken?: EthProvider }; + return w.imToken ?? null; +} + // Find a specific provider from the providers array or direct injection function findProvider(predicate: (p: EthProvider) => boolean): EthProvider | null { const eth = getEth(); @@ -158,15 +207,16 @@ function findProvider(predicate: (p: EthProvider) => boolean): EthProvider | nul return predicate(eth) ? eth : null; } -async function requestAccounts(provider: EthProvider): Promise { +// Connect to a specific provider and return { address, provider } +async function connectProvider(provider: EthProvider): Promise<{ address: string; provider: EthProvider } | null> { try { const accounts = await provider.request({ method: "eth_requestAccounts" }) as string[]; - return accounts?.[0] ?? null; + const address = accounts?.[0]; + if (!address) return null; + return { address, provider }; } catch (err: unknown) { const error = err as { code?: number; message?: string }; - // User rejected if (error?.code === 4001) throw new Error("user_rejected"); - // MetaMask not initialized / locked if (error?.code === -32002) throw new Error("wallet_pending"); throw err; } @@ -175,29 +225,20 @@ async function requestAccounts(provider: EthProvider): Promise { // ── Wallet definitions ──────────────────────────────────────────────────────── function buildWallets(): WalletInfo[] { + const currentUrl = typeof window !== "undefined" ? window.location.href : "https://pre-sale.newassetchain.io"; + const urlNoProto = currentUrl.replace(/^https?:\/\//, ""); + return [ { id: "metamask", name: "MetaMask", icon: , installUrl: "https://metamask.io/download/", - mobileDeepLink: buildDeepLink("https://metamask.app.link/dapp/"), + mobileDeepLink: `https://metamask.app.link/dapp/${urlNoProto}`, isInstalled: () => !!findProvider(p => !!p.isMetaMask), connect: async () => { const p = findProvider(p => !!p.isMetaMask) ?? getEth(); - return p ? requestAccounts(p) : null; - }, - }, - { - id: "trust", - name: "Trust Wallet", - icon: , - installUrl: "https://trustwallet.com/download", - mobileDeepLink: buildDeepLink("https://link.trustwallet.com/open_url?coin_id=60&url=https://"), - isInstalled: () => !!findProvider(p => !!(p.isTrust || p.isTrustWallet)), - connect: async () => { - const p = findProvider(p => !!(p.isTrust || p.isTrustWallet)) ?? getEth(); - return p ? requestAccounts(p) : null; + return p ? connectProvider(p) : null; }, }, { @@ -205,22 +246,23 @@ function buildWallets(): WalletInfo[] { name: "OKX Wallet", icon: , installUrl: "https://www.okx.com/web3", - mobileDeepLink: buildDeepLink("okx://wallet/dapp/url?dappUrl=https://"), + mobileDeepLink: `okx://wallet/dapp/url?dappUrl=${encodeURIComponent(currentUrl)}`, isInstalled: () => !!(getOKX() || findProvider(p => !!(p.isOKExWallet || p.isOkxWallet))), connect: async () => { const p = getOKX() ?? findProvider(p => !!(p.isOKExWallet || p.isOkxWallet)); - return p ? requestAccounts(p) : null; + return p ? connectProvider(p) : null; }, }, { - id: "coinbase", - name: "Coinbase Wallet", - icon: , - installUrl: "https://www.coinbase.com/wallet/downloads", - isInstalled: () => !!findProvider(p => !!p.isCoinbaseWallet), + id: "trust", + name: "Trust Wallet", + icon: , + installUrl: "https://trustwallet.com/download", + mobileDeepLink: `https://link.trustwallet.com/open_url?coin_id=60&url=${encodeURIComponent(currentUrl)}`, + isInstalled: () => !!findProvider(p => !!(p.isTrust || p.isTrustWallet)), connect: async () => { - const p = findProvider(p => !!p.isCoinbaseWallet) ?? getEth(); - return p ? requestAccounts(p) : null; + const p = findProvider(p => !!(p.isTrust || p.isTrustWallet)) ?? getEth(); + return p ? connectProvider(p) : null; }, }, { @@ -228,10 +270,11 @@ function buildWallets(): WalletInfo[] { name: "TokenPocket", icon: , installUrl: "https://www.tokenpocket.pro/en/download/app", + mobileDeepLink: `tpoutside://pull?param=${encodeURIComponent(JSON.stringify({ url: currentUrl }))}`, isInstalled: () => !!findProvider(p => !!p.isTokenPocket), connect: async () => { const p = findProvider(p => !!p.isTokenPocket) ?? getEth(); - return p ? requestAccounts(p) : null; + return p ? connectProvider(p) : null; }, }, { @@ -242,7 +285,63 @@ function buildWallets(): WalletInfo[] { isInstalled: () => !!(getBitget() || findProvider(p => !!(p.isBitkeep || p.isBitgetWallet))), connect: async () => { const p = getBitget() ?? findProvider(p => !!(p.isBitkeep || p.isBitgetWallet)); - return p ? requestAccounts(p) : null; + return p ? connectProvider(p) : null; + }, + }, + { + id: "coinbase", + name: "Coinbase Wallet", + icon: , + installUrl: "https://www.coinbase.com/wallet/downloads", + isInstalled: () => !!findProvider(p => !!p.isCoinbaseWallet), + connect: async () => { + const p = findProvider(p => !!p.isCoinbaseWallet) ?? getEth(); + return p ? connectProvider(p) : null; + }, + }, + { + id: "rabby", + name: "Rabby Wallet", + icon: , + installUrl: "https://rabby.io/", + isInstalled: () => !!findProvider(p => !!p.isRabby), + connect: async () => { + const p = findProvider(p => !!p.isRabby) ?? getEth(); + return p ? connectProvider(p) : null; + }, + }, + { + id: "safepal", + name: "SafePal", + icon: , + installUrl: "https://www.safepal.com/download", + isInstalled: () => !!(getSafePal() || findProvider(p => !!p.isSafePal)), + connect: async () => { + const p = getSafePal() ?? findProvider(p => !!p.isSafePal) ?? getEth(); + return p ? connectProvider(p) : null; + }, + }, + { + id: "imtoken", + name: "imToken", + icon: , + installUrl: "https://token.im/download", + mobileDeepLink: `imtokenv2://navigate/DAppBrowser?url=${encodeURIComponent(currentUrl)}`, + isInstalled: () => !!(getImToken() || findProvider(p => !!p.isImToken)), + connect: async () => { + const p = getImToken() ?? findProvider(p => !!p.isImToken) ?? getEth(); + return p ? connectProvider(p) : null; + }, + }, + { + id: "phantom", + name: "Phantom (EVM)", + icon: , + installUrl: "https://phantom.app/download", + isInstalled: () => !!findProvider(p => !!p.isPhantom), + connect: async () => { + const p = findProvider(p => !!p.isPhantom) ?? getEth(); + return p ? connectProvider(p) : null; }, }, ]; @@ -257,24 +356,15 @@ function isValidEthAddress(addr: string): boolean { function MobileDeepLinkPanel({ lang }: { lang: Lang }) { const currentUrl = typeof window !== "undefined" ? window.location.href : "https://pre-sale.newassetchain.io"; - const urlWithoutProtocol = currentUrl.replace(/^https?:\/\//, ""); + const urlNoProto = currentUrl.replace(/^https?:\/\//, ""); const mobileWallets = [ { id: "metamask", name: "MetaMask", icon: , - deepLink: `https://metamask.app.link/dapp/${urlWithoutProtocol}`, + deepLink: `https://metamask.app.link/dapp/${urlNoProto}`, installUrl: "https://metamask.io/download/", - color: "#E27625", - }, - { - id: "trust", - name: "Trust Wallet", - icon: , - deepLink: `https://link.trustwallet.com/open_url?coin_id=60&url=${encodeURIComponent(currentUrl)}`, - installUrl: "https://trustwallet.com/download", - color: "#3375BB", }, { id: "okx", @@ -282,7 +372,13 @@ function MobileDeepLinkPanel({ lang }: { lang: Lang }) { icon: , deepLink: `okx://wallet/dapp/url?dappUrl=${encodeURIComponent(currentUrl)}`, installUrl: "https://www.okx.com/web3", - color: "#00F0FF", + }, + { + id: "trust", + name: "Trust Wallet", + icon: , + deepLink: `https://link.trustwallet.com/open_url?coin_id=60&url=${encodeURIComponent(currentUrl)}`, + installUrl: "https://trustwallet.com/download", }, { id: "tokenpocket", @@ -290,13 +386,25 @@ function MobileDeepLinkPanel({ lang }: { lang: Lang }) { icon: , deepLink: `tpoutside://pull?param=${encodeURIComponent(JSON.stringify({ url: currentUrl }))}`, installUrl: "https://www.tokenpocket.pro/en/download/app", - color: "#2980FE", + }, + { + id: "imtoken", + name: "imToken", + icon: , + deepLink: `imtokenv2://navigate/DAppBrowser?url=${encodeURIComponent(currentUrl)}`, + installUrl: "https://token.im/download", + }, + { + id: "safepal", + name: "SafePal", + icon: , + deepLink: `safepal://browser?url=${encodeURIComponent(currentUrl)}`, + installUrl: "https://www.safepal.com/download", }, ]; return (
- {/* Mobile guidance header */}

{lang === "zh" - ? "手机浏览器不支持钱包扩展。请选择以下任一钱包 App,在其内置浏览器中打开本页面即可连接钱包。" - : "Mobile browsers don't support wallet extensions. Open this page in a wallet app's built-in browser to connect."} + ? "请选择以下钱包 App,在其内置浏览器中打开本页面即可连接钱包。" + : "Open this page in a wallet app's built-in browser to connect."}

- {/* Wallet DeepLink buttons */}

{lang === "zh" ? "选择钱包 App 打开本页面" : "Choose a wallet app to open this page"} @@ -348,7 +455,6 @@ function MobileDeepLinkPanel({ lang }: { lang: Lang }) { ))}

- {/* Step guide */}
{ setDetecting(true); setError(null); - // Wait for wallet extensions to fully inject (up to 1500ms) const timer = setTimeout(() => { setWallets(buildWallets()); setDetecting(false); @@ -401,9 +506,9 @@ export function WalletSelector({ lang, onAddressDetected, connectedAddress, comp setConnecting(wallet.id); setError(null); try { - const address = await wallet.connect(); - if (address) { - onAddressDetected(address); + const result = await wallet.connect(); + if (result) { + onAddressDetected(result.address, result.provider); } else { setError(lang === "zh" ? "未获取到地址,请重试" : "No address returned, please try again"); } @@ -436,7 +541,15 @@ export function WalletSelector({ lang, onAddressDetected, connectedAddress, comp return; } setManualError(null); - onAddressDetected(addr); + // For manual input, use window.ethereum as fallback provider + const fallbackProvider = (window as unknown as { ethereum?: EthProvider }).ethereum; + if (fallbackProvider) { + onAddressDetected(addr, fallbackProvider); + } else { + // No provider available — create a minimal stub so the callback still works + const stub = { request: async () => { throw new Error("no provider"); } } as unknown as EthProvider; + onAddressDetected(addr, stub); + } }; const installedWallets = wallets.filter(w => w.isInstalled()); @@ -469,7 +582,6 @@ export function WalletSelector({ lang, onAddressDetected, connectedAddress, comp
- {/* Manual address fallback */}
@@ -1235,6 +1237,67 @@ export default function Home() { {value}
))} + {/* Contract Address Row with Copy Button */} +
+ {t("token_contract_addr")} +
+ + {CONTRACTS.BSC.token.slice(0, 6)}...{CONTRACTS.BSC.token.slice(-4)} + + +
+
+ {/* Add XIC to Wallet — calls wallet_watchAsset to open MetaMask/OKX add-token dialog */} +
- {/* How to Buy Guide — 3-Step Cards */} -
-

{t("guide_title")}

-
- {/* Step 1 */} -
-
-
1
- {t("guide_step1_title")} -
-
    - {[t("guide_step1_1"), t("guide_step1_2"), t("guide_step1_3")].map((item, i) => ( -
  • - - {item} -
  • - ))} -
-
- {/* Step 2 */} -
-
-
2
- {t("guide_step2_title")} -
-
    - {[t("guide_step2_1"), t("guide_step2_2"), t("guide_step2_3"), t("guide_step2_4")].map((item, i) => ( -
  • - - {item} -
  • - ))} -
-
- {/* Step 3 */} -
-
-
3
- {t("guide_step3_title")} -
-
    - {[t("guide_step3_1"), t("guide_step3_2"), t("guide_step3_3"), t("guide_step3_4")].map((item, i) => ( -
  • - - {item} -
  • - ))} -
-
+ {/* How to Buy Guide — 3 Text Paragraphs */} +
+

{t("guide_title")}

+ {/* Step 1 */} +
+ {t("guide_step1_title")}: + {t("guide_step1_1")},{t("guide_step1_2")},{t("guide_step1_3")}。 +
+ {/* Step 2 */} +
+ {t("guide_step2_title")}: + {t("guide_step2_1")},{t("guide_step2_2")},{t("guide_step2_3")},{t("guide_step2_4")}。 +
+ {/* Step 3 */} +
+ {t("guide_step3_title")}: + {t("guide_step3_1")},{t("guide_step3_2")},{t("guide_step3_3")},{t("guide_step3_4")}。