fix(pwa): prepare beforeinstallprompt listener early to avoid event loss
The previous install flow had a timing race: the `beforeinstallprompt` event fires at unpredictable times after page load (often ~30s+ into the session, when Chrome's heuristic finally decides the user is engaged). If the listener was attached only inside the dialog's onClick handler, by the time the user clicked OK the event had already fired into the void and the deferred prompt was null — so `install()` returned "unavailable" every time. The new flow attaches the listener as early as possible: 1. `pwaUtil.prepareInstallPrompt()` — new public method that calls the private `attachListener()`. Idempotent (guard via `listenersAttached`). 2. `pwaUtil.canPromptInstall()` — companion getter that returns whether a deferred event is currently cached. Useful for UI to decide whether to even show the install CTA. 3. `pwa-install-overlay.tsx` — calls `prepareInstallPrompt()` right after the isSupported / isInstalled guards. The overlay mounts when the user enters /chat, so this is the earliest practical point in the user flow. 4. `pwa-screen.tsx` — calls `prepareInstallPrompt()` BOTH at module load (synchronous, so the listener is attached before the splash screen's useEffects run) AND in a useEffect for robustness. The splash screen is the very first screen the user sees, so attaching here is the earliest possible. 5. `pwa-install-dialog.tsx` — `handleInstall` is now async, and `onInstall` accepts `() => void | Promise<void>`. The overlay calls `await pwaUtil.install()` so the deferred prompt flow can complete before the dialog unmounts. Together: the listener is now attached from the moment the splash screen module loads, the deferred prompt is captured whenever Chrome decides to fire the event, and the dialog's install button can reliably `prompt()` against the cached deferred. The full install flow now works end-to-end.
This commit is contained in:
+17
-6
@@ -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();
|
||||
export const pwaUtil = new PwaUtil();
|
||||
|
||||
Reference in New Issue
Block a user