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:
@@ -13,13 +13,13 @@ import styles from "./pwa-install-dialog.module.css";
|
||||
|
||||
export interface PwaInstallDialogProps {
|
||||
onClose: () => void;
|
||||
onInstall?: () => void;
|
||||
onInstall?: () => void | Promise<void>;
|
||||
}
|
||||
|
||||
export function PwaInstallDialog({ onClose, onInstall }: PwaInstallDialogProps) {
|
||||
const handleInstall = () => {
|
||||
const handleInstall = async () => {
|
||||
if (onInstall) {
|
||||
onInstall();
|
||||
await onInstall();
|
||||
} else {
|
||||
// 提示用户使用浏览器"添加到主屏幕"功能
|
||||
window.alert(
|
||||
|
||||
@@ -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();
|
||||
}}
|
||||
/>
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user