"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 { User as UserIcon, Shield, Fingerprint, Bot, RefreshCw, LogOut, ChevronLeft, Copy, CheckCircle2, UserCheck, Mail, Hash, LogIn, Clock, Crown, ExternalLink, Trophy, Coins, TrendingUp, Info, Wallet, Zap, Minus, ShieldCheck, BarChart3, Sparkles, ChevronRight, Loader2, CreditCard, } from "lucide-react"; import { getSupabaseBrowserClient, hasSupabasePublicEnv, } from "@/lib/supabase/client"; import { markAnalyticsOnce, trackAppEvent } from "@/lib/app-analytics"; import { useI18n } from "@/hooks/useI18n"; import { UnlockProOverlay } from "@/components/subscription/UnlockProOverlay"; import type { AuthMeResponse } from "./types"; import { SUBSCRIPTION_HELP_HREF, TELEGRAM_BOT_URL, TELEGRAM_GROUP_URL, TELEGRAM_TOPICS_GROUP_URL, WALLETCONNECT_PROJECT_ID, } from "./constants"; import { InfoRow, PlusIcon } from "./AccountInfoRow"; import { chainIdToDisplayName, clearStoredPaymentRecovery, formatTime, parseSubscriptionExpiry, shortAddress, } from "./formatters"; import { createAccountCopy } from "./account-copy"; import { resetWalletConnectProvider } from "./wallet"; import { useAccountPayment } from "./useAccountPayment"; // --- Main Component --- export function AccountCenter() { const router = useRouter(); const { locale } = useI18n(); const isEn = locale === "en-US"; const copy = useMemo(() => createAccountCopy(isEn), [isEn]); // ── UI-only state ────────────────────────────────────── const [loading, setLoading] = useState(true); const [refreshing, setRefreshing] = useState(false); const [copied, setCopied] = useState(false); const [showSecondarySections, setShowSecondarySections] = useState(false); // ── Shared state (declared in component, written by hook via setters) ─ const [showOverlay, setShowOverlay] = useState(false); const [usePoints, setUsePoints] = useState(true); const [errorText, setErrorText] = useState(""); const [updatedAt, setUpdatedAt] = useState(""); const [user, setUser] = useState(null); const [backend, setBackend] = useState(null); const supabaseReady = hasSupabasePublicEnv(); const walletConnectEnabled = Boolean(WALLETCONNECT_PROJECT_ID); // ── Hook ──────────────────────────────────────────────── const { // State from usePaymentState paymentBusy, paymentInfo, paymentError, lastIntentId, lastTxHash, lastPaymentStartedAt, telegramBindOpening, telegramBindUrl, manualPayment, manualTxHash, txValidation, paymentMethodTab, clearPaymentMessages, clearPaymentState, // Setters setPaymentBusy, setPaymentInfo, setPaymentError, setLastIntentId, setLastTxHash, setLastPaymentStartedAt, setTelegramBindOpening, setPaymentMethodTab, setManualPayment, setManualTxHash, setTxValidation, // Additional state paymentConfig, boundWallets, walletAddress, selectedPlanCode, selectedTokenAddress, selectedWallet, providerMode, injectedProviderOptions, selectedInjectedProviderKey, reconcileBusy, // Shared setters setSelectedTokenAddress, setSelectedWallet, setSelectedInjectedProviderKey, setProviderMode, // Derived values authUserId, authIsAuthenticated, paymentReadyForRecovery, hasRecentPaymentRecovery, allowedPaymentHosts, currentPaymentHost, paymentHostAllowed, selectedPlan, selectedPaymentToken, selectedTokenLabel, availableTokenList, effectivePlanList, resolvedSelectedTokenAddress, paymentReceiverAddress, paymentWalletLabel, hasPayingWallet, totalPoints, billing, // Callbacks loadSnapshot, loadPaymentSnapshot, connectAndBindWallet, handleUnbindWallet, createIntentAndPay, createManualPaymentIntent, submitManualPaymentTx, validateTxHash, handleOverlayCheckout, openTelegramBotBindLink, } = useAccountPayment({ isEn, supabaseReady, walletConnectEnabled, copy, backend, user, setUser, setBackend, setErrorText, setUpdatedAt, showOverlay, setShowOverlay, usePoints, setUsePoints, }); // ── Auth analytics effect ────────────────────────────── useEffect(() => { if (!authIsAuthenticated || !authUserId) return; const actorKey = authUserId.toLowerCase(); if (markAnalyticsOnce(`signup_completed:${actorKey}`, "local")) { trackAppEvent("signup_completed", { entry: "account_center", user_id: authUserId, }); } if (markAnalyticsOnce(`dashboard_active:${actorKey}`, "session")) { trackAppEvent("dashboard_active", { entry: "account_center", user_id: authUserId, }); } }, [authIsAuthenticated, authUserId]); // ── Idle callback effect ────────────────────────────── useEffect(() => { let canceled = false; let timeoutId: number | null = null; let idleId: number | null = null; const win = typeof window !== "undefined" ? (window as any) : null; const reveal = () => { if (!canceled) { setShowSecondarySections(true); } }; if (win && typeof win.requestIdleCallback === "function") { idleId = win.requestIdleCallback(reveal, { timeout: 320 }); } else if (typeof window !== "undefined") { timeoutId = window.setTimeout(reveal, 140); } else { setShowSecondarySections(true); } return () => { canceled = true; if ( win && idleId != null && typeof win.cancelIdleCallback === "function" ) { win.cancelIdleCallback(idleId); } if (timeoutId != null && typeof window !== "undefined") { window.clearTimeout(timeoutId); } }; }, []); // ── Initial load effect ──────────────────────────────── useEffect(() => { let cancelled = false; const run = async () => { setLoading(true); await loadSnapshot(); if (!cancelled) setLoading(false); }; void run(); return () => { cancelled = true; }; }, [loadSnapshot]); // ── Refresh ──────────────────────────────────────────── const onRefresh = async () => { setRefreshing(true); await loadSnapshot(); await loadPaymentSnapshot(); setRefreshing(false); }; // ── Sign out ──────────────────────────────────────────── const onSignOut = async () => { clearPaymentState(); clearStoredPaymentRecovery(); await resetWalletConnectProvider(); if (supabaseReady) { try { await getSupabaseBrowserClient().auth.signOut(); } catch { // ignore } } router.replace("/"); }; // ── Derived display values ────────────────────────────── 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] : "") || copy.guestUser; const initials = (displayName.slice(0, 2) || "PW").toUpperCase(); const joinedAt = formatTime(user?.created_at, locale); const isSubscribed = Boolean(backend?.subscription_active); const planCode = String(backend?.subscription_plan_code || "").trim(); const currentExpiryRaw = String( backend?.subscription_expires_at || user?.user_metadata?.pro_expiry || "", ).trim(); const totalExpiryRaw = String( backend?.subscription_total_expires_at || backend?.subscription_expires_at || user?.user_metadata?.pro_expiry || "", ).trim(); const queuedExtensionDays = Math.max( 0, Number(backend?.subscription_queued_days || 0), ); const hasQueuedExtension = Boolean( isSubscribed && queuedExtensionDays > 0, ); const canAccessPaidTelegramGroup = Boolean(isSubscribed); const telegramBound = Number(backend?.telegram_pricing?.telegram_id || 0) > 0; const displayExpiryRaw = isSubscribed ? totalExpiryRaw : currentExpiryRaw; const reminderExpiryRaw = isSubscribed ? totalExpiryRaw : currentExpiryRaw || totalExpiryRaw; const expiryInfo = parseSubscriptionExpiry(reminderExpiryRaw); const expiryFormatted = formatTime(displayExpiryRaw, locale); const currentExpiryFormatted = formatTime(currentExpiryRaw, locale); const totalExpiryFormatted = formatTime(totalExpiryRaw, locale); const proExpiry = isSubscribed ? expiryFormatted !== "--" ? expiryFormatted : displayExpiryRaw || copy.proPendingSync : copy.noProSubscription; const showExpiringSoon = Boolean( isSubscribed && !hasQueuedExtension && expiryInfo && !expiryInfo.expired && expiryInfo.daysLeft <= 3, ); const showExpiredReminder = Boolean( !isSubscribed && expiryInfo && expiryInfo.expired, ); const paymentFeatureReady = paymentReadyForRecovery; const canOpenCheckoutOverlay = Boolean( paymentFeatureReady && (!isSubscribed || showExpiringSoon || showExpiredReminder), ); const subscriptionStatusTitle = showExpiredReminder ? copy.proExpiredTitle : showExpiringSoon ? copy.proEndsSoonTitle : ""; const subscriptionStatusBody = showExpiredReminder ? copy.proExpiredBody : showExpiringSoon ? copy.proEndsSoonBody : ""; const subscriptionStatusMeta = expiryInfo && (showExpiringSoon || showExpiredReminder) ? `${formatTime(expiryInfo.raw, locale)} · ${copy.daysLeft.replace("{days}", String(Math.max(expiryInfo.daysLeft, 0)))}` : ""; const queuedExtensionSummary = hasQueuedExtension ? copy.queuedExtensionSummary .replace("{current}", currentExpiryFormatted) .replace("{days}", String(queuedExtensionDays)) .replace("{total}", totalExpiryFormatted) : ""; const expiryLabel = hasQueuedExtension ? copy.accessUntil : copy.renewalDate; // ── Payment overlay tracking effect ────────────────────── useEffect(() => { if (!showOverlay || !canOpenCheckoutOverlay) return; trackAppEvent("paywall_viewed", { entry: "account_center", user_state: isAuthenticated ? "logged_in" : "guest", expired: showExpiredReminder, expiring_soon: showExpiringSoon, subscription_plan_code: planCode || null, }); }, [ isAuthenticated, canOpenCheckoutOverlay, planCode, showExpiredReminder, showExpiringSoon, showOverlay, ]); // ── Weekly points display (component-only derived) ────── const backendWeeklyPointsRaw = Number(backend?.weekly_points); const metadataWeeklyPointsRaw = Number( user?.user_metadata?.weekly_points ?? 0, ); const weeklyPointsRaw = Number.isFinite(backendWeeklyPointsRaw) ? backendWeeklyPointsRaw : metadataWeeklyPointsRaw; const weeklyRankRaw = backend?.weekly_rank ?? user?.user_metadata?.weekly_rank; const weeklyPoints = Number.isFinite(weeklyPointsRaw) ? Math.max(0, weeklyPointsRaw) : 0; const weeklyRank = weeklyRankRaw == null ? "--" : String(weeklyRankRaw); // ── Telegram bind command ────────────────────────────── const bindCommand = userId ? `/bind ${userId}${email ? ` ${email}` : ""}` : "/bind "; // ── Copy handler ────────────────────────────────────── const handleCopy = (text: string) => { navigator.clipboard.writeText(text).then(() => { setCopied(true); window.setTimeout(() => setCopied(false), 2000); }); }; // ── Render ──────────────────────────────────────────── if (loading && !refreshing) { return (

