diff --git a/src/app/chat/components/pwa-install-overlay.tsx b/src/app/chat/components/pwa-install-overlay.tsx index 477a7a0d..337ca233 100644 --- a/src/app/chat/components/pwa-install-overlay.tsx +++ b/src/app/chat/components/pwa-install-overlay.tsx @@ -26,26 +26,13 @@ export function PwaInstallOverlay() { let mounted = true; const init = async () => { - // 0) PWA 支持 / 安装 双重门禁(两种环境都检查): - // isSupported() 检查 serviceWorker + window(SSR 安全) - // isInstalled() 检查 standalone display-mode(避免给已安装用户再弹) - if (!pwaUtil.isSupported()) return; - if (pwaUtil.isInstalled()) return; - if (BrowserDetector.isInAppBrowser()) return; + const isDev = AppEnvUtil.isDevelopment(); + const shouldShowDialog = await shouldShowPwaInstallDialog(isDev); + if (!shouldShowDialog) return; + // 提前挂 beforeinstallprompt 监听,避免用户点 OK 时事件已在更早时机丢失。 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 后弹(让用户先看到聊天内容) const delay = isDev ? 1000 : 3500; setTimeout(() => { @@ -63,6 +50,23 @@ export function PwaInstallOverlay() { return
; } +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() { if (typeof document === "undefined") return; // 记录显示时间 diff --git a/src/app/subscription/components/subscription-checkout-button.tsx b/src/app/subscription/components/subscription-checkout-button.tsx index a4ed49ce..fa50aca9 100644 --- a/src/app/subscription/components/subscription-checkout-button.tsx +++ b/src/app/subscription/components/subscription-checkout-button.tsx @@ -63,10 +63,12 @@ export function SubscriptionCheckoutButton({ } if (isEzpay) { - void redirectToEzpay({ + void launchEzpayRedirect({ orderId: payment.currentOrderId, paymentUrl, subscriptionType, + onFailed: (errorMessage) => + paymentDispatch({ type: "PaymentLaunchFailed", errorMessage }), }); return; } @@ -144,10 +146,14 @@ export function SubscriptionCheckoutButton({ const handleEzpayConfirm = () => { if (!payment.currentOrderId || !ezpayPaymentUrl) return; setIsConfirmingEzpay(true); - void redirectToEzpay({ + void launchEzpayRedirect({ orderId: payment.currentOrderId, paymentUrl: ezpayPaymentUrl, subscriptionType, + onFailed: (errorMessage) => { + setIsConfirmingEzpay(false); + paymentDispatch({ type: "PaymentLaunchFailed", errorMessage }); + }, }); }; @@ -235,47 +241,57 @@ interface RedirectToEzpayInput { orderId: string | null; paymentUrl: string; subscriptionType: "vip" | "voice"; + onFailed: (errorMessage: string) => void; } -async function redirectToEzpay({ +async function launchEzpayRedirect({ orderId, paymentUrl, subscriptionType, + onFailed, }: RedirectToEzpayInput): Promise