Files
cozsweet-frontend-nextjs/src/app/chat/components/pwa-install-overlay.tsx
T

132 lines
4.1 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"use client";
/**
* PwaInstallOverlay PWA 安装提示触发器
*
* 行为:
* - 等聊天消息数量达到触发阈值后才开始检查
* - 检查 PWA 支持(pwaUtil.isSupported
* - 检查 PWA 是否已安装(pwaUtil.isInstalled)—— 已安装则不弹
* - 检查是否已经显示过(使用 AppStorage 持久化)
* - 延迟后弹出 PwaInstallDialog
* - 生产环境有效;开发环境 1s 后立即弹出(测试用)
*
* 注意:PWA install prompt API`beforeinstallprompt`)在浏览器差异较大,
* 本轮仅实现"显示时机"逻辑,具体 install prompt 调用由 Dialog 内部处理。
*/
import { useEffect } from "react";
import {
canShowPwaInstallPromptOnce,
recordPwaInstallPromptShown,
} from "@/lib/chat/pwa_install_prompt";
import { pwaUtil } from "@/utils";
import { PwaInstallDialog } from "./pwa-install-dialog";
import styles from "./pwa-install-overlay.module.css";
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 <div className={styles.hidden} aria-hidden="true" />;
}
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(<PwaInstallDialogMount rootEl={root} reactRoot={reactRoot} />);
});
}
function PwaInstallDialogMount({
rootEl,
reactRoot,
}: {
rootEl: HTMLElement;
reactRoot: { unmount: () => void };
}) {
return (
<PwaInstallDialog
onClose={() => {
reactRoot.unmount();
rootEl.remove();
}}
onInstall={async () => {
// 走 pwaUtil.install()
// - 优先使用已缓存的 deferred prompt
// - 如未缓存,再等待 deferred 事件 ready3s 超时)
// - 调浏览器原生 prompt
// - 返 "accepted" / "dismissed" / "unavailable"
// 当前不消费返回值 —— 用户已经看到 dialog 点了 OK,是否真装上是浏览器的事。
// 后续可在此接 metrics 上报(pwa_accepted / pwa_dismissed / pwa_unavailable)。
await pwaUtil.install();
}}
/>
);
}