Persist recent payment recovery state across account sessions
This commit is contained in:
@@ -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`,
|
||||||
|
|||||||
Reference in New Issue
Block a user