"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();
}}
/>
);
}