fix(pwa): show install prompt only once
This commit is contained in:
@@ -6,8 +6,8 @@
|
|||||||
* - 等聊天消息数量达到触发阈值后才开始检查
|
* - 等聊天消息数量达到触发阈值后才开始检查
|
||||||
* - 检查 PWA 支持(pwaUtil.isSupported)
|
* - 检查 PWA 支持(pwaUtil.isSupported)
|
||||||
* - 检查 PWA 是否已安装(pwaUtil.isInstalled)—— 已安装则不弹
|
* - 检查 PWA 是否已安装(pwaUtil.isInstalled)—— 已安装则不弹
|
||||||
* - 检查每日是否已显示(使用 AppStorage 持久化)
|
* - 检查是否已经显示过(使用 AppStorage 持久化)
|
||||||
* - 延迟 3.5s 后弹出 PwaInstallDialog
|
* - 延迟后弹出 PwaInstallDialog
|
||||||
* - 生产环境有效;开发环境 1s 后立即弹出(测试用)
|
* - 生产环境有效;开发环境 1s 后立即弹出(测试用)
|
||||||
*
|
*
|
||||||
* 注意:PWA install prompt API(`beforeinstallprompt`)在浏览器差异较大,
|
* 注意:PWA install prompt API(`beforeinstallprompt`)在浏览器差异较大,
|
||||||
@@ -15,12 +15,15 @@
|
|||||||
*/
|
*/
|
||||||
import { useEffect } from "react";
|
import { useEffect } from "react";
|
||||||
|
|
||||||
import { BrowserDetector, SpAsyncUtil, pwaUtil } from "@/utils";
|
import {
|
||||||
|
canShowPwaInstallPromptOnce,
|
||||||
|
recordPwaInstallPromptShown,
|
||||||
|
} from "@/lib/chat/pwa_install_prompt";
|
||||||
|
import { BrowserDetector, pwaUtil } from "@/utils";
|
||||||
|
|
||||||
import { PwaInstallDialog } from "./pwa-install-dialog";
|
import { PwaInstallDialog } from "./pwa-install-dialog";
|
||||||
import styles from "./pwa-install-overlay.module.css";
|
import styles from "./pwa-install-overlay.module.css";
|
||||||
|
|
||||||
const PWA_DIALOG_KEY = "cozsweet:lastPwaDialogShown";
|
|
||||||
const DEVELOPMENT_DIALOG_DELAY_MS = 1000;
|
const DEVELOPMENT_DIALOG_DELAY_MS = 1000;
|
||||||
const PRODUCTION_DIALOG_DELAY_MS = 1500;
|
const PRODUCTION_DIALOG_DELAY_MS = 1500;
|
||||||
|
|
||||||
@@ -70,22 +73,17 @@ async function shouldShowPwaInstallDialog(isDevelopment: boolean) {
|
|||||||
if (pwaUtil.isInstalled()) return false;
|
if (pwaUtil.isInstalled()) return false;
|
||||||
if (BrowserDetector.isInAppBrowser()) return false;
|
if (BrowserDetector.isInAppBrowser()) return false;
|
||||||
|
|
||||||
// 开发环境:跳过"每日一次"门禁,每次进入都弹,方便 UI 测试。
|
// 开发环境:跳过"只弹一次"门禁,每次进入都弹,方便 UI 测试。
|
||||||
if (isDevelopment) return true;
|
if (isDevelopment) return true;
|
||||||
|
|
||||||
// 生产环境:每日只弹一次(防骚扰)。
|
// 生产环境:永久只弹一次(防骚扰)。
|
||||||
const lastResult = await SpAsyncUtil.getString(PWA_DIALOG_KEY);
|
return canShowPwaInstallPromptOnce();
|
||||||
if (!lastResult.success) return false;
|
|
||||||
|
|
||||||
const today = new Date().toISOString().slice(0, 10);
|
|
||||||
return lastResult.data !== today;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function showPwaDialog() {
|
function showPwaDialog() {
|
||||||
if (typeof document === "undefined") return;
|
if (typeof document === "undefined") return;
|
||||||
// 记录显示时间
|
// 记录已显示,后续不再弹出。
|
||||||
const today = new Date().toISOString().slice(0, 10);
|
void recordPwaInstallPromptShown();
|
||||||
void SpAsyncUtil.setString(PWA_DIALOG_KEY, today);
|
|
||||||
|
|
||||||
// 创建挂载点
|
// 创建挂载点
|
||||||
const root = document.createElement("div");
|
const root = document.createElement("div");
|
||||||
|
|||||||
@@ -4,30 +4,41 @@
|
|||||||
* AppStorage 完整实现(基于 unstorage 抽象)
|
* AppStorage 完整实现(基于 unstorage 抽象)
|
||||||
*
|
*
|
||||||
* 对齐 Dart 端 `AppStorage`(lib/data/services/storage/app_storage.dart):
|
* 对齐 Dart 端 `AppStorage`(lib/data/services/storage/app_storage.dart):
|
||||||
* - PWA 对话框每日展示 / PWA 事件每日上报 / app-info 每日上报
|
* - PWA 对话框一次性展示 / PWA 事件每日上报 / app-info 每日上报
|
||||||
* - 内部用 `yyyy-MM-dd` 字符串比对实现"每日一次"语义
|
|
||||||
*
|
*
|
||||||
* 注意:Dart 端是 `static class` 没有接口,本 TS 版本同样保持静态方法风格。
|
* 注意:Dart 端是 `static class` 没有接口,本 TS 版本同样保持静态方法风格。
|
||||||
* 通过 SpAsyncUtil 静态类访问底层存储(浏览器 localStorage / SSR memory 自动切换)。
|
* 通过 SpAsyncUtil 静态类访问底层存储(浏览器 localStorage / SSR memory 自动切换)。
|
||||||
*
|
*
|
||||||
* 语义:
|
* PWA 对话框使用布尔标记;每日上报类方法仍使用 `yyyy-MM-dd` 字符串比对。
|
||||||
* - `canXxx(today)` 返回 `true` 当且仅当"自上次记录以来日期变了"
|
|
||||||
* (即 stored !== today)。首次调用(key 不存在)也返回 `true`。
|
|
||||||
* - `recordXxx(today)` 写入 todayString,后续 `canXxx` 在同一天返回 `false`。
|
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { Result, type Result as ResultT, SpAsyncUtil } from "@/utils";
|
import { Result, type Result as ResultT, SpAsyncUtil } from "@/utils";
|
||||||
import { StorageKeys } from "../storage_keys";
|
import { StorageKeys } from "../storage_keys";
|
||||||
|
|
||||||
|
const LEGACY_PWA_DIALOG_SHOWN_KEYS = [
|
||||||
|
"cozsweet:lastPwaDialogShown",
|
||||||
|
"last_pwa_dialog_shown",
|
||||||
|
];
|
||||||
|
|
||||||
export class AppStorage {
|
export class AppStorage {
|
||||||
// ---- PWA install dialog ----
|
// ---- PWA install dialog ----
|
||||||
|
|
||||||
static async canShowPwaDialog(todayString: string): Promise<ResultT<boolean>> {
|
static async canShowPwaDialog(): Promise<ResultT<boolean>> {
|
||||||
return AppStorage.canReportKey(StorageKeys.lastPwaDialogShown, todayString);
|
const shownResult = await SpAsyncUtil.getBool(StorageKeys.pwaDialogShown);
|
||||||
|
if (!shownResult.success) return shownResult;
|
||||||
|
if (shownResult.data === true) return Result.ok(false);
|
||||||
|
|
||||||
|
for (const key of LEGACY_PWA_DIALOG_SHOWN_KEYS) {
|
||||||
|
const legacyResult = await SpAsyncUtil.getString(key);
|
||||||
|
if (!legacyResult.success) return legacyResult;
|
||||||
|
if (legacyResult.data !== null) return Result.ok(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
static recordPwaDialogShown(todayString: string): Promise<ResultT<void>> {
|
return Result.ok(true);
|
||||||
return SpAsyncUtil.setString(StorageKeys.lastPwaDialogShown, todayString);
|
}
|
||||||
|
|
||||||
|
static recordPwaDialogShown(): Promise<ResultT<void>> {
|
||||||
|
return SpAsyncUtil.setBool(StorageKeys.pwaDialogShown, true);
|
||||||
}
|
}
|
||||||
|
|
||||||
// ---- external browser dialog ----
|
// ---- external browser dialog ----
|
||||||
|
|||||||
@@ -28,7 +28,7 @@ export const StorageKeys = {
|
|||||||
pendingChatUnlock: "pending_chat_unlock",
|
pendingChatUnlock: "pending_chat_unlock",
|
||||||
|
|
||||||
// pwa / app info
|
// pwa / app info
|
||||||
lastPwaDialogShown: "last_pwa_dialog_shown",
|
pwaDialogShown: "pwa_dialog_shown",
|
||||||
lastExternalBrowserDialogShown: "last_external_browser_dialog_shown",
|
lastExternalBrowserDialogShown: "last_external_browser_dialog_shown",
|
||||||
lastPwaEventReported: "last_pwa_event_reported",
|
lastPwaEventReported: "last_pwa_event_reported",
|
||||||
lastAppInfoReported: "last_app_info_reported",
|
lastAppInfoReported: "last_app_info_reported",
|
||||||
|
|||||||
@@ -0,0 +1,12 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import { AppStorage } from "@/data/storage/app/app_storage";
|
||||||
|
|
||||||
|
export async function canShowPwaInstallPromptOnce(): Promise<boolean> {
|
||||||
|
const result = await AppStorage.canShowPwaDialog();
|
||||||
|
return result.success && result.data;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function recordPwaInstallPromptShown(): Promise<unknown> {
|
||||||
|
return AppStorage.recordPwaDialogShown();
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user