"use client"; import { useCallback, useEffect, useMemo, useState } from "react"; import Link from "next/link"; import { useRouter } from "next/navigation"; import type { User } from "@supabase/supabase-js"; import type { LucideIcon } from "lucide-react"; import { ArrowRight, BadgeCheck, Bot, CheckCircle2, ChevronLeft, Clock, Coins, Copy, Crown, CreditCard, Fingerprint, Hash, Info, Loader2, Lock, LogIn, LogOut, Mail, RefreshCw, Shield, ShieldCheck, Sparkles, Trophy, User as UserIcon, UserCheck, Wallet, X, } from "lucide-react"; import { getSupabaseBrowserClient, hasSupabasePublicEnv } from "@/lib/supabase/client"; type AuthMeResponse = { authenticated?: boolean; user_id?: string | null; email?: string | null; entitlement_mode?: string | null; auth_required?: boolean; subscription_required?: boolean; subscription_active?: boolean | null; }; type PaymentPlan = { plan_code: string; plan_id: number; amount_usdc: string; duration_days: number; }; type PaymentConfig = { enabled?: boolean; configured?: boolean; chain_id?: number; token_address?: string; token_decimals?: number; receiver_contract?: string; confirmations?: number; plans?: PaymentPlan[]; }; type BoundWallet = { chain_id: number; address: string; status: string; is_primary: boolean; verified_at?: string | null; }; type CreatedIntent = { intent?: { intent_id: string; order_id_hex: string; plan_code: string; amount_usdc: string; allowed_wallet?: string | null; }; tx_payload?: { chain_id: number; to: string; data: string; value: string; amount_units: string; token_address: string; }; }; declare global { interface Window { ethereum?: { request: (args: { method: string; params?: any[] | object }) => Promise; }; } } type InfoItemProps = { icon: LucideIcon; label: string; value: string; status?: "default" | "primary"; }; function formatTime(value: string | undefined | null, locale: string) { if (!value) return "--"; try { const dt = new Date(value); if (Number.isNaN(dt.getTime())) return "--"; return new Intl.DateTimeFormat(locale, { year: "numeric", month: "2-digit", day: "2-digit", hour: "2-digit", minute: "2-digit", }).format(dt); } catch { return "--"; } } function normalizeProvider(user: User | null) { const provider = String(user?.app_metadata?.provider || "").trim().toLowerCase(); if (provider) return provider; const providers = user?.app_metadata?.providers; if (Array.isArray(providers) && providers.length) { return String(providers[0] || "").trim().toLowerCase(); } return ""; } function shortAddress(address: string) { const text = String(address || ""); if (!text.startsWith("0x") || text.length < 12) return text || "--"; return `${text.slice(0, 8)}...${text.slice(-6)}`; } function planDisplayName(planCode: string) { const code = String(planCode || "").trim().toLowerCase(); if (code === "pro_monthly") return "Pro 月付"; if (code === "pro_quarterly") return "Pro 季付"; if (code === "pro_yearly") return "Pro 年付"; return planCode || "--"; } function toPaddedHex(value: bigint) { return value.toString(16).padStart(64, "0"); } function toPaddedAddress(address: string) { return String(address || "").toLowerCase().replace(/^0x/, "").padStart(64, "0"); } function buildAllowanceCalldata(owner: string, spender: string) { return `0xdd62ed3e${toPaddedAddress(owner)}${toPaddedAddress(spender)}`; } function buildApproveCalldata(spender: string, amount: bigint) { return `0x095ea7b3${toPaddedAddress(spender)}${toPaddedHex(amount)}`; } function InfoItem({ icon: Icon, label, value, status = "default" }: InfoItemProps) { return (
{label}
{value}
); } export function AccountCenter() { const router = useRouter(); const [loading, setLoading] = useState(true); const [refreshing, setRefreshing] = useState(false); const [errorText, setErrorText] = useState(""); const [copied, setCopied] = useState(false); const [showOverlay, setShowOverlay] = useState(false); const [usePoints, setUsePoints] = useState(true); const [updatedAt, setUpdatedAt] = useState(""); const [user, setUser] = useState(null); const [backend, setBackend] = useState(null); const [paymentConfig, setPaymentConfig] = useState(null); const [boundWallets, setBoundWallets] = useState([]); const [walletAddress, setWalletAddress] = useState(""); const [selectedPlanCode, setSelectedPlanCode] = useState("pro_monthly"); const [selectedWallet, setSelectedWallet] = useState(""); const [paymentBusy, setPaymentBusy] = useState(false); const [paymentInfo, setPaymentInfo] = useState(""); const [paymentError, setPaymentError] = useState(""); const [lastIntentId, setLastIntentId] = useState(""); const [lastTxHash, setLastTxHash] = useState(""); const supabaseReady = hasSupabasePublicEnv(); const buildAuthedHeaders = useCallback(async (withJson = false): Promise> => { const headers: Record = {}; if (withJson) headers["Content-Type"] = "application/json"; if (!supabaseReady) return headers; try { const { data: { session } } = await getSupabaseBrowserClient().auth.getSession(); const accessToken = String(session?.access_token || "").trim(); if (accessToken) headers.Authorization = `Bearer ${accessToken}`; } catch { // no-op } return headers; }, [supabaseReady]); const loadPaymentSnapshot = useCallback(async () => { if (!backend?.authenticated) { setPaymentConfig(null); setBoundWallets([]); return; } try { const authHeaders = await buildAuthedHeaders(false); const [configRes, walletsRes] = await Promise.all([ fetch("/api/payments/config", { cache: "no-store", headers: authHeaders }), fetch("/api/payments/wallets", { cache: "no-store", headers: authHeaders }), ]); if (configRes.ok) { const configJson = (await configRes.json()) as PaymentConfig; setPaymentConfig(configJson); if (!selectedPlanCode && configJson.plans?.length) { setSelectedPlanCode(configJson.plans[0].plan_code); } } if (walletsRes.ok) { const walletsJson = (await walletsRes.json()) as { wallets?: BoundWallet[] }; const wallets = Array.isArray(walletsJson.wallets) ? walletsJson.wallets : []; setBoundWallets(wallets); if (wallets.length && !selectedWallet) setSelectedWallet(wallets[0].address); } if (configRes.status === 401 || walletsRes.status === 401) { setPaymentError("登录会话已过期,请重新登录后再进行钱包绑定或支付。"); } } catch { // ignore } }, [backend?.authenticated, buildAuthedHeaders, selectedPlanCode, selectedWallet]); const loadSnapshot = useCallback(async () => { setErrorText(""); try { const userPromise = supabaseReady ? getSupabaseBrowserClient().auth.getUser() : Promise.resolve({ data: { user: null as User | null } }); const authHeaders = await buildAuthedHeaders(false); const backendPromise = fetch("/api/auth/me", { cache: "no-store", headers: authHeaders }); const [userResult, backendResult] = await Promise.all([userPromise, backendPromise]); setUser(userResult.data?.user ?? null); if (!backendResult.ok) { const raw = (await backendResult.text()).slice(0, 260); throw new Error(`HTTP ${backendResult.status} ${raw}`.trim()); } const backendJson = (await backendResult.json()) as AuthMeResponse; setBackend(backendJson); setUpdatedAt(new Date().toISOString()); } catch (error) { setErrorText(String(error)); } }, [buildAuthedHeaders, supabaseReady]); useEffect(() => { let cancelled = false; const run = async () => { setLoading(true); await loadSnapshot(); if (!cancelled) setLoading(false); }; void run(); return () => { cancelled = true; }; }, [loadSnapshot]); useEffect(() => { void loadPaymentSnapshot(); }, [loadPaymentSnapshot]); const onRefresh = async () => { setRefreshing(true); await loadSnapshot(); await loadPaymentSnapshot(); setRefreshing(false); }; const onSignOut = async () => { if (supabaseReady) { try { await getSupabaseBrowserClient().auth.signOut(); } catch { // ignore } } router.replace("/"); }; const userId = backend?.user_id || user?.id || ""; const isAuthenticated = Boolean(userId); const email = backend?.email || user?.email || ""; const displayName = String(user?.user_metadata?.full_name || "").trim() || (email ? String(email).split("@")[0] : "") || "PolyWeather 用户"; const initials = (displayName.slice(0, 2) || "PW").toUpperCase(); const providerRaw = normalizeProvider(user); const provider = providerRaw ? providerRaw.toUpperCase() : "--"; const locale = typeof navigator !== "undefined" ? navigator.language : "zh-CN"; const lastSignIn = formatTime(user?.last_sign_in_at, locale); const joinedAt = formatTime(user?.created_at, locale); const updatedAtLabel = formatTime(updatedAt, locale); const modeLabel = useMemo(() => { const mode = String(backend?.entitlement_mode || "").trim().toLowerCase(); if (mode === "supabase_required") return "Supabase 强制登录"; if (mode === "supabase_optional") return "Supabase 可选登录"; if (mode === "legacy_token") return "Legacy Token 鉴权"; if (mode === "disabled") return "未启用鉴权"; return "未知模式"; }, [backend?.entitlement_mode]); const backendStatus = useMemo(() => { if (backend?.authenticated) return "通过"; if (backend?.auth_required) return "未登录"; return "游客模式"; }, [backend?.authenticated, backend?.auth_required]); const subscriptionRequirement = backend?.subscription_required ? "已启用订阅校验" : "当前未强制订阅"; const subscriptionResult = !backend?.subscription_required ? "当前未强制订阅" : backend?.subscription_active ? "有效订阅" : "无有效订阅"; const isSubscribed = Boolean(backend?.subscription_active); const bindCommand = userId ? `/bind ${userId}${email ? ` ${email}` : ""}` : "/bind "; const copyBindCommand = async () => { try { await navigator.clipboard.writeText(bindCommand); setCopied(true); window.setTimeout(() => setCopied(false), 2000); } catch { // ignore } }; const planList = paymentConfig?.plans || []; const monthlyPlanList = planList.filter((plan) => String(plan.plan_code || "").trim().toLowerCase() === "pro_monthly"); const effectivePlanList = monthlyPlanList.length ? monthlyPlanList : planList; const selectedPlan = effectivePlanList.find((plan) => plan.plan_code === selectedPlanCode) || effectivePlanList[0]; const paymentFeatureReady = Boolean(paymentConfig?.enabled && paymentConfig?.configured); const hasPayingWallet = Boolean(String(selectedWallet || walletAddress || boundWallets[0]?.address || "").trim()); const pointsRaw = Number(user?.user_metadata?.points ?? user?.user_metadata?.total_points ?? 0); const weeklyPointsRaw = Number(user?.user_metadata?.weekly_points ?? 0); const weeklyRankRaw = user?.user_metadata?.weekly_rank; const totalPoints = Number.isFinite(pointsRaw) ? Math.max(0, pointsRaw) : 0; const weeklyPoints = Number.isFinite(weeklyPointsRaw) ? Math.max(0, weeklyPointsRaw) : 0; const weeklyRank = weeklyRankRaw == null ? "--" : String(weeklyRankRaw); const billing = useMemo(() => { const price = Number(selectedPlan?.amount_usdc || 5); const pointsPerUsdc = 500; const maxDiscount = 3; const pointsUsed = Math.min(totalPoints, pointsPerUsdc * maxDiscount); const discountAmount = usePoints ? Math.min(maxDiscount, pointsUsed / pointsPerUsdc) : 0; return { price, pointsUsed, discountAmount, payAmount: Math.max(0, price - discountAmount) }; }, [selectedPlan?.amount_usdc, totalPoints, usePoints]); useEffect(() => { if (isSubscribed) setShowOverlay(false); }, [isSubscribed]); const waitForReceipt = async (txHash: string, timeoutMs = 120000, pollMs = 3000) => { const eth = window.ethereum; if (!eth) throw new Error("MetaMask not found"); const started = Date.now(); while (Date.now() - started < timeoutMs) { const receipt = (await eth.request({ method: "eth_getTransactionReceipt", params: [txHash] })) as { status?: string } | null; if (receipt && receipt.status) { if (receipt.status === "0x1") return receipt; throw new Error(`transaction reverted: ${txHash}`); } await new Promise((resolve) => setTimeout(resolve, pollMs)); } throw new Error(`transaction confirmation timeout: ${txHash}`); }; const connectAndBindWallet = async () => { setPaymentError(""); setPaymentInfo(""); if (!isAuthenticated) { setPaymentError("请先登录后再绑定钱包。"); return; } const eth = window.ethereum; if (!eth) { setPaymentError("未检测到 MetaMask,请先安装扩展。"); return; } setPaymentBusy(true); try { const accounts = (await eth.request({ method: "eth_requestAccounts" })) as string[]; const address = String(accounts?.[0] || "").toLowerCase(); if (!address) throw new Error("钱包账户为空"); const authHeaders = await buildAuthedHeaders(true); if (!authHeaders.Authorization) throw new Error("登录会话失效,请重新登录后再绑定钱包。"); setWalletAddress(address); const challengeRes = await fetch("/api/payments/wallets/challenge", { method: "POST", headers: authHeaders, body: JSON.stringify({ address }), }); if (!challengeRes.ok) { const raw = (await challengeRes.text()).slice(0, 300); throw new Error(`challenge failed: ${raw}`); } const challengeJson = (await challengeRes.json()) as { nonce?: string; message?: string }; const message = String(challengeJson.message || ""); const nonce = String(challengeJson.nonce || ""); if (!message || !nonce) throw new Error("challenge payload invalid"); const signature = (await eth.request({ method: "personal_sign", params: [message, address] })) as string; const verifyRes = await fetch("/api/payments/wallets/verify", { method: "POST", headers: authHeaders, body: JSON.stringify({ address, nonce, signature }), }); if (!verifyRes.ok) { const raw = (await verifyRes.text()).slice(0, 300); throw new Error(`verify failed: ${raw}`); } setPaymentInfo(`钱包绑定成功: ${shortAddress(address)}`); await loadPaymentSnapshot(); } catch (error) { setPaymentError(String(error)); } finally { setPaymentBusy(false); } }; const createIntentAndPay = async () => { setPaymentError(""); setPaymentInfo(""); if (!isAuthenticated) { setPaymentError("请先登录后再支付。"); return; } if (!paymentConfig?.configured) { setPaymentError("支付服务未配置完成。"); return; } const eth = window.ethereum; if (!eth) { setPaymentError("未检测到 MetaMask。"); return; } const payingWallet = String(selectedWallet || walletAddress || boundWallets[0]?.address || "").toLowerCase(); if (!payingWallet) { setPaymentError("请先绑定钱包。"); return; } setPaymentBusy(true); try { const authHeaders = await buildAuthedHeaders(true); if (!authHeaders.Authorization) throw new Error("登录会话失效,请重新登录后再支付。"); const currentChainIdHex = String((await eth.request({ method: "eth_chainId" })) || ""); const targetChainId = Number(paymentConfig.chain_id || 137); const targetChainHex = `0x${targetChainId.toString(16)}`; if (currentChainIdHex.toLowerCase() !== targetChainHex.toLowerCase()) { await eth.request({ method: "wallet_switchEthereumChain", params: [{ chainId: targetChainHex }] }); } const createRes = await fetch("/api/payments/intents", { method: "POST", headers: authHeaders, body: JSON.stringify({ plan_code: selectedPlan?.plan_code || "pro_monthly", payment_mode: "strict", allowed_wallet: payingWallet, metadata: { source: "account_center" }, }), }); if (!createRes.ok) { const raw = (await createRes.text()).slice(0, 350); throw new Error(`create intent failed: ${raw}`); } const created = (await createRes.json()) as CreatedIntent; const intentId = String(created.intent?.intent_id || ""); const txPayload = created.tx_payload; if (!intentId || !txPayload?.to || !txPayload?.data) throw new Error("intent payload invalid"); setLastIntentId(intentId); const tokenAddress = String(txPayload.token_address || "").toLowerCase(); const amountUnits = BigInt(String(txPayload.amount_units || "0")); if (!tokenAddress.startsWith("0x") || amountUnits <= 0n) throw new Error("intent token/amount invalid"); const allowanceHex = (await eth.request({ method: "eth_call", params: [{ to: tokenAddress, data: buildAllowanceCalldata(payingWallet, txPayload.to) }, "latest"], })) as string; const allowance = BigInt(String(allowanceHex || "0x0")); if (allowance < amountUnits) { setPaymentInfo("检测到授权不足,正在发起 USDC 授权..."); const approveHash = (await eth.request({ method: "eth_sendTransaction", params: [{ from: payingWallet, to: tokenAddress, data: buildApproveCalldata(txPayload.to, amountUnits), value: "0x0" }], })) as string; await waitForReceipt(String(approveHash || "")); setPaymentInfo("USDC 授权成功,正在发起支付..."); } else { setPaymentInfo("授权额度充足,正在发起支付..."); } const txHash = (await eth.request({ method: "eth_sendTransaction", params: [{ from: payingWallet, to: txPayload.to, data: txPayload.data, value: txPayload.value || "0x0" }], })) as string; const txHashNorm = String(txHash || "").toLowerCase(); setLastTxHash(txHashNorm); const submitRes = await fetch(`/api/payments/intents/${intentId}/submit`, { method: "POST", headers: authHeaders, body: JSON.stringify({ tx_hash: txHashNorm, from_address: payingWallet }), }); if (!submitRes.ok) { const raw = (await submitRes.text()).slice(0, 350); throw new Error(`submit tx failed: ${raw}`); } const confirmRes = await fetch(`/api/payments/intents/${intentId}/confirm`, { method: "POST", headers: authHeaders, body: JSON.stringify({ tx_hash: txHashNorm }), }); if (!confirmRes.ok) { const raw = (await confirmRes.text()).slice(0, 350); setPaymentInfo(`交易已提交: ${shortAddress(txHashNorm)},等待确认中。`); throw new Error(`confirm pending: ${raw}`); } setPaymentInfo(`支付确认成功,交易: ${shortAddress(txHashNorm)}`); await loadSnapshot(); await loadPaymentSnapshot(); } catch (error) { setPaymentError(String(error)); } finally { setPaymentBusy(false); } }; const handleOverlayCheckout = async () => { if (!isAuthenticated) { router.push("/auth/login?next=%2Faccount"); return; } if (!paymentFeatureReady) { setPaymentError("支付服务未配置完成,请稍后再试。"); return; } if (!hasPayingWallet) { await connectAndBindWallet(); return; } await createIntentAndPay(); }; return (

账户中心

积分体系 v4.3 · 管理身份、订阅与 Bot 绑定

{!isSubscribed && paymentFeatureReady ? ( ) : null} {isAuthenticated ? ( ) : ( 登录 / 注册 )}
{errorText ?
加载失败: {errorText}
: null} {!supabaseReady ?
NEXT_PUBLIC_SUPABASE_URL / NEXT_PUBLIC_SUPABASE_ANON_KEY 未配置。
: null}
最近同步: {updatedAtLabel}
{initials}

{displayName}

{isSubscribed ? "PRO MEMBER" : "FREE TIER"} {isAuthenticated ? "已登录" : "游客"}

{email || "--"}

{userId ? `${userId.slice(0, 12)}...` : "--"} 加入时间: {joinedAt}

总积分

{totalPoints.toLocaleString("en-US")}

周排行

#{weeklyRank}

本周积分: {weeklyPoints}

周榜奖励

Top 1+500 pts & 7D Pro
Top 2-3+300 pts & 3D Pro
Top 4-10+150 pts

周榜每周一结算。发言积分永久累计,可用于订阅抵扣。

会员与权限

身份信息

已绑定钱包

{!isAuthenticated ? (

请先登录,再进行钱包绑定与支付。

) : boundWallets.length ? (
{boundWallets.map((wallet) => { const active = String(selectedWallet || walletAddress).toLowerCase() === String(wallet.address || "").toLowerCase(); return ( ); })}
) : (

暂无已绑定钱包。

)}

选择套餐并支付

Chain #{paymentConfig?.chain_id ?? 137}
{paymentFeatureReady ? ( <>
{effectivePlanList.length ? ( effectivePlanList.map((plan) => { const checked = (selectedPlan?.plan_code || "") === plan.plan_code; return ( ); }) ) : (
套餐配置为空,请检查后端计划配置。
)}
积分抵扣
最多可抵扣 $3(500 分 = $1) 消耗积分: {usePoints ? billing.pointsUsed : 0}
应付金额 ${billing.payAmount.toFixed(2)}
) : (
支付功能暂未启用或未配置完成。
)} {paymentInfo ?
{paymentInfo}
: null} {paymentError ?
{paymentError}
: null} {lastIntentId ?
Intent: {lastIntentId}
: null} {lastTxHash ?
Tx: {shortAddress(lastTxHash)}
: null}
{!isSubscribed && showOverlay ? (

开启 PolyWeather Pro

解锁今日日内分析、历史对账、未来日期分析等 Pro 功能。

今日日内分析
历史对账
未来日期分析
无广告体验
月付套餐 USD {selectedPlan?.amount_usdc || "5"} / 30天
积分抵扣 -USD {billing.discountAmount.toFixed(2)}
应付总计 USD {billing.payAmount.toFixed(2)}
所有交易均在链上确认并加密传输
) : null}

Bot 绑定

发送以下命令到 Telegram Bot,绑定网页身份并同步权限状态。

{bindCommand}
); }