From fbd1e4ab16d82c55e48b580a30f5a44e28571979 Mon Sep 17 00:00:00 2001 From: "2569718930@qq.com" <2569718930@qq.com> Date: Mon, 6 Apr 2026 20:40:26 +0800 Subject: [PATCH] Persist recent payment recovery state across account sessions --- frontend/components/account/AccountCenter.tsx | 93 ++++++++++++++++--- 1 file changed, 79 insertions(+), 14 deletions(-) diff --git a/frontend/components/account/AccountCenter.tsx b/frontend/components/account/AccountCenter.tsx index 1f0c0284..3d5cbda1 100644 --- a/frontend/components/account/AccountCenter.tsx +++ b/frontend/components/account/AccountCenter.tsx @@ -205,6 +205,13 @@ type ConnectBindOptions = { openOverlayAfterBind?: boolean; }; +type PaymentRecoveryState = { + intentId: string; + txHash: string; + userId: string; + createdAt: number; +}; + const WALLETCONNECT_PROJECT_ID = String( process.env.NEXT_PUBLIC_WALLETCONNECT_PROJECT_ID || "", ).trim(); @@ -221,6 +228,8 @@ const TELEGRAM_BOT_URL = String( ).trim(); const TELEGRAM_MARKET_CHANNEL_URL = "https://t.me/+hGAk7JsjtdhiOTUx"; const SUBSCRIPTION_HELP_HREF = "/subscription-help"; +const PAYMENT_RECOVERY_STORAGE_KEY = "polyweather:lastPaymentRecovery"; +const PAYMENT_RECOVERY_TTL_MS = 6 * 60 * 60 * 1000; let walletConnectProviderCache: EvmProvider | null = null; let walletConnectProviderChainId: number | null = null; @@ -323,6 +332,11 @@ function shortAddress(address: string) { return `${text.slice(0, 8)}...${text.slice(-6)}`; } +function clearStoredPaymentRecovery() { + if (typeof window === "undefined") return; + window.sessionStorage.removeItem(PAYMENT_RECOVERY_STORAGE_KEY); +} + function getEvmProvider(): EvmProvider | null { return listInjectedProviders()[0]?.provider || null; } @@ -743,8 +757,8 @@ export function AccountCenter() { ? "Pro entitlement recovered." : "Pro 权限已恢复。", walletRecoveryFailed: isEn - ? "Paid detected but entitlement is still pending. Please refresh in a minute or contact support." - : "已检测到支付,但订阅状态仍在同步中。请稍后刷新,或联系管理员处理。", + ? "A recent on-chain payment is still syncing to your subscription. Please refresh in a minute or contact support." + : "检测到最近的链上支付流程,但订阅状态仍在同步中。请稍后刷新,或联系管理员处理。", unbind: isEn ? "Unbind" : "解绑", unbindConfirm: isEn ? "Unbind wallet {address}? You can bind it again later." @@ -838,6 +852,7 @@ export function AccountCenter() { const [paymentError, setPaymentError] = useState(""); const [lastIntentId, setLastIntentId] = useState(""); const [lastTxHash, setLastTxHash] = useState(""); + const [lastPaymentStartedAt, setLastPaymentStartedAt] = useState(0); const [showSecondarySections, setShowSecondarySections] = useState(false); const [reconcileBusy, setReconcileBusy] = useState(false); @@ -848,6 +863,9 @@ export function AccountCenter() { const paymentReadyForRecovery = Boolean( paymentConfig?.enabled && paymentConfig?.configured, ); + const hasRecentPaymentRecovery = + Boolean(lastIntentId && lastTxHash && authUserId && lastPaymentStartedAt) && + Date.now() - lastPaymentStartedAt <= PAYMENT_RECOVERY_TTL_MS; const allowedPaymentHosts = useMemo(() => getAllowedPaymentHosts(), []); const currentPaymentHost = useMemo(() => getCurrentPaymentHost(), []); const paymentHostAllowed = useMemo( @@ -1283,22 +1301,61 @@ export function AccountCenter() { useEffect(() => { if (typeof window === "undefined") return; - if (!lastIntentId) return; + if (!(lastIntentId && lastTxHash && authUserId && lastPaymentStartedAt)) { + clearStoredPaymentRecovery(); + return; + } + const payload: PaymentRecoveryState = { + intentId: lastIntentId, + txHash: lastTxHash, + userId: authUserId, + createdAt: lastPaymentStartedAt, + }; window.sessionStorage.setItem( - "polyweather:lastPaymentIntentId", - lastIntentId, + PAYMENT_RECOVERY_STORAGE_KEY, + JSON.stringify(payload), ); - }, [lastIntentId]); + }, [authUserId, lastIntentId, lastPaymentStartedAt, lastTxHash]); useEffect(() => { if (typeof window === "undefined") return; - const storedIntentId = window.sessionStorage.getItem( - "polyweather:lastPaymentIntentId", - ); - if (storedIntentId && !lastIntentId) { - setLastIntentId(storedIntentId); + if (!authUserId) return; + if (lastIntentId && lastTxHash && lastPaymentStartedAt) return; + const raw = window.sessionStorage.getItem(PAYMENT_RECOVERY_STORAGE_KEY); + if (!raw) return; + try { + const parsed = JSON.parse(raw) as PaymentRecoveryState; + const userId = String(parsed?.userId || "").trim(); + const intentId = String(parsed?.intentId || "").trim(); + const txHash = String(parsed?.txHash || "").trim().toLowerCase(); + const createdAt = Number(parsed?.createdAt || 0); + const expired = + !createdAt || Date.now() - createdAt > PAYMENT_RECOVERY_TTL_MS; + if ( + expired || + !intentId || + !txHash || + !userId || + userId !== authUserId + ) { + clearStoredPaymentRecovery(); + return; + } + setLastIntentId(intentId); + setLastTxHash(txHash); + setLastPaymentStartedAt(createdAt); + } catch { + clearStoredPaymentRecovery(); } - }, [lastIntentId]); + }, [authUserId, lastIntentId, lastPaymentStartedAt, lastTxHash]); + + useEffect(() => { + if (!backend?.subscription_active) return; + setLastIntentId(""); + setLastTxHash(""); + setLastPaymentStartedAt(0); + clearStoredPaymentRecovery(); + }, [backend?.subscription_active]); const onRefresh = async () => { setRefreshing(true); @@ -1350,7 +1407,7 @@ export function AccountCenter() { if (!authIsAuthenticated) return; if (backend?.subscription_active) return; if (!paymentReadyForRecovery) return; - if (!lastIntentId) return; + if (!hasRecentPaymentRecovery) return; let cancelled = false; const run = async () => { setPaymentInfo(copy.walletRecoveryBusy); @@ -1370,12 +1427,16 @@ export function AccountCenter() { authIsAuthenticated, copy.walletRecoveryBusy, copy.walletRecoveryFailed, - lastIntentId, + hasRecentPaymentRecovery, paymentReadyForRecovery, reconcileLatestPayment, ]); const onSignOut = async () => { + setLastIntentId(""); + setLastTxHash(""); + setLastPaymentStartedAt(0); + clearStoredPaymentRecovery(); if (walletConnectProviderCache?.disconnect) { try { await walletConnectProviderCache.disconnect(); @@ -1967,7 +2028,10 @@ export function AccountCenter() { const createIntentAndPay = async () => { setPaymentError(""); setPaymentInfo(""); + setLastIntentId(""); setLastTxHash(""); + setLastPaymentStartedAt(0); + clearStoredPaymentRecovery(); if (!paymentHostAllowed) { setPaymentError( copy.paymentHostBlocked.replace( @@ -2187,6 +2251,7 @@ export function AccountCenter() { })) as string; const txHashNorm = String(txHash || "").toLowerCase(); setLastTxHash(txHashNorm); + setLastPaymentStartedAt(Date.now()); const submitRes = await fetch( `/api/payments/intents/${intentId}/submit`,