"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 [referralCodeInput, setReferralCodeInput] = useState(""); const [referralApplying, setReferralApplying] = useState(false); 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, selectedPaymentChainId, selectedTokenAddress, selectedWallet, providerMode, injectedProviderOptions, selectedInjectedProviderKey, reconcileBusy, // Shared setters setSelectedTokenAddress, setSelectedPaymentChainId, setSelectedPlanCode, setSelectedWallet, setSelectedInjectedProviderKey, setProviderMode, // Derived values authUserId, authIsAuthenticated, paymentReadyForRecovery, hasRecentPaymentRecovery, allowedPaymentHosts, currentPaymentHost, paymentHostAllowed, selectedPlan, selectedPaymentToken, selectedTokenLabel, availableTokenList, availableChainList, selectedPaymentChain, 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 backendAuthenticated = backend?.authenticated === true; const localAuthenticated = Boolean(user?.id); const isSubscriptionUnknown = Boolean( localAuthenticated && (!backend || backend.subscription_active == null || backend.authenticated === false), ); const isSubscribed = backend?.subscription_active === true; const subscriptionSource = String(backend?.subscription_source || "").trim(); const isTrialSubscription = Boolean( backend?.subscription_is_trial === true || String(backend?.subscription_plan_code || "").toLowerCase().includes("trial") || subscriptionSource.toLowerCase().includes("trial"), ); const subscriptionStatusLabel = isSubscribed ? isTrialSubscription ? copy.trialBadge : copy.proMember : isSubscriptionUnknown ? copy.subscriptionChecking : isEn ? "UNSUBSCRIBED" : "未订阅"; 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 && !isTrialSubscription); const hasTelegramPanel = Boolean(isTrialSubscription || canAccessPaidTelegramGroup); 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 : isSubscriptionUnknown ? copy.subscriptionUnknown : 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 && !isSubscriptionUnknown && (!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; const displayPlanList = effectivePlanList.length ? effectivePlanList : [ { plan_code: "pro_monthly", plan_id: 101, amount_usdc: "29.9", duration_days: 30 }, { plan_code: "pro_quarterly", plan_id: 102, amount_usdc: "79.9", duration_days: 90 }, ]; const referral = backend?.referral; const referralCode = String(referral?.code || "").trim(); const appliedReferralCode = String(referral?.applied_code || "").trim(); const canApplyReferralCode = Boolean( isAuthenticated && !isSubscribed && !appliedReferralCode && referralCodeInput.trim(), ); // ── 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, ]); // ── Referral points display ──────────────────────────── const referralRewardPointsRaw = Number(referral?.reward_points ?? 3500); const referralRewardPoints = Number.isFinite(referralRewardPointsRaw) ? Math.max(0, referralRewardPointsRaw) : 3500; const monthlyReferralCountRaw = Number(referral?.monthly_reward_count ?? 0); const monthlyReferralCount = Number.isFinite(monthlyReferralCountRaw) ? Math.max(0, monthlyReferralCountRaw) : 0; const monthlyReferralLimitRaw = Number(referral?.monthly_reward_limit ?? 10); const monthlyReferralLimit = Number.isFinite(monthlyReferralLimitRaw) ? Math.max(0, monthlyReferralLimitRaw) : 10; const monthlyReferralPointsRaw = Number( referral?.monthly_reward_points ?? monthlyReferralCount * referralRewardPoints, ); const monthlyReferralPoints = Number.isFinite(monthlyReferralPointsRaw) ? Math.max(0, monthlyReferralPointsRaw) : 0; const monthlyReferralPointsLimitRaw = Number( referral?.monthly_reward_points_limit ?? monthlyReferralLimit * referralRewardPoints, ); const monthlyReferralPointsLimit = Number.isFinite(monthlyReferralPointsLimitRaw) ? Math.max(0, monthlyReferralPointsLimitRaw) : monthlyReferralLimit * referralRewardPoints; // ── 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); }); }; const applyReferralCode = useCallback(async () => { const code = referralCodeInput.trim(); if (!code || referralApplying) return; setReferralApplying(true); setPaymentError(""); setPaymentInfo(""); try { const headers: Record = { "Content-Type": "application/json", }; if (supabaseReady) { const { data: { session }, } = await getSupabaseBrowserClient().auth.getSession(); const token = String(session?.access_token || "").trim(); if (token) headers.Authorization = `Bearer ${token}`; } const res = await fetch("/api/auth/referral/apply", { method: "POST", headers, body: JSON.stringify({ code }), }); if (!res.ok) { const raw = (await res.text()).slice(0, 240); throw new Error(raw || copy.referralApplyFailed); } setPaymentInfo(copy.referralApplied); setReferralCodeInput(""); await loadSnapshot(); await loadPaymentSnapshot(); } catch (error) { setPaymentError( error instanceof Error ? error.message : copy.referralApplyFailed, ); } finally { setReferralApplying(false); } }, [ copy.referralApplied, copy.referralApplyFailed, loadPaymentSnapshot, loadSnapshot, referralApplying, referralCodeInput, setPaymentError, setPaymentInfo, supabaseReady, ]); // ── 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}

{subscriptionStatusLabel}

{email || copy.guestUser}

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

{copy.totalPoints}

{" "} {totalPoints.toLocaleString()}

{copy.weeklyPoints}

{" "} {monthlyReferralCount.toLocaleString()}

{copy.weeklyRank}

{" "} {monthlyReferralCount}/{monthlyReferralLimit}

{/* Referral rewards */} {showSecondarySections ? (

{" "} {copy.weeklyRewards}

{" "} {copy.referralRewardHint} +{referralRewardPoints.toLocaleString()}
{" "} {copy.weeklyPoints} {monthlyReferralCount}/{monthlyReferralLimit}
{" "} {copy.totalPoints} {monthlyReferralPoints.toLocaleString()}/{monthlyReferralPointsLimit.toLocaleString()}

{copy.pointsRule}

) : (
)} {/* 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={selectedPaymentChainId || paymentConfig?.chain_id || 137} paymentTokenLabel={selectedTokenLabel} faqHref={SUBSCRIPTION_HELP_HREF} telegramGroupUrl="" />
)}
{/* Telegram Bot Section & Payment Details */} {showSecondarySections ? (
{isTrialSubscription && (

{copy.telegramBind}

{copy.trialPaidGroupLocked}

)} {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.proPlan}

{displayPlanList.map((plan) => { const code = String(plan.plan_code || ""); const active = code === selectedPlanCode; const isQuarterly = code === "pro_quarterly"; return ( ); })}
{appliedReferralCode && selectedPlanCode === "pro_monthly" ? (

{copy.referralDiscountHint}

) : null}

{copy.referralTitle}

{copy.referralInviteLimit}

{copy.referralMyCode}

{referralCode || "--"} {referralCode ? ( ) : null}

{copy.referralRewardHint}

{copy.referralApplyLabel}

{appliedReferralCode ? (
{appliedReferralCode}
) : (
setReferralCodeInput(event.target.value)} placeholder={copy.referralApplyPlaceholder} className="min-w-0 flex-1 rounded-xl border border-slate-300 bg-white px-3 py-2 text-xs text-slate-950 outline-none focus:border-blue-500 focus:ring-2 focus:ring-blue-500/20" />
)}

{copy.referralDiscountHint}

{copy.paymentGuardHint}

{availableChainList.length > 1 && (

{copy.paymentNetwork}

{availableChainList.map((chain) => { const active = chain.chain_id === selectedPaymentChainId; return ( ); })}
)} {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} )}
{chainIdToDisplayName(w.chain_id)}
))}
) : (

{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}
)}
) : (
)}
); }