Make PWA install hint dismiss reliable across reloads.

Use delegated clicks, explicit hide state, and init before service worker registration.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-06-20 11:34:46 +02:00
parent ae589eed92
commit 6ba4cf447f
4 changed files with 70 additions and 17 deletions
+46 -13
View File
@@ -4,6 +4,7 @@ const Pwa = (() => {
const DISMISS_KEY = "pwa-hint-dismissed"; const DISMISS_KEY = "pwa-hint-dismissed";
let deferredPrompt = null; let deferredPrompt = null;
let refreshInstallHint = null; let refreshInstallHint = null;
let hintBound = false;
function isStandalone() { function isStandalone() {
return window.matchMedia("(display-mode: standalone)").matches return window.matchMedia("(display-mode: standalone)").matches
@@ -42,27 +43,64 @@ const Pwa = (() => {
return "pwa.hintDesktop"; return "pwa.hintDesktop";
} }
function setupInstallHint() { function hideHint(hint, { persist = true } = {}) {
if (isStandalone()) return null; if (persist) {
if (localStorage.getItem(DISMISS_KEY) === "1") return null; try {
localStorage.setItem(DISMISS_KEY, "1");
} catch {
/* private mode with storage blocked still hide for this session */
}
}
hint.hidden = true;
hint.classList.add("is-dismissed");
}
function showHint(hint) {
hint.hidden = false;
hint.classList.remove("is-dismissed");
}
function bindHintActions(hint) {
if (hintBound) return;
hintBound = true;
hint.addEventListener("click", (event) => {
if (event.target.closest(".pwa-hint-dismiss")) {
event.preventDefault();
hideHint(hint);
}
});
}
function setupInstallHint() {
const hint = document.getElementById("pwa-install-hint"); const hint = document.getElementById("pwa-install-hint");
if (!hint) return null; if (!hint) return null;
bindHintActions(hint);
if (isStandalone()) {
hideHint(hint, { persist: false });
return null;
}
if (localStorage.getItem(DISMISS_KEY) === "1") {
hideHint(hint, { persist: false });
return null;
}
const body = document.getElementById("pwa-hint-body"); const body = document.getElementById("pwa-hint-body");
const installBtn = document.getElementById("pwa-install-btn"); const installBtn = document.getElementById("pwa-install-btn");
const dismissBtn = document.getElementById("pwa-hint-dismiss"); const dismissBtn = hint.querySelector(".pwa-hint-dismiss");
const updateHintText = () => { const updateHintText = () => {
const title = hint.querySelector("[data-i18n='pwa.hintTitle']"); const title = hint.querySelector("[data-i18n='pwa.hintTitle']");
if (title) title.textContent = t("pwa.hintTitle"); if (title) title.textContent = t("pwa.hintTitle");
if (body && !deferredPrompt) body.textContent = t(platformHintKey()); if (body && !deferredPrompt) body.textContent = t(platformHintKey());
if (body && deferredPrompt) body.textContent = t("pwa.hintBody"); if (body && deferredPrompt) body.textContent = t("pwa.hintBody");
if (dismissBtn) dismissBtn.title = t("actions.dismiss"); if (dismissBtn) dismissBtn.setAttribute("aria-label", t("actions.dismiss"));
if (installBtn && !installBtn.hidden) installBtn.textContent = t("pwa.install"); if (installBtn && !installBtn.hidden) installBtn.textContent = t("pwa.install");
}; };
hint.hidden = false; showHint(hint);
updateHintText(); updateHintText();
window.addEventListener("beforeinstallprompt", (e) => { window.addEventListener("beforeinstallprompt", (e) => {
@@ -73,11 +111,6 @@ const Pwa = (() => {
updateHintText(); updateHintText();
}); });
dismissBtn?.addEventListener("click", () => {
localStorage.setItem(DISMISS_KEY, "1");
hint.hidden = true;
});
installBtn?.addEventListener("click", async () => { installBtn?.addEventListener("click", async () => {
if (!deferredPrompt) return; if (!deferredPrompt) return;
deferredPrompt.prompt(); deferredPrompt.prompt();
@@ -86,7 +119,7 @@ const Pwa = (() => {
installBtn.hidden = true; installBtn.hidden = true;
if (outcome === "accepted") { if (outcome === "accepted") {
if (body) body.textContent = t("pwa.installed"); if (body) body.textContent = t("pwa.installed");
setTimeout(() => { hint.hidden = true; }, 3000); setTimeout(() => { hideHint(hint); }, 3000);
} else { } else {
updateHintText(); updateHintText();
} }
@@ -96,8 +129,8 @@ const Pwa = (() => {
} }
async function init() { async function init() {
await registerServiceWorker();
refreshInstallHint = setupInstallHint(); refreshInstallHint = setupInstallHint();
await registerServiceWorker();
} }
function refreshHint() { function refreshHint() {
+22 -2
View File
@@ -856,8 +856,9 @@ body.inv-chart-modal-open {
} }
/* PWA install hint */ /* PWA install hint */
.pwa-hint[hidden] { .pwa-hint[hidden],
display: none; .pwa-hint.is-dismissed {
display: none !important;
} }
.pwa-hint { .pwa-hint {
@@ -898,6 +899,25 @@ body.inv-chart-modal-open {
flex-shrink: 0; flex-shrink: 0;
} }
.pwa-hint-dismiss {
background: none;
border: none;
color: var(--text-muted);
font-size: 1.25rem;
line-height: 1;
cursor: pointer;
min-width: 32px;
min-height: 32px;
padding: 4px;
border-radius: 6px;
flex-shrink: 0;
}
.pwa-hint-dismiss:hover {
color: var(--text);
background: var(--bg-hover);
}
.landing-card .pwa-hint { .landing-card .pwa-hint {
margin-top: 8px; margin-top: 8px;
margin-bottom: 0; margin-bottom: 0;
+1 -1
View File
@@ -1,4 +1,4 @@
const CACHE = "if-viewer-static-v4"; const CACHE = "if-viewer-static-v5";
const ASSETS = [ const ASSETS = [
"/static/style.css", "/static/style.css",
"/static/favicon.svg", "/static/favicon.svg",
+1 -1
View File
@@ -5,6 +5,6 @@
</div> </div>
<div class="pwa-hint-actions"> <div class="pwa-hint-actions">
<button type="button" id="pwa-install-btn" class="viewer-copy-btn" hidden data-i18n="pwa.install">Install</button> <button type="button" id="pwa-install-btn" class="viewer-copy-btn" hidden data-i18n="pwa.install">Install</button>
<button type="button" id="pwa-hint-dismiss" class="import-report-dismiss" data-i18n-title="actions.dismiss" title="Dismiss">×</button> <button type="button" class="pwa-hint-dismiss" aria-label="Dismiss">×</button>
</div> </div>
</div> </div>