fix(payment): guard ezpay redirect persistence

This commit is contained in:
2026-06-23 17:55:51 +08:00
parent c8881a33c6
commit d5574f1adb
2 changed files with 63 additions and 43 deletions
+21 -17
View File
@@ -26,26 +26,13 @@ export function PwaInstallOverlay() {
let mounted = true; let mounted = true;
const init = async () => { const init = async () => {
// 0) PWA 支持 / 安装 双重门禁(两种环境都检查): const isDev = AppEnvUtil.isDevelopment();
// isSupported() 检查 serviceWorker + windowSSR 安全) const shouldShowDialog = await shouldShowPwaInstallDialog(isDev);
// isInstalled() 检查 standalone display-mode(避免给已安装用户再弹) if (!shouldShowDialog) return;
if (!pwaUtil.isSupported()) return;
if (pwaUtil.isInstalled()) return;
if (BrowserDetector.isInAppBrowser()) return;
// 提前挂 beforeinstallprompt 监听,避免用户点 OK 时事件已在更早时机丢失。 // 提前挂 beforeinstallprompt 监听,避免用户点 OK 时事件已在更早时机丢失。
pwaUtil.prepareInstallPrompt(); pwaUtil.prepareInstallPrompt();
const isDev = AppEnvUtil.isDevelopment();
if (!isDev) {
// 生产环境:每日只弹一次(防骚扰)
const lastResult = await SpAsyncUtil.getString(PWA_DIALOG_KEY);
if (!lastResult.success) return;
const today = new Date().toISOString().slice(0, 10);
if (lastResult.data === today) return; // 今天已显示过
}
// 开发环境:跳过"每日一次"门禁,每次进入都弹,方便 UI 测试
// 开发环境 1s 后弹(快);生产环境 3.5s 后弹(让用户先看到聊天内容) // 开发环境 1s 后弹(快);生产环境 3.5s 后弹(让用户先看到聊天内容)
const delay = isDev ? 1000 : 3500; const delay = isDev ? 1000 : 3500;
setTimeout(() => { setTimeout(() => {
@@ -63,6 +50,23 @@ export function PwaInstallOverlay() {
return <div className={styles.hidden} aria-hidden="true" />; return <div className={styles.hidden} aria-hidden="true" />;
} }
async function shouldShowPwaInstallDialog(isDevelopment: boolean) {
// PWA 支持 / 安装 双重门禁(两种环境都检查)。
if (!pwaUtil.isSupported()) return false;
if (pwaUtil.isInstalled()) return false;
if (BrowserDetector.isInAppBrowser()) return false;
// 开发环境:跳过"每日一次"门禁,每次进入都弹,方便 UI 测试。
if (isDevelopment) return true;
// 生产环境:每日只弹一次(防骚扰)。
const lastResult = await SpAsyncUtil.getString(PWA_DIALOG_KEY);
if (!lastResult.success) return false;
const today = new Date().toISOString().slice(0, 10);
return lastResult.data !== today;
}
function showPwaDialog() { function showPwaDialog() {
if (typeof document === "undefined") return; if (typeof document === "undefined") return;
// 记录显示时间 // 记录显示时间
@@ -63,10 +63,12 @@ export function SubscriptionCheckoutButton({
} }
if (isEzpay) { if (isEzpay) {
void redirectToEzpay({ void launchEzpayRedirect({
orderId: payment.currentOrderId, orderId: payment.currentOrderId,
paymentUrl, paymentUrl,
subscriptionType, subscriptionType,
onFailed: (errorMessage) =>
paymentDispatch({ type: "PaymentLaunchFailed", errorMessage }),
}); });
return; return;
} }
@@ -144,10 +146,14 @@ export function SubscriptionCheckoutButton({
const handleEzpayConfirm = () => { const handleEzpayConfirm = () => {
if (!payment.currentOrderId || !ezpayPaymentUrl) return; if (!payment.currentOrderId || !ezpayPaymentUrl) return;
setIsConfirmingEzpay(true); setIsConfirmingEzpay(true);
void redirectToEzpay({ void launchEzpayRedirect({
orderId: payment.currentOrderId, orderId: payment.currentOrderId,
paymentUrl: ezpayPaymentUrl, paymentUrl: ezpayPaymentUrl,
subscriptionType, subscriptionType,
onFailed: (errorMessage) => {
setIsConfirmingEzpay(false);
paymentDispatch({ type: "PaymentLaunchFailed", errorMessage });
},
}); });
}; };
@@ -235,47 +241,57 @@ interface RedirectToEzpayInput {
orderId: string | null; orderId: string | null;
paymentUrl: string; paymentUrl: string;
subscriptionType: "vip" | "voice"; subscriptionType: "vip" | "voice";
onFailed: (errorMessage: string) => void;
} }
async function redirectToEzpay({ async function launchEzpayRedirect({
orderId, orderId,
paymentUrl, paymentUrl,
subscriptionType, subscriptionType,
onFailed,
}: RedirectToEzpayInput): Promise<void> { }: RedirectToEzpayInput): Promise<void> {
log.debug("[subscription-checkout] redirectToEzpay START", { log.debug("[subscription-checkout] launchEzpayRedirect START", {
hasOrderId: Boolean(orderId), hasOrderId: Boolean(orderId),
orderId, orderId,
subscriptionType, subscriptionType,
paymentUrl, paymentUrl,
}); });
if (orderId) { if (!orderId) {
const errorMessage = "Missing order id before opening Ezpay.";
log.error("[subscription-checkout] pending ezpay order save skipped", {
subscriptionType,
paymentUrl,
errorMessage,
});
onFailed(errorMessage);
return;
}
const saveResult = await PendingPaymentOrderStorage.setPendingOrder({ const saveResult = await PendingPaymentOrderStorage.setPendingOrder({
orderId, orderId,
payChannel: "ezpay", payChannel: "ezpay",
subscriptionType, subscriptionType,
createdAt: Date.now(), createdAt: Date.now(),
}); });
if (Result.isOk(saveResult)) { if (Result.isErr(saveResult)) {
log.debug("[subscription-checkout] pending ezpay order saved", { const errorMessage =
orderId, "Could not save payment order before opening Ezpay. Please try again.";
subscriptionType,
});
} else {
log.error("[subscription-checkout] pending ezpay order save failed", { log.error("[subscription-checkout] pending ezpay order save failed", {
orderId, orderId,
subscriptionType, subscriptionType,
error: saveResult.error, error: saveResult.error,
}); });
} onFailed(errorMessage);
} else { return;
log.warn("[subscription-checkout] skip pending ezpay order save: missing orderId", {
subscriptionType,
paymentUrl,
});
} }
log.debug("[subscription-checkout] redirectToEzpay NOW", { log.debug("[subscription-checkout] pending ezpay order saved", {
orderId,
subscriptionType,
});
log.debug("[subscription-checkout] launchEzpayRedirect NOW", {
orderId, orderId,
subscriptionType, subscriptionType,
paymentUrl, paymentUrl,