diff --git a/src/app/chat/components/pwa-install-dialog.tsx b/src/app/chat/components/pwa-install-dialog.tsx index afb32c1e..e62bb216 100644 --- a/src/app/chat/components/pwa-install-dialog.tsx +++ b/src/app/chat/components/pwa-install-dialog.tsx @@ -13,13 +13,13 @@ import styles from "./pwa-install-dialog.module.css"; export interface PwaInstallDialogProps { onClose: () => void; - onInstall?: () => void; + onInstall?: () => void | Promise; } export function PwaInstallDialog({ onClose, onInstall }: PwaInstallDialogProps) { - const handleInstall = () => { + const handleInstall = async () => { if (onInstall) { - onInstall(); + await onInstall(); } else { // 提示用户使用浏览器"添加到主屏幕"功能 window.alert( diff --git a/src/app/chat/components/pwa-install-overlay.tsx b/src/app/chat/components/pwa-install-overlay.tsx index b7b32616..dddfc007 100644 --- a/src/app/chat/components/pwa-install-overlay.tsx +++ b/src/app/chat/components/pwa-install-overlay.tsx @@ -32,6 +32,8 @@ export function PwaInstallOverlay() { // isInstalled() 检查 standalone display-mode(避免给已安装用户再弹) if (!pwaUtil.isSupported()) return; if (pwaUtil.isInstalled()) return; + // 提前挂 beforeinstallprompt 监听,避免用户点 OK 时事件已在更早时机丢失。 + pwaUtil.prepareInstallPrompt(); const isDev = process.env.NODE_ENV === "development"; @@ -92,13 +94,13 @@ function PwaInstallDialogMount({ }} onInstall={async () => { // 走 pwaUtil.install(): - // - 挂 beforeinstallprompt 监听(如未挂) - // - 等待 deferred 事件 ready(3s 超时) + // - 优先使用已缓存的 deferred prompt + // - 如未缓存,再等待 deferred 事件 ready(3s 超时) // - 调浏览器原生 prompt // - 返 "accepted" / "dismissed" / "unavailable" // 当前不消费返回值 —— 用户已经看到 dialog 点了 OK,是否真装上是浏览器的事。 // 后续可在此接 metrics 上报(pwa_accepted / pwa_dismissed / pwa_unavailable)。 - void pwaUtil.install(); + await pwaUtil.install(); }} /> ); diff --git a/src/app/splash/components/splash-screen.tsx b/src/app/splash/components/splash-screen.tsx index a3077873..d11edd9f 100644 --- a/src/app/splash/components/splash-screen.tsx +++ b/src/app/splash/components/splash-screen.tsx @@ -8,6 +8,7 @@ import { useAuthDispatch, useAuthState } from "@/stores/auth/auth-context"; import { useUserDispatch } from "@/stores/user/user-context"; import { AuthStorage } from "@/data/storage/auth/auth_storage"; import { ROUTES } from "@/router/routes"; +import { pwaUtil } from "@/utils/pwa"; import { SplashBackground } from "./splash-background"; import { SplashLogo } from "./splash-logo"; @@ -15,6 +16,11 @@ import { SplashContent } from "./splash-content"; import { SplashButton } from "./splash-button"; import styles from "./splash-screen.module.css"; +// 尽量早地缓存 beforeinstallprompt,避免用户停留 splash 时浏览器事件已触发并丢失。 +if (typeof window !== "undefined") { + pwaUtil.prepareInstallPrompt(); +} + export function SplashScreen() { const router = useRouter(); const state = useAuthState(); @@ -26,6 +32,10 @@ export function SplashScreen() { router.replace(ROUTES.chat); }; + useEffect(() => { + pwaUtil.prepareInstallPrompt(); + }, []); + // ───────────────────────────────────────────────────────────── // useEffect ① 启动时检查 localStorage 里的 loginToken: // - loginToken(OAuth/email sync 写下的)→ 视为"已登录"跳 /chat diff --git a/src/utils/pwa.ts b/src/utils/pwa.ts index 835f1b04..99658b63 100644 --- a/src/utils/pwa.ts +++ b/src/utils/pwa.ts @@ -2,11 +2,6 @@ /** * PwaUtil — PWA 工具类 * - * 三个核心能力(与 [src/hooks/use-pwa-install.ts] 对齐,但面向非 React 代码): - * - `isSupported()` 浏览器是否支持 PWA 安装(serviceWorker + manifest) - * - `isInstalled()` PWA 是否已安装为 standalone(避免重复提示) - * - `install()` 触发浏览器原生安装流程(`beforeinstallprompt`) - * * 设计要点: * - SSR 安全:所有方法 `typeof window === "undefined"` 守卫,SSR 全部返 false / "unavailable" * - 内部缓存 `beforeinstallprompt` 事件对象;install() 时如有缓存直接 prompt,否则等 @@ -80,6 +75,22 @@ export class PwaUtil { return false; } + /** + * 提前挂上 `beforeinstallprompt` 监听。 + * + * 目的:浏览器事件常常会在页面稳定后自行触发;如果等到用户点"安装"时才挂监听, + * 很容易错过那次事件,导致后续 `install()` 拿不到 deferred prompt。 + */ + prepareInstallPrompt(): void { + if (!this.isSupported()) return; + this.attachListener(); + } + + /** 当前是否已缓存可用的原生安装提示事件。 */ + canPromptInstall(): boolean { + return this.deferred !== null; + } + /** * 触发 PWA 安装(浏览器原生安装提示)。 * @@ -170,4 +181,4 @@ export class PwaUtil { } /** 全局单例(与 `deviceIdentifier` / `SpAsyncUtil` 命名约定一致) */ -export const pwaUtil = new PwaUtil(); \ No newline at end of file +export const pwaUtil = new PwaUtil();