Persist recent payment recovery state across account sessions

This commit is contained in:
2569718930@qq.com
2026-04-06 20:40:26 +08:00
parent 7fe16bd584
commit fbd1e4ab16
+79 -14
View File
@@ -205,6 +205,13 @@ type ConnectBindOptions = {
openOverlayAfterBind?: boolean; openOverlayAfterBind?: boolean;
}; };
type PaymentRecoveryState = {
intentId: string;
txHash: string;
userId: string;
createdAt: number;
};
const WALLETCONNECT_PROJECT_ID = String( const WALLETCONNECT_PROJECT_ID = String(
process.env.NEXT_PUBLIC_WALLETCONNECT_PROJECT_ID || "", process.env.NEXT_PUBLIC_WALLETCONNECT_PROJECT_ID || "",
).trim(); ).trim();
@@ -221,6 +228,8 @@ const TELEGRAM_BOT_URL = String(
).trim(); ).trim();
const TELEGRAM_MARKET_CHANNEL_URL = "https://t.me/+hGAk7JsjtdhiOTUx"; const TELEGRAM_MARKET_CHANNEL_URL = "https://t.me/+hGAk7JsjtdhiOTUx";
const SUBSCRIPTION_HELP_HREF = "/subscription-help"; 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 walletConnectProviderCache: EvmProvider | null = null;
let walletConnectProviderChainId: number | null = null; let walletConnectProviderChainId: number | null = null;
@@ -323,6 +332,11 @@ function shortAddress(address: string) {
return `${text.slice(0, 8)}...${text.slice(-6)}`; 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 { function getEvmProvider(): EvmProvider | null {
return listInjectedProviders()[0]?.provider || null; return listInjectedProviders()[0]?.provider || null;
} }
@@ -743,8 +757,8 @@ export function AccountCenter() {
? "Pro entitlement recovered." ? "Pro entitlement recovered."
: "Pro 权限已恢复。", : "Pro 权限已恢复。",
walletRecoveryFailed: isEn 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" : "解绑", unbind: isEn ? "Unbind" : "解绑",
unbindConfirm: isEn unbindConfirm: isEn
? "Unbind wallet {address}? You can bind it again later." ? "Unbind wallet {address}? You can bind it again later."
@@ -838,6 +852,7 @@ export function AccountCenter() {
const [paymentError, setPaymentError] = useState(""); const [paymentError, setPaymentError] = useState("");
const [lastIntentId, setLastIntentId] = useState(""); const [lastIntentId, setLastIntentId] = useState("");
const [lastTxHash, setLastTxHash] = useState(""); const [lastTxHash, setLastTxHash] = useState("");
const [lastPaymentStartedAt, setLastPaymentStartedAt] = useState(0);
const [showSecondarySections, setShowSecondarySections] = useState(false); const [showSecondarySections, setShowSecondarySections] = useState(false);
const [reconcileBusy, setReconcileBusy] = useState(false); const [reconcileBusy, setReconcileBusy] = useState(false);
@@ -848,6 +863,9 @@ export function AccountCenter() {
const paymentReadyForRecovery = Boolean( const paymentReadyForRecovery = Boolean(
paymentConfig?.enabled && paymentConfig?.configured, paymentConfig?.enabled && paymentConfig?.configured,
); );
const hasRecentPaymentRecovery =
Boolean(lastIntentId && lastTxHash && authUserId && lastPaymentStartedAt) &&
Date.now() - lastPaymentStartedAt <= PAYMENT_RECOVERY_TTL_MS;
const allowedPaymentHosts = useMemo(() => getAllowedPaymentHosts(), []); const allowedPaymentHosts = useMemo(() => getAllowedPaymentHosts(), []);
const currentPaymentHost = useMemo(() => getCurrentPaymentHost(), []); const currentPaymentHost = useMemo(() => getCurrentPaymentHost(), []);
const paymentHostAllowed = useMemo( const paymentHostAllowed = useMemo(
@@ -1283,22 +1301,61 @@ export function AccountCenter() {
useEffect(() => { useEffect(() => {
if (typeof window === "undefined") return; 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( window.sessionStorage.setItem(
"polyweather:lastPaymentIntentId", PAYMENT_RECOVERY_STORAGE_KEY,
lastIntentId, JSON.stringify(payload),
); );
}, [lastIntentId]); }, [authUserId, lastIntentId, lastPaymentStartedAt, lastTxHash]);
useEffect(() => { useEffect(() => {
if (typeof window === "undefined") return; if (typeof window === "undefined") return;
const storedIntentId = window.sessionStorage.getItem( if (!authUserId) return;
"polyweather:lastPaymentIntentId", if (lastIntentId && lastTxHash && lastPaymentStartedAt) return;
); const raw = window.sessionStorage.getItem(PAYMENT_RECOVERY_STORAGE_KEY);
if (storedIntentId && !lastIntentId) { if (!raw) return;
setLastIntentId(storedIntentId); 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 () => { const onRefresh = async () => {
setRefreshing(true); setRefreshing(true);
@@ -1350,7 +1407,7 @@ export function AccountCenter() {
if (!authIsAuthenticated) return; if (!authIsAuthenticated) return;
if (backend?.subscription_active) return; if (backend?.subscription_active) return;
if (!paymentReadyForRecovery) return; if (!paymentReadyForRecovery) return;
if (!lastIntentId) return; if (!hasRecentPaymentRecovery) return;
let cancelled = false; let cancelled = false;
const run = async () => { const run = async () => {
setPaymentInfo(copy.walletRecoveryBusy); setPaymentInfo(copy.walletRecoveryBusy);
@@ -1370,12 +1427,16 @@ export function AccountCenter() {
authIsAuthenticated, authIsAuthenticated,
copy.walletRecoveryBusy, copy.walletRecoveryBusy,
copy.walletRecoveryFailed, copy.walletRecoveryFailed,
lastIntentId, hasRecentPaymentRecovery,
paymentReadyForRecovery, paymentReadyForRecovery,
reconcileLatestPayment, reconcileLatestPayment,
]); ]);
const onSignOut = async () => { const onSignOut = async () => {
setLastIntentId("");
setLastTxHash("");
setLastPaymentStartedAt(0);
clearStoredPaymentRecovery();
if (walletConnectProviderCache?.disconnect) { if (walletConnectProviderCache?.disconnect) {
try { try {
await walletConnectProviderCache.disconnect(); await walletConnectProviderCache.disconnect();
@@ -1967,7 +2028,10 @@ export function AccountCenter() {
const createIntentAndPay = async () => { const createIntentAndPay = async () => {
setPaymentError(""); setPaymentError("");
setPaymentInfo(""); setPaymentInfo("");
setLastIntentId("");
setLastTxHash(""); setLastTxHash("");
setLastPaymentStartedAt(0);
clearStoredPaymentRecovery();
if (!paymentHostAllowed) { if (!paymentHostAllowed) {
setPaymentError( setPaymentError(
copy.paymentHostBlocked.replace( copy.paymentHostBlocked.replace(
@@ -2187,6 +2251,7 @@ export function AccountCenter() {
})) as string; })) as string;
const txHashNorm = String(txHash || "").toLowerCase(); const txHashNorm = String(txHash || "").toLowerCase();
setLastTxHash(txHashNorm); setLastTxHash(txHashNorm);
setLastPaymentStartedAt(Date.now());
const submitRes = await fetch( const submitRes = await fetch(
`/api/payments/intents/${intentId}/submit`, `/api/payments/intents/${intentId}/submit`,