diff --git a/frontend/components/account/AccountCenter.tsx b/frontend/components/account/AccountCenter.tsx index a104e54b..0a1bcaf0 100644 --- a/frontend/components/account/AccountCenter.tsx +++ b/frontend/components/account/AccountCenter.tsx @@ -183,6 +183,18 @@ type InjectedProviderOption = ProviderSelection & { key: string; }; +type Eip6963ProviderInfo = { + uuid: string; + name: string; + icon: string; + rdns: string; +}; + +type Eip6963ProviderDetail = { + info: Eip6963ProviderInfo; + provider: EvmProvider; +}; + type ConnectBindOptions = { openOverlayAfterBind?: boolean; }; @@ -205,6 +217,7 @@ const SUBSCRIPTION_HELP_HREF = "/subscription-help"; let walletConnectProviderCache: EvmProvider | null = null; let walletConnectProviderChainId: number | null = null; +const eip6963Providers = new Map(); function isWalletConnectResetError(error: unknown): boolean { const source = error as any; @@ -293,6 +306,50 @@ function getEvmProvider(): EvmProvider | null { return listInjectedProviders()[0]?.provider || null; } +function getEip6963Providers(): Eip6963ProviderDetail[] { + return Array.from(eip6963Providers.values()); +} + +function detectWalletLabel( + provider: EvmProvider | null, + detail?: Eip6963ProviderDetail, +): string { + if (!provider && !detail) return "EVM 钱包"; + const announcedName = String(detail?.info?.name || "").trim(); + const announcedRdns = String(detail?.info?.rdns || "").toLowerCase(); + if ( + provider?.isOkxWallet || + announcedName.toLowerCase().includes("okx") || + announcedRdns.includes("okx") + ) { + return "OKX Wallet"; + } + if ( + provider?.isMetaMask || + announcedName.toLowerCase().includes("metamask") || + announcedRdns.includes("metamask") + ) { + return "MetaMask"; + } + if ( + provider?.isRabby || + announcedName.toLowerCase().includes("rabby") || + announcedRdns.includes("rabby") + ) { + return "Rabby"; + } + if ( + provider?.isBitKeep || + announcedName.toLowerCase().includes("bitget") || + announcedRdns.includes("bitkeep") || + announcedRdns.includes("bitget") + ) { + return "Bitget Wallet"; + } + if (announcedName) return announcedName; + return "EVM 钱包"; +} + function collectInjectedProviders(): EvmProvider[] { if (typeof window === "undefined") return []; const out: EvmProvider[] = []; @@ -320,21 +377,46 @@ function collectInjectedProviders(): EvmProvider[] { return out; } -function getInjectedProviderStableId(provider: EvmProvider, index: number): string { - if (provider.isOkxWallet) return `okx:${index}`; - if (provider.isMetaMask) return `metamask:${index}`; - if (provider.isRabby) return `rabby:${index}`; - if (provider.isBitKeep) return `bitget:${index}`; +function getInjectedProviderStableId( + provider: EvmProvider, + index: number, + detail?: Eip6963ProviderDetail, +): string { + const uuid = String(detail?.info?.uuid || "").trim(); + const rdns = String(detail?.info?.rdns || "").toLowerCase(); + if (uuid) return `eip6963:${uuid}`; + if (provider.isOkxWallet || rdns.includes("okx")) return `okx:${index}`; + if (provider.isMetaMask || rdns.includes("metamask")) return `metamask:${index}`; + if (provider.isRabby || rdns.includes("rabby")) return `rabby:${index}`; + if ( + provider.isBitKeep || + rdns.includes("bitkeep") || + rdns.includes("bitget") + ) { + return `bitget:${index}`; + } return `evm:${index}`; } function listInjectedProviders(): InjectedProviderOption[] { + const detailByProvider = new Map(); + getEip6963Providers().forEach((detail) => { + if (detail?.provider && typeof detail.provider.request === "function") { + detailByProvider.set(detail.provider, detail); + } + }); const candidates = collectInjectedProviders(); + detailByProvider.forEach((_detail, provider) => { + if (!candidates.includes(provider)) { + candidates.push(provider); + } + }); const seen = new Set(); const out: InjectedProviderOption[] = []; candidates.forEach((provider, index) => { - const label = getEvmWalletLabel(provider); - const key = getInjectedProviderStableId(provider, index); + const detail = detailByProvider.get(provider); + const label = detectWalletLabel(provider, detail); + const key = getInjectedProviderStableId(provider, index, detail); if (seen.has(key)) return; seen.add(key); out.push({ @@ -348,12 +430,7 @@ function listInjectedProviders(): InjectedProviderOption[] { } function getEvmWalletLabel(provider: EvmProvider | null): string { - if (!provider) return "EVM 钱包"; - if (provider.isMetaMask) return "MetaMask"; - if (provider.isRabby) return "Rabby"; - if (provider.isOkxWallet) return "OKX Wallet"; - if (provider.isBitKeep) return "Bitget Wallet"; - return "EVM 钱包"; + return detectWalletLabel(provider); } async function getWalletConnectProvider( @@ -476,7 +553,10 @@ function normalizePaymentError(error: unknown): NormalizedPaymentError { ?.trim(); const lower = String(rawMessage || "").toLowerCase(); - if (lower.includes("confirm pending") || lower.includes("payment pending timeout")) { + if ( + lower.includes("confirm pending") || + lower.includes("payment pending timeout") + ) { return { message: "链上交易已提交,正在确认中,请稍后刷新查看状态。", pending: true, @@ -569,12 +649,15 @@ export function AccountCenter() { authMode: isEn ? "Auth Mode" : "鉴权模式", weatherEngine: isEn ? "Weather Engine" : "气象引擎", intradayAnalysis: isEn ? "Intraday Analysis" : "今日内分析", - historyFuture: - isEn ? "Historical + Future-date Analysis" : "历史对账 + 未来日期分析", - smartPush: - isEn ? "Cross-platform Smart Weather Push" : "全平台智能气象推送", - deepMode: - isEn ? "Deep mode (incl. high-temp window)" : "深度版(含高温时段)", + historyFuture: isEn + ? "Historical + Future-date Analysis" + : "历史对账 + 未来日期分析", + smartPush: isEn + ? "Cross-platform Smart Weather Push" + : "全平台智能气象查询", + deepMode: isEn + ? "Deep mode (incl. high-temp window)" + : "深度版(含高温时段)", compactVisible: isEn ? "Compact visible" : "简版可见", enabled: isEn ? "Enabled" : "已开启", locked: isEn ? "Locked" : "锁定", @@ -587,7 +670,7 @@ export function AccountCenter() { telegramBind: isEn ? "Telegram Bot Binding" : "Telegram Bot 绑定", telegramHint: isEn ? "Send the command below to the polyweather bot to sync notifications and access." - : "将下方命令发送给polyweather机器人,实现全平台气象推送与权限同步。", + : "将下方命令发送给polyweather机器人,实现全平台气象查询与权限同步。", telegramBotLink: isEn ? "Open Bot (@WeatherQuant_bot)" : "打开机器人 (@WeatherQuant_bot)", @@ -598,9 +681,12 @@ export function AccountCenter() { primary: "Primary", polygonChain: "Polygon Chain", noWallet: isEn ? "No payout wallet bound yet." : "未绑定任何收件钱包", - bindExt: - isEn ? "Bind Browser Wallet (EVM Extension)" : "绑定浏览器钱包(EVM扩展)", - bindQr: isEn ? "Bind via QR (WalletConnect)" : "扫码绑定(WalletConnect)", + bindExt: isEn + ? "Bind Browser Wallet (EVM Extension)" + : "绑定浏览器钱包(EVM扩展)", + bindQr: isEn + ? "Bind via QR (WalletConnect)" + : "扫码绑定(WalletConnect)", walletConnectMissing: isEn ? "WalletConnect disabled: please configure" : "未启用 WalletConnect:请配置", @@ -720,7 +806,11 @@ export function AccountCenter() { return () => { canceled = true; - if (win && idleId != null && typeof win.cancelIdleCallback === "function") { + if ( + win && + idleId != null && + typeof win.cancelIdleCallback === "function" + ) { win.cancelIdleCallback(idleId); } if (timeoutId != null && typeof window !== "undefined") { @@ -740,12 +830,40 @@ export function AccountCenter() { return nextOptions[0]?.key || ""; }); }; + + const handleAnnounce = (event: Event) => { + const customEvent = event as CustomEvent; + const detail = customEvent.detail; + if (!detail?.provider || typeof detail.provider.request !== "function") { + return; + } + const uuid = String(detail.info?.uuid || "").trim(); + const fallbackKey = `${String(detail.info?.rdns || "wallet").toLowerCase()}:${String( + detail.info?.name || "wallet", + ).toLowerCase()}`; + eip6963Providers.set(uuid || fallbackKey, detail); + syncProviders(); + }; + syncProviders(); if (typeof window === "undefined") return; - window.addEventListener("ethereum#initialized", syncProviders as EventListener, { - once: false, - }); + window.addEventListener( + "eip6963:announceProvider", + handleAnnounce as EventListener, + ); + window.dispatchEvent(new Event("eip6963:requestProvider")); + window.addEventListener( + "ethereum#initialized", + syncProviders as EventListener, + { + once: false, + }, + ); return () => { + window.removeEventListener( + "eip6963:announceProvider", + handleAnnounce as EventListener, + ); window.removeEventListener( "ethereum#initialized", syncProviders as EventListener, @@ -790,11 +908,7 @@ export function AccountCenter() { } = await client.auth.refreshSession(); const refreshedToken = String(refreshed?.access_token || "").trim(); if (refreshedToken) return refreshedToken; - if ( - cachedToken && - Number.isFinite(expiresAtSec) && - expiresAtSec > nowSec - ) { + if (cachedToken && Number.isFinite(expiresAtSec) && expiresAtSec > nowSec) { return cachedToken; } throw new Error( @@ -852,8 +966,8 @@ export function AccountCenter() { injectedOptions.find((row) => row.key === preferredInjectedKey) ?.provider || getEvmProvider(); const label = - injectedOptions.find((row) => row.key === preferredInjectedKey)?.label || - getEvmWalletLabel(injected); + injectedOptions.find((row) => row.key === preferredInjectedKey) + ?.label || getEvmWalletLabel(injected); if (injected) { return { provider: injected, @@ -971,7 +1085,8 @@ export function AccountCenter() { if (wallets.length) { const currentSelected = String(selectedWallet || "").toLowerCase(); const hasCurrent = wallets.some( - (row) => String(row.address || "").toLowerCase() === currentSelected, + (row) => + String(row.address || "").toLowerCase() === currentSelected, ); const fallback = wallets.find((row) => Boolean(row.is_primary))?.address || @@ -979,7 +1094,9 @@ export function AccountCenter() { if (!currentSelected || !hasCurrent) { setSelectedWallet(fallback); } - const currentWalletAddress = String(walletAddress || "").toLowerCase(); + const currentWalletAddress = String( + walletAddress || "", + ).toLowerCase(); const hasWalletAddress = wallets.some( (row) => String(row.address || "").toLowerCase() === currentWalletAddress, @@ -1053,7 +1170,10 @@ export function AccountCenter() { useEffect(() => { if (typeof window === "undefined") return; if (!lastIntentId) return; - window.sessionStorage.setItem("polyweather:lastPaymentIntentId", lastIntentId); + window.sessionStorage.setItem( + "polyweather:lastPaymentIntentId", + lastIntentId, + ); }, [lastIntentId]); useEffect(() => { @@ -1405,7 +1525,11 @@ export function AccountCenter() { await loadPaymentSnapshot(); return; } - if (status === "failed" || status === "cancelled" || status === "expired") { + if ( + status === "failed" || + status === "cancelled" || + status === "expired" + ) { throw new Error(`payment ${status}`); } setPaymentInfo( @@ -1508,7 +1632,8 @@ export function AccountCenter() { method: "eth_requestAccounts", })) as string[]; const address = String(accounts?.[0] || "").toLowerCase(); - if (!address) throw new Error(isEn ? "Wallet account is empty." : "钱包账户为空"); + if (!address) + throw new Error(isEn ? "Wallet account is empty." : "钱包账户为空"); const existingWallet = boundWallets.find( (w) => String(w.address || "").toLowerCase() === address, @@ -1637,7 +1762,10 @@ export function AccountCenter() { await loadPaymentSnapshot(); setPaymentInfo( newPrimary - ? copy.unbindDonePrimary.replace("{address}", shortAddress(newPrimary)) + ? copy.unbindDonePrimary.replace( + "{address}", + shortAddress(newPrimary), + ) : copy.unbindDone, ); } catch (error) { @@ -1690,7 +1818,8 @@ export function AccountCenter() { method: "eth_requestAccounts", })) as string[]; const activeAddress = String(activeAccounts?.[0] || "").toLowerCase(); - if (!activeAddress) throw new Error(isEn ? "Wallet account is empty." : "钱包账户为空"); + if (!activeAddress) + throw new Error(isEn ? "Wallet account is empty." : "钱包账户为空"); const boundAddrSet = new Set( boundWallets.map((row) => String(row.address || "").toLowerCase()), @@ -2132,8 +2261,16 @@ export function AccountCenter() {

{copy.membershipDetails}

- - + + {copy.identityStatus} - + void handleOverlayCheckout()} onClose={() => setShowOverlay(false)} payBusy={paymentBusy} - payLabel={ - hasPayingWallet ? copy.payNow : copy.connectAndPay - } + payLabel={hasPayingWallet ? copy.payNow : copy.connectAndPay} errorText={paymentError || undefined} infoText={paymentInfo || undefined} txHash={lastTxHash || undefined} @@ -2214,207 +2353,209 @@ export function AccountCenter() { {/* Telegram Bot Section */} {showSecondarySections ? ( -
-
- -
-

- {copy.telegramBind} -

-

- {copy.telegramHint} -

-
- {TELEGRAM_BOT_URL ? ( - - {copy.telegramBotLink} - - - ) : null} - {TELEGRAM_GROUP_URL ? ( - - {copy.telegramGroupLink} - - - ) : null} -
-
- - {bindCommand} - - -
-
-
- - {/* Payment Details / Wallet Management */} -
-
-

- {copy.paymentMgmt} -

- {paymentError ? ( -
- {paymentError} -
- ) : null} - {!paymentError && paymentInfo ? ( -
- {paymentInfo} -
- ) : null} - {availableTokenList.length > 0 && ( -
-

- {copy.paymentToken} -

-
- {availableTokenList.map((token) => { - const active = - token.address === - (resolvedSelectedTokenAddress || token.address); - return ( - - ); - })} -
-
- )} - {boundWallets.length ? ( -
- {boundWallets.map((w) => ( -
+
+ +
+

+ {copy.telegramBind} +

+

+ {copy.telegramHint} +

+
+ {TELEGRAM_BOT_URL ? ( + -
- - {shortAddress(w.address)} - - {w.is_primary && ( - - {copy.primary} - - )} -
-
{copy.polygonChain}
-
- -
-
- ))} + {copy.telegramBotLink} + + + ) : null} + {TELEGRAM_GROUP_URL ? ( + + {copy.telegramGroupLink} + + + ) : null}
- ) : ( -

- {copy.noWallet} -

- )} -
- -
- {injectedProviderOptions.length > 1 && ( - - )} - - - {!walletConnectEnabled && ( -

- {copy.walletConnectMissing} - - NEXT_PUBLIC_WALLETCONNECT_PROJECT_ID +

+ + {bindCommand} -

- )} -
-
-
+ + + + + + {/* Payment Details / Wallet Management */} +
+
+

+ {copy.paymentMgmt} +

+ {paymentError ? ( +
+ {paymentError} +
+ ) : null} + {!paymentError && paymentInfo ? ( +
+ {paymentInfo} +
+ ) : null} + {availableTokenList.length > 0 && ( +
+

+ {copy.paymentToken} +

+
+ {availableTokenList.map((token) => { + const active = + token.address === + (resolvedSelectedTokenAddress || token.address); + return ( + + ); + })} +
+
+ )} + {boundWallets.length ? ( +
+ {boundWallets.map((w) => ( +
+
+ + {shortAddress(w.address)} + + {w.is_primary && ( + + {copy.primary} + + )} +
+
{copy.polygonChain}
+
+ +
+
+ ))} +
+ ) : ( +

+ {copy.noWallet} +

+ )} +
+ +
+ {injectedProviderOptions.length > 1 && ( + + )} + + + {!walletConnectEnabled && ( +

+ {copy.walletConnectMissing} + + NEXT_PUBLIC_WALLETCONNECT_PROJECT_ID + +

+ )} +
+
+ ) : (