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

113 lines
3.7 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 持久化)
* - 延迟 3.5s 后弹出 PwaInstallDialog
* - 生产环境有效;开发环境 1s 后立即弹出(测试用)
*
* 注意:PWA install prompt API`beforeinstallprompt`)在浏览器差异较大,
* 本轮仅实现"显示时机"逻辑,具体 install prompt 调用由 Dialog 内部处理。
*/
import { useEffect } from "react";
import { BrowserDetector, AppEnvUtil, SpAsyncUtil, pwaUtil } from "@/utils";
import { PwaInstallDialog } from "./pwa-install-dialog";
import styles from "./pwa-install-overlay.module.css";
const PWA_DIALOG_KEY = "cozsweet:lastPwaDialogShown";
export function PwaInstallOverlay() {
useEffect(() => {
let mounted = true;
const init = async () => {
// const isDev = AppEnvUtil.isDevelopment();
const isDev = false;
const shouldShowDialog = await shouldShowPwaInstallDialog(isDev);
if (!shouldShowDialog) return;
// 提前挂 beforeinstallprompt 监听,避免用户点 OK 时事件已在更早时机丢失。
pwaUtil.prepareInstallPrompt();
// 开发环境 1s 后弹(快);生产环境 3.5s 后弹(让用户先看到聊天内容)
const delay = isDev ? 1000 : 3500;
setTimeout(() => {
if (!mounted) return;
showPwaDialog();
}, delay);
};
void init();
return () => {
mounted = false;
};
}, []);
return <div className={styles.hidden} aria-hidden="true" />;
}
async function shouldShowPwaInstallDialog(isDevelopment: boolean) {
// PWA 支持 / 安装 双重门禁(两种环境都检查)。
if (!pwaUtil.isSupported()) return false;
if (pwaUtil.isInstalled()) return false;
if (BrowserDetector.isInAppBrowser()) return false;
// 开发环境:跳过"每日一次"门禁,每次进入都弹,方便 UI 测试。
if (isDevelopment) return true;
// 生产环境:每日只弹一次(防骚扰)。
const lastResult = await SpAsyncUtil.getString(PWA_DIALOG_KEY);
if (!lastResult.success) return false;
const today = new Date().toISOString().slice(0, 10);
return lastResult.data !== today;
}
function showPwaDialog() {
if (typeof document === "undefined") return;
// 记录显示时间
const today = new Date().toISOString().slice(0, 10);
void SpAsyncUtil.setString(PWA_DIALOG_KEY, today);
// 创建挂载点
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();
}}
/>
);
}