Fix PWA install banner: persist dismiss, prevent duplicates

- Add appinstalled event listener so browser-initiated installs also
  permanently dismiss the banner
- Ensure only one banner shows at a time (update hides install first)
- Refactor dismiss logic into single helper function

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Jeff Emmett 2026-04-01 12:07:17 -07:00
parent a4cd977d17
commit fef217798e
1 changed files with 19 additions and 12 deletions

View File

@ -361,6 +361,9 @@ export function renderShell(opts: ShellOptions): string {
if (!isStandalone) return; // Only show update prompt in installed PWA if (!isStandalone) return; // Only show update prompt in installed PWA
const b = document.getElementById('pwa-update-banner'); const b = document.getElementById('pwa-update-banner');
if (!b || b.style.display !== 'none') return; if (!b || b.style.display !== 'none') return;
// Hide install banner first if somehow both would show
const ib = document.getElementById('pwa-install-banner');
if (ib) ib.style.display = 'none';
b.style.display = ''; b.style.display = '';
document.body.classList.add('rspace-banner-visible'); document.body.classList.add('rspace-banner-visible');
} }
@ -382,31 +385,34 @@ export function renderShell(opts: ShellOptions): string {
} }
// Install banner — browser only (not shown in installed PWA) // Install banner — browser only (not shown in installed PWA)
// Permanently dismissed once closed or installed (localStorage key persists)
if (!isStandalone) { if (!isStandalone) {
const installDismissed = () => localStorage.getItem('rspace_install_dismissed') === '1';
const dismissInstall = () => {
const b = document.getElementById('pwa-install-banner');
if (b) b.style.display = 'none';
document.body.classList.remove('rspace-banner-visible');
localStorage.setItem('rspace_install_dismissed', '1');
};
window.addEventListener("beforeinstallprompt", (e) => { window.addEventListener("beforeinstallprompt", (e) => {
e.preventDefault(); e.preventDefault();
window.__rspaceInstallPrompt = () => { e.prompt(); return e.userChoice; }; window.__rspaceInstallPrompt = () => { e.prompt(); return e.userChoice; };
window.dispatchEvent(new CustomEvent("rspace-install-available")); window.dispatchEvent(new CustomEvent("rspace-install-available"));
if (localStorage.getItem('rspace_install_dismissed') !== '1') { if (!installDismissed()) {
const b = document.getElementById('pwa-install-banner'); const b = document.getElementById('pwa-install-banner');
if (b) { b.style.display = ''; document.body.classList.add('rspace-banner-visible'); } if (b) { b.style.display = ''; document.body.classList.add('rspace-banner-visible'); }
} }
}); });
// User installed via browser UI (not our button) — still dismiss permanently
window.addEventListener("appinstalled", () => { dismissInstall(); });
document.getElementById('pwa-install-btn')?.addEventListener('click', async () => { document.getElementById('pwa-install-btn')?.addEventListener('click', async () => {
if (window.__rspaceInstallPrompt) { if (window.__rspaceInstallPrompt) {
const choice = await window.__rspaceInstallPrompt(); const choice = await window.__rspaceInstallPrompt();
if (choice?.outcome === 'accepted') { if (choice?.outcome === 'accepted') dismissInstall();
document.getElementById('pwa-install-banner').style.display = 'none';
document.body.classList.remove('rspace-banner-visible');
localStorage.setItem('rspace_install_dismissed', '1');
}
} }
}); });
document.getElementById('pwa-install-close')?.addEventListener('click', () => { document.getElementById('pwa-install-close')?.addEventListener('click', () => { dismissInstall(); });
document.getElementById('pwa-install-banner').style.display = 'none';
document.body.classList.remove('rspace-banner-visible');
localStorage.setItem('rspace_install_dismissed', '1');
});
} }
// Update banner button handlers (always wire, shown conditionally above) // Update banner button handlers (always wire, shown conditionally above)
@ -546,9 +552,10 @@ export function renderShell(opts: ShellOptions): string {
// ── Invite acceptance on page load ── // ── Invite acceptance on page load ──
(function() { (function() {
var params = new URLSearchParams(window.location.search); var params = new URLSearchParams(window.location.search);
var inviteToken = params.get('invite'); var inviteToken = params.get('inviteToken') || params.get('invite');
if (!inviteToken) return; if (!inviteToken) return;
// Remove token from URL immediately // Remove token from URL immediately
params.delete('inviteToken');
params.delete('invite'); params.delete('invite');
var newUrl = window.location.pathname + (params.toString() ? '?' + params.toString() : ''); var newUrl = window.location.pathname + (params.toString() ? '?' + params.toString() : '');
history.replaceState(null, '', newUrl); history.replaceState(null, '', newUrl);