feat(pwa): installable app — manifest + SW + Settings install button
CI / test (push) Has been cancelled

The captive-portal Step-2 QR opens pictureframe.edholm.me in Safari,
which is the perfect moment to also offer "pin this to your home
screen" so the recipient gets one-tap access without typing the URL
again. Two pieces:

* Service worker at /sw.js (document root, scope "/"). Minimal —
  install/activate calls skipWaiting + clients.claim, fetch is
  passthrough. Real offline caching is intentionally out of scope;
  we only need the SW to exist so Chrome's PWA-install heuristic
  fires.

* Settings → Install app section, hidden when display-mode standalone.
  Android Chrome path: native beforeinstallprompt button.
  iOS Safari (and any other non-prompt browser): button opens a
  modal with step-by-step Share → Add to Home Screen instructions.

usePwaInstall composable handles the singleton lifecycle —
beforeinstallprompt fires once per page load and may fire before the
user navigates to Settings, so we register on module import and stash
the event for later.

Tests cover: install button rendered when not standalone, modal opens
on click without a native prompt, modal close button works.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-05-09 13:49:12 -04:00
parent 2be153a103
commit 00121aaec9
25 changed files with 347 additions and 16 deletions
+12
View File
@@ -10,3 +10,15 @@ app.use(createPinia())
app.use(router)
app.use(VueKonva)
app.mount('#app')
// Register the PWA service worker. Done after mount so the SW
// register call doesn't block first paint. /sw.js (not /build/sw.js)
// so the SW's scope is "/" and covers the whole SPA.
if ('serviceWorker' in navigator) {
window.addEventListener('load', () => {
navigator.serviceWorker.register('/sw.js').catch(() => {
// SW registration failure is non-fatal — PWA install just won't be
// available, but the app still works.
})
})
}