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;
};
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`,