From a2e23101c0b39371724a74345d7ed750dc61c135 Mon Sep 17 00:00:00 2001 From: baixiangcpp Date: Fri, 17 Jul 2026 09:21:07 -0600 Subject: [PATCH] Honor persisted PWA install status (#260) --- src/components/layout/app-runtime.tsx | 14 +------ src/core/pwa/install-prompt-store.ts | 22 ++++++++++ .../components/install-app-client.tsx | 10 +---- src/generated/route-width-inventory.json | 2 +- tests/component/install-app-page.test.tsx | 41 +++++++++++++++++++ 5 files changed, 67 insertions(+), 22 deletions(-) diff --git a/src/components/layout/app-runtime.tsx b/src/components/layout/app-runtime.tsx index e366b2bc..f86ce9ee 100644 --- a/src/components/layout/app-runtime.tsx +++ b/src/components/layout/app-runtime.tsx @@ -8,6 +8,7 @@ import { Button } from "@/components/ui/button" import { requireTranslationValue } from "@/core/i18n/i18n" import { useLang } from "@/core/i18n/lang-provider" import { + isPwaInstalled, PWA_INSTALL_INSTALLED_KEY, PWA_INSTALL_SESSION_PROMPTED_KEY, type BeforeInstallPromptEvent, @@ -35,19 +36,6 @@ type AppRuntimeProps = { onInstallPromptConsumed?: () => void } -function isPwaInstalled(): boolean { - if (typeof window === "undefined") return false - const standaloneDisplayMode = window.matchMedia("(display-mode: standalone)").matches - const iosStandalone = (window.navigator as Navigator & { standalone?: boolean }).standalone === true - let persistedInstalledFlag = false - try { - persistedInstalledFlag = window.localStorage.getItem(PWA_INSTALL_INSTALLED_KEY) === "1" - } catch { - persistedInstalledFlag = false - } - return standaloneDisplayMode || iosStandalone || persistedInstalledFlag -} - function setPwaInstallStorageValue(key: string, value: string): void { try { window.localStorage.setItem(key, value) diff --git a/src/core/pwa/install-prompt-store.ts b/src/core/pwa/install-prompt-store.ts index 8e580f9b..45f11f9c 100644 --- a/src/core/pwa/install-prompt-store.ts +++ b/src/core/pwa/install-prompt-store.ts @@ -20,6 +20,28 @@ type PwaInstallPromptWindow = Window & { [PWA_INSTALL_PROMPT_SLOT]?: BeforeInstallPromptEvent | null } +export function isPwaInstalled(): boolean { + if (typeof window === "undefined") return false + + let standaloneDisplayMode = false + try { + standaloneDisplayMode = typeof window.matchMedia === "function" + && window.matchMedia("(display-mode: standalone)").matches + } catch { + standaloneDisplayMode = false + } + + const iosStandalone = (window.navigator as Navigator & { standalone?: boolean }).standalone === true + let persistedInstalledFlag = false + try { + persistedInstalledFlag = window.localStorage.getItem(PWA_INSTALL_INSTALLED_KEY) === "1" + } catch { + persistedInstalledFlag = false + } + + return standaloneDisplayMode || iosStandalone || persistedInstalledFlag +} + function readWindowPrompt(): BeforeInstallPromptEvent | null { if (typeof window === "undefined") return null return (window as PwaInstallPromptWindow)[PWA_INSTALL_PROMPT_SLOT] ?? null diff --git a/src/features/install-app/components/install-app-client.tsx b/src/features/install-app/components/install-app-client.tsx index c4f40bdf..9b4f317d 100644 --- a/src/features/install-app/components/install-app-client.tsx +++ b/src/features/install-app/components/install-app-client.tsx @@ -11,6 +11,7 @@ import type { Locale } from "@/core/i18n/i18n" import { JsonLdScript } from "@/core/seo/components/json-ld-script" import { consumePwaInstallPrompt, + isPwaInstalled, usePwaInstallPrompt, } from "@/core/pwa/install-prompt-store" import { clearByteflowPwaCaches } from "@/core/storage/pwa-cache-controls" @@ -18,13 +19,6 @@ import type { GuidePlatform, InstallPageCopy } from "@/core/utils/install-app-co import { StaticPageContainer } from "@/components/layout/page-container" import { detectInstallGuidePlatform } from "../install-guide-platform" -function isStandaloneInstalled() { - if (typeof window === "undefined") return false - const standaloneDisplayMode = window.matchMedia("(display-mode: standalone)").matches - const iosStandalone = (window.navigator as Navigator & { standalone?: boolean }).standalone === true - return standaloneDisplayMode || iosStandalone -} - const BENEFIT_ICON_BY_KEY = { instant_launch: Zap, works_offline: WifiOff, @@ -81,7 +75,7 @@ export function InstallAppClient({ }, [locale]) React.useEffect(() => { - setInstalled(isStandaloneInstalled()) + setInstalled(isPwaInstalled()) const onInstalled = () => { setInstalled(true) diff --git a/src/generated/route-width-inventory.json b/src/generated/route-width-inventory.json index b1fd954d..4013758c 100644 --- a/src/generated/route-width-inventory.json +++ b/src/generated/route-width-inventory.json @@ -1035,7 +1035,7 @@ { "file": "src/components/layout/app-runtime.tsx", "element": "div", - "line": 267, + "line": 255, "reason": "runtime-banner", "maxWidthTokens": [ "max-w-6xl" diff --git a/tests/component/install-app-page.test.tsx b/tests/component/install-app-page.test.tsx index cd5c4ad2..070819d0 100644 --- a/tests/component/install-app-page.test.tsx +++ b/tests/component/install-app-page.test.tsx @@ -5,6 +5,7 @@ import { capturePwaInstallPrompt, consumePwaInstallPrompt, getPwaInstallPromptSnapshot, + PWA_INSTALL_INSTALLED_KEY, } from "@/core/pwa/install-prompt-store" import { getAllToolsHref } from "@/core/routing/all-tools-route" import { getInstallPageCopy } from "@/core/utils/install-app-copy" @@ -30,6 +31,7 @@ vi.mock("@/core/analytics/analytics", () => ({ describe("install app page", () => { beforeEach(() => { consumePwaInstallPrompt() + window.localStorage.clear() if (!window.matchMedia) { Object.defineProperty(window, "matchMedia", { writable: true, @@ -204,4 +206,43 @@ describe("install app page", () => { await waitFor(() => expect(getPwaInstallPromptSnapshot()).toBeNull()) await waitFor(() => expect(screen.getAllByText(copy.manualHint).length).toBeGreaterThan(0)) }) + + it("restores persisted install evidence and clears it when a fresh prompt arrives", async () => { + const copy = getInstallPageCopy("en") + window.localStorage.setItem(PWA_INSTALL_INSTALLED_KEY, "1") + + render( + , + ) + + await waitFor(() => { + const installedActions = screen.getAllByRole("button", { name: copy.alreadyInstalled }) + expect(installedActions).toHaveLength(2) + installedActions.forEach((action) => expect(action).toBeDisabled()) + }) + + const beforeInstallPromptEvent = Object.assign(new Event("beforeinstallprompt", { cancelable: true }), { + prompt: vi.fn().mockResolvedValue(undefined), + userChoice: Promise.resolve({ outcome: "dismissed", platform: "web" }), + }) + await act(async () => { + capturePwaInstallPrompt(beforeInstallPromptEvent) + }) + + expect(window.localStorage.getItem(PWA_INSTALL_INSTALLED_KEY)).toBeNull() + await waitFor(() => { + expect(screen.getAllByRole("button", { name: copy.installNow })).toHaveLength(2) + }) + expect(screen.queryByRole("button", { name: copy.alreadyInstalled })).not.toBeInTheDocument() + }) })