{copy.loadingAccount}

); } return (
{/* Header */}

{copy.accountCenter}

{!showOverlay && canOpenCheckoutOverlay && ( )} {isAuthenticated ? ( ) : ( {copy.signIn} )}
{(showExpiringSoon || showExpiredReminder) && (
{subscriptionStatusTitle}

{subscriptionStatusBody}

{subscriptionStatusMeta ? (

{subscriptionStatusMeta}

) : null} {billing.canRedeem ? (

当前可用 {billing.pointsUsed} 积分抵扣 $ {billing.discountAmount.toFixed(2)}, 续费时会自动生效。

) : null}
)} {/* User Card */}
{initials}

{displayName}

{isSubscribed ? copy.proMember : isEn ? "UNSUBSCRIBED" : "未订阅"}

{email || copy.guestUser}

{" "} {userId ? `${userId.substring(0, 12)}...` : "--"}
{" "} {copy.joinedAt}: {joinedAt}

{copy.totalPoints}

{" "} {totalPoints.toLocaleString()}

{copy.weeklyPoints}

{" "} {weeklyPoints.toLocaleString()}

{copy.weeklyRank}

{" "} {weeklyRank === "--" ? weeklyRank : `#${weeklyRank}`}

{/* Weekly Ranking Motivation */} {showSecondarySections ? (

{" "} {copy.weeklyRewards}

1
{" "} Top 1
+200 积分 & 7天Pro
2
{" "} Top 2-3
+100 积分
4
{" "} Top 4-10
+50 积分

积分规则:群内有效发言(自动防刷检测)+ 每日首条发言额外奖励。每周一零点结算周榜,所有活跃用户均享参与奖。

) : (
)} {/* Subscription Info & Paywall */}

{copy.membershipDetails}

{copy.identityStatus}

{queuedExtensionSummary ? (

{queuedExtensionSummary}

) : null}
{/* Paywall Mask */} {canOpenCheckoutOverlay && showOverlay && (
setUsePoints((prev) => !prev)} billing={{ pointsEnabled: billing.pointsEnabled, isEligible: billing.canRedeem, pointsUsed: billing.pointsUsed, discountAmount: billing.discountAmount, finalPrice: billing.payAmount, maxDiscountUsd: billing.maxDiscountUsdc, pointsPerUsd: billing.pointsPerUsdc, }} onPay={() => void handleOverlayCheckout()} onManualPay={() => void createManualPaymentIntent()} onClose={() => setShowOverlay(false)} payBusy={paymentBusy} payLabel={hasPayingWallet ? copy.payNow : copy.connectAndPay} manualPayLabel="手动转账" errorText={paymentError || undefined} infoText={paymentInfo || undefined} txHash={lastTxHash || undefined} chainId={paymentConfig?.chain_id || 137} paymentTokenLabel={selectedTokenLabel} faqHref={SUBSCRIPTION_HELP_HREF} telegramGroupUrl="" />
)}
{/* Telegram Bot Section & Payment Details */} {showSecondarySections ? (
{canAccessPaidTelegramGroup && (

{copy.telegramBind}

{copy.telegramHint}

{TELEGRAM_TOPICS_GROUP_URL && TELEGRAM_TOPICS_GROUP_URL !== TELEGRAM_GROUP_URL && telegramBound ? ( {copy.telegramTopicsGroupLink} ) : null} {TELEGRAM_GROUP_URL && telegramBound ? ( {copy.telegramGroupLink} ) : null}
{bindCommand}

{copy.telegramFallbackHint}

{copy.paymentManualSupport}
)} {/* Payment Details / Wallet Management */}

{copy.paymentMgmt}

{paymentError ? (
{paymentError}
) : null} {!paymentError && paymentInfo ? (
{paymentInfo} {telegramBindUrl ? ( {telegramBindUrl} ) : null}
) : null} {!paymentHostAllowed ? (
{copy.paymentHostBlocked.replace( "{host}", allowedPaymentHosts[0] || "polyweather.top", )}
) : null}

{copy.paymentGuardHint}

{availableTokenList.length > 0 && (

{copy.paymentToken}

{availableTokenList.map((token) => { const active = token.address === (resolvedSelectedTokenAddress || token.address); return ( ); })}
)} {/* Payment Method Tabs */}

{copy.paymentMethodLabel}

{paymentMethodTab === "wallet" ? (

{copy.paymentWalletDesc}

{copy.paymentGasWarning}
{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

)}
) : (

{copy.paymentManualDesc}

{copy.paymentManualTitle}

{copy.paymentManualHint}

{manualPayment ? (

{copy.paymentAmount}

{manualPayment.amount_usdc}{" "} {manualPayment.token_symbol || selectedTokenLabel}

{copy.paymentReceiverLabel}

{ manualPayment.receiver_address }

Tx Hash

{ setManualTxHash( event.target.value, ); void validateTxHash( manualPayment.intent_id || lastIntentId || "", event.target.value, ); }} placeholder="0x..." className="mt-1 w-full rounded-xl border border-slate-300 bg-white px-3 py-2 font-mono text-xs text-slate-950 outline-none focus:border-emerald-500 focus:ring-2 focus:ring-emerald-500/20" /> {txValidation.loading ? (

{copy.verifying}

) : txValidation.checked && txValidation.valid ? (

{copy.verifyAddressMatch}

) : txValidation.checked && txValidation.valid === false ? (

{txValidation.reason === "tx_not_mined" ? copy.verifyTxNotMined : txValidation.reason === "receiver_mismatch" ? copy .verifyAddressMismatch : txValidation.reason === "amount_insufficient" ? copy .verifyAmountLow : txValidation.reason === "tx_reverted" ? copy .verifyTxReverted : txValidation.detail || copy.verifyFailed + (txValidation.reason || copy .verifyUnknown)}

) : null}
) : null}
)}
) : (
)}
); }