"use client"; /** * PwaInstallOverlay PWA 安装提示触发器 * * 行为: * - 等聊天消息数量达到触发阈值后才开始检查 * - 检查 PWA 支持(pwaUtil.isSupported) * - 检查 PWA 是否已安装(pwaUtil.isInstalled)—— 已安装则不弹 * - 检查是否已经显示过(使用 AppStorage 持久化) * - 延迟后弹出 PwaInstallDialog * - 生产环境有效;开发环境 1s 后立即弹出(测试用) * * 注意:PWA install prompt API(`beforeinstallprompt`)在浏览器差异较大, * 本轮仅实现"显示时机"逻辑,具体 install prompt 调用由 PwaInstallDialog 处理。 */ import { useEffect } from "react"; import { canShowPwaInstallPromptOnce, recordPwaInstallPromptShown, } from "@/lib/chat/pwa_install_prompt"; import { pwaUtil } from "@/utils/pwa"; import { PwaInstallDialog } from "./pwa-install-dialog"; const DEVELOPMENT_DIALOG_DELAY_MS = 1000; const PRODUCTION_DIALOG_DELAY_MS = 1500; export interface PwaInstallOverlayProps { enabled: boolean; } export function PwaInstallOverlay({ enabled }: PwaInstallOverlayProps) { useEffect(() => { if (!enabled) return; let mounted = true; let timer: number | undefined; let unsubscribe: (() => void) | undefined; const init = async () => { // const isDev = AppEnvUtil.isDevelopment(); const isDev = false; // 提前挂 beforeinstallprompt 监听,避免用户点 OK 时事件已在更早时机丢失。 pwaUtil.prepareInstallPrompt(); const tryScheduleDialog = async () => { const shouldShowDialog = await shouldShowPwaInstallDialog(isDev); if (!mounted || !shouldShowDialog || timer !== undefined) return; // 达到聊天数量阈值后稍作停顿,避免弹窗与消息刷新挤在同一瞬间。 const delay = isDev ? DEVELOPMENT_DIALOG_DELAY_MS : PRODUCTION_DIALOG_DELAY_MS; timer = window.setTimeout(() => { if (!mounted) return; showPwaDialog(); }, delay); }; await tryScheduleDialog(); unsubscribe = pwaUtil.subscribe(() => { void tryScheduleDialog(); }); }; void init(); return () => { mounted = false; unsubscribe?.(); if (timer !== undefined) window.clearTimeout(timer); }; }, [enabled]); return null; } async function shouldShowPwaInstallDialog(isDevelopment: boolean) { // PWA 支持 / 安装 双重门禁(两种环境都检查)。 if (!pwaUtil.canShowInstallEntry()) return false; // 开发环境:跳过"只弹一次"门禁,每次进入都弹,方便 UI 测试。 if (isDevelopment) return true; // 生产环境:永久只弹一次(防骚扰)。 return canShowPwaInstallPromptOnce(); } function showPwaDialog() { if (typeof document === "undefined") return; // 记录已显示,后续不再弹出。 void recordPwaInstallPromptShown(); // 创建挂载点 const root = document.createElement("div"); document.body.appendChild(root); // 用 React DOM 渲染(通过 import) import("react-dom/client").then(({ createRoot }) => { const reactRoot = createRoot(root); reactRoot.render(); }); } function PwaInstallDialogMount({ rootEl, reactRoot, }: { rootEl: HTMLElement; reactRoot: { unmount: () => void }; }) { return ( { reactRoot.unmount(); rootEl.remove(); }} onInstall={async () => { // 走 pwaUtil.install(): // - 优先使用已缓存的 deferred prompt // - 如未缓存,再等待 deferred 事件 ready(3s 超时) // - 调浏览器原生 prompt // - 返 "accepted" / "dismissed" / "unavailable" // 当前不消费返回值 —— 用户已经看到 dialog 点了 OK,是否真装上是浏览器的事。 // 后续可在此接 metrics 上报(pwa_accepted / pwa_dismissed / pwa_unavailable)。 await pwaUtil.install(); }} /> ); }