mirror of
https://github.com/Memo-2023/mana-monorepo.git
synced 2026-05-14 18:41:08 +02:00
fix(pwa): wire up manifest link + SW registration so install prompt works
The PWA was configured end-to-end in vite.config.ts and built the manifest + service worker correctly, but neither was ever loaded by the browser — no <link rel="manifest"> in the HTML and no script registering the generated registerSW.js. Chrome therefore never fired beforeinstallprompt, no install icon appeared in the URL bar, and the hand-rolled PwaUpdatePrompt hung on navigator.serviceWorker.ready because no SW had been registered. Changes: - Render pwaInfo.webManifest.linkTag into <svelte:head> in the root layout so Chrome finds the hashed manifest. - Replace the hand-rolled SW-update logic in PwaUpdatePrompt with useRegisterSW() from virtual:pwa-register/svelte — it registers the worker (immediate: true) and exposes reactive needRefresh + updateServiceWorker stores that match registerType: 'prompt'. - Add triple-slash refs to vite-plugin-pwa/info and /svelte in app.d.ts for the virtual module types. - Set manifest.id = startUrl in @mana/shared-pwa so Chrome doesn't warn and keeps the install identity stable across start_url edits. - Keep devEnabled: false and expand the comment: the 2026-04-08 dreams mic-button bug and the /offline navigateFallback both misbehave when Workbox precache doesn't run under vite dev. Test the install flow via `pnpm build && pnpm preview` instead. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
4192a4bd9b
commit
4b2007e97c
6 changed files with 55 additions and 55 deletions
7
apps/mana/apps/web/src/app.d.ts
vendored
7
apps/mana/apps/web/src/app.d.ts
vendored
|
|
@ -4,6 +4,13 @@
|
|||
* Authentication is handled entirely by Mana Core Auth (@mana/shared-auth).
|
||||
* No Supabase is needed - all data comes from mana-auth APIs.
|
||||
*/
|
||||
|
||||
// Virtual modules provided by vite-plugin-pwa (wrapped by @vite-pwa/sveltekit):
|
||||
// - virtual:pwa-info → pwaInfo.webManifest.linkTag for <svelte:head>
|
||||
// - virtual:pwa-register/svelte → useRegisterSW() Svelte-store hook
|
||||
/// <reference types="vite-plugin-pwa/info" />
|
||||
/// <reference types="vite-plugin-pwa/svelte" />
|
||||
|
||||
declare global {
|
||||
const __BUILD_HASH__: string;
|
||||
const __BUILD_TIME__: string;
|
||||
|
|
|
|||
|
|
@ -1,64 +1,34 @@
|
|||
<script lang="ts">
|
||||
import { onMount } from 'svelte';
|
||||
import { _ } from 'svelte-i18n';
|
||||
import { useRegisterSW } from 'virtual:pwa-register/svelte';
|
||||
|
||||
let showPrompt = $state(false);
|
||||
let registration = $state<ServiceWorkerRegistration | null>(null);
|
||||
|
||||
onMount(() => {
|
||||
if (!('serviceWorker' in navigator)) return;
|
||||
|
||||
function onNewSW(reg: ServiceWorkerRegistration) {
|
||||
registration = reg;
|
||||
showPrompt = true;
|
||||
}
|
||||
|
||||
navigator.serviceWorker.ready.then((reg) => {
|
||||
// Already waiting worker present
|
||||
if (reg.waiting) {
|
||||
onNewSW(reg);
|
||||
return;
|
||||
}
|
||||
|
||||
// Watch for new installs
|
||||
reg.addEventListener('updatefound', () => {
|
||||
const newWorker = reg.installing;
|
||||
if (!newWorker) return;
|
||||
|
||||
newWorker.addEventListener('statechange', () => {
|
||||
if (newWorker.state === 'installed' && navigator.serviceWorker.controller) {
|
||||
onNewSW(reg);
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
// `useRegisterSW` registers the service worker and exposes two Svelte
|
||||
// stores we actually care about:
|
||||
// - needRefresh → a new SW is waiting (matches registerType: 'prompt')
|
||||
// - updateServiceWorker(reload) → skipWaiting + reload page
|
||||
// `offlineReady` is available too if we ever want a "ready for offline"
|
||||
// toast; we don't surface it today to keep the UI quiet.
|
||||
const { needRefresh, updateServiceWorker } = useRegisterSW({
|
||||
immediate: true,
|
||||
onRegisterError(error: unknown) {
|
||||
console.error('[pwa] service worker registration failed', error);
|
||||
},
|
||||
});
|
||||
|
||||
let reloading = false;
|
||||
|
||||
function handleUpdate() {
|
||||
if (!registration?.waiting || reloading) return;
|
||||
|
||||
registration.waiting.postMessage({ type: 'SKIP_WAITING' });
|
||||
|
||||
// Reload once the new SW takes over (once only)
|
||||
navigator.serviceWorker.addEventListener(
|
||||
'controllerchange',
|
||||
() => {
|
||||
if (reloading) return;
|
||||
reloading = true;
|
||||
window.location.reload();
|
||||
},
|
||||
{ once: true }
|
||||
);
|
||||
if (reloading) return;
|
||||
reloading = true;
|
||||
void updateServiceWorker(true);
|
||||
}
|
||||
|
||||
function handleDismiss() {
|
||||
showPrompt = false;
|
||||
needRefresh.set(false);
|
||||
}
|
||||
</script>
|
||||
|
||||
{#if showPrompt}
|
||||
{#if $needRefresh}
|
||||
<div class="update-banner" role="alert">
|
||||
<div class="update-content">
|
||||
<svg class="update-icon" width="18" height="18" viewBox="0 0 256 256" fill="currentColor">
|
||||
|
|
|
|||
|
|
@ -15,9 +15,16 @@
|
|||
import OfflineIndicator from '$lib/components/OfflineIndicator.svelte';
|
||||
import PwaUpdatePrompt from '$lib/components/PwaUpdatePrompt.svelte';
|
||||
import AuthRequiredModal from '$lib/components/auth/AuthRequiredModal.svelte';
|
||||
import { pwaInfo } from 'virtual:pwa-info';
|
||||
|
||||
let { children } = $props();
|
||||
|
||||
// vite-plugin-pwa emits the <link rel="manifest"> tag (with the hashed
|
||||
// manifest filename) via this virtual module. Without rendering it into
|
||||
// <svelte:head>, Chrome can't read the manifest → no install prompt,
|
||||
// no install icon in the URL bar, no A2HS on mobile.
|
||||
const webManifestLink = $derived(pwaInfo?.webManifest.linkTag ?? '');
|
||||
|
||||
// Tracks the last user id we pushed into the data layer. Comparing
|
||||
// against this lets us short-circuit identity-update churn during auth
|
||||
// initialisation, which previously caused effect_update_depth_exceeded.
|
||||
|
|
@ -112,6 +119,10 @@
|
|||
});
|
||||
</script>
|
||||
|
||||
<svelte:head>
|
||||
{@html webManifestLink}
|
||||
</svelte:head>
|
||||
|
||||
{@render children()}
|
||||
<SyncConflictToast />
|
||||
<OfflineIndicator />
|
||||
|
|
|
|||
|
|
@ -23,14 +23,15 @@ export default defineConfig({
|
|||
themeColor: '#6366f1',
|
||||
registerType: 'prompt',
|
||||
preset: 'full',
|
||||
// Disable the service worker in dev. With devEnabled=true (the
|
||||
// default) vite-plugin-pwa registers a SW that aggressively
|
||||
// precaches the route chunks — and after the first dev session
|
||||
// the SW keeps serving the OLD JS even when Vite HMR pushes
|
||||
// new code, so source edits become invisible until the user
|
||||
// manually unregisters the worker in DevTools. The 2026-04-08
|
||||
// dreams mic-button bug took an extra hour to track down for
|
||||
// exactly this reason. Production still gets the full SW.
|
||||
// SW disabled in dev. Reasons:
|
||||
// 1. Workbox precache doesn't run under `vite dev`, so the
|
||||
// `navigateFallback: '/offline'` rule fires for every
|
||||
// navigation → users land on the offline page even while
|
||||
// online. (Observed 2026-04-14.)
|
||||
// 2. The 2026-04-08 dreams mic-button bug: a stuck dev SW
|
||||
// kept serving old JS chunks through HMR reloads.
|
||||
// Test install prompt + offline behavior via `pnpm build &&
|
||||
// pnpm preview` instead — production SW works correctly.
|
||||
devEnabled: false,
|
||||
shortcuts: [
|
||||
{ name: 'Dashboard', short_name: 'Home', url: '/', description: 'Zum Dashboard' },
|
||||
|
|
|
|||
|
|
@ -62,6 +62,10 @@ export function createPWAConfig(options: PWAConfigOptions): PWAConfig {
|
|||
|
||||
// Build manifest
|
||||
const manifest: ManifestConfig = {
|
||||
// Pin the app identity to the start URL. Without `id`, Chrome derives
|
||||
// one from start_url and warns in DevTools; it also refuses to
|
||||
// re-prompt an install if start_url ever changes.
|
||||
id: startUrl,
|
||||
name,
|
||||
short_name: shortName,
|
||||
description,
|
||||
|
|
|
|||
|
|
@ -139,6 +139,13 @@ export interface ManifestIcon {
|
|||
* Full manifest configuration
|
||||
*/
|
||||
export interface ManifestConfig {
|
||||
/**
|
||||
* Unique manifest identifier. Chrome uses this to correlate the installed
|
||||
* app with its manifest across `start_url` changes. Strongly recommended
|
||||
* by the spec since 2023 — omitting it triggers a DevTools warning and
|
||||
* can suppress the install prompt on re-installs.
|
||||
*/
|
||||
id: string;
|
||||
name: string;
|
||||
short_name: string;
|
||||
description: string;
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue