mirror of
https://github.com/Memo-2023/mana-monorepo.git
synced 2026-05-14 20:01:09 +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).
|
* Authentication is handled entirely by Mana Core Auth (@mana/shared-auth).
|
||||||
* No Supabase is needed - all data comes from mana-auth APIs.
|
* 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 {
|
declare global {
|
||||||
const __BUILD_HASH__: string;
|
const __BUILD_HASH__: string;
|
||||||
const __BUILD_TIME__: string;
|
const __BUILD_TIME__: string;
|
||||||
|
|
|
||||||
|
|
@ -1,64 +1,34 @@
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { onMount } from 'svelte';
|
|
||||||
import { _ } from 'svelte-i18n';
|
import { _ } from 'svelte-i18n';
|
||||||
|
import { useRegisterSW } from 'virtual:pwa-register/svelte';
|
||||||
|
|
||||||
let showPrompt = $state(false);
|
// `useRegisterSW` registers the service worker and exposes two Svelte
|
||||||
let registration = $state<ServiceWorkerRegistration | null>(null);
|
// stores we actually care about:
|
||||||
|
// - needRefresh → a new SW is waiting (matches registerType: 'prompt')
|
||||||
onMount(() => {
|
// - updateServiceWorker(reload) → skipWaiting + reload page
|
||||||
if (!('serviceWorker' in navigator)) return;
|
// `offlineReady` is available too if we ever want a "ready for offline"
|
||||||
|
// toast; we don't surface it today to keep the UI quiet.
|
||||||
function onNewSW(reg: ServiceWorkerRegistration) {
|
const { needRefresh, updateServiceWorker } = useRegisterSW({
|
||||||
registration = reg;
|
immediate: true,
|
||||||
showPrompt = true;
|
onRegisterError(error: unknown) {
|
||||||
}
|
console.error('[pwa] service worker registration failed', error);
|
||||||
|
},
|
||||||
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);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
|
||||||
let reloading = false;
|
let reloading = false;
|
||||||
|
|
||||||
function handleUpdate() {
|
function handleUpdate() {
|
||||||
if (!registration?.waiting || reloading) return;
|
if (reloading) return;
|
||||||
|
reloading = true;
|
||||||
registration.waiting.postMessage({ type: 'SKIP_WAITING' });
|
void updateServiceWorker(true);
|
||||||
|
|
||||||
// Reload once the new SW takes over (once only)
|
|
||||||
navigator.serviceWorker.addEventListener(
|
|
||||||
'controllerchange',
|
|
||||||
() => {
|
|
||||||
if (reloading) return;
|
|
||||||
reloading = true;
|
|
||||||
window.location.reload();
|
|
||||||
},
|
|
||||||
{ once: true }
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleDismiss() {
|
function handleDismiss() {
|
||||||
showPrompt = false;
|
needRefresh.set(false);
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
{#if showPrompt}
|
{#if $needRefresh}
|
||||||
<div class="update-banner" role="alert">
|
<div class="update-banner" role="alert">
|
||||||
<div class="update-content">
|
<div class="update-content">
|
||||||
<svg class="update-icon" width="18" height="18" viewBox="0 0 256 256" fill="currentColor">
|
<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 OfflineIndicator from '$lib/components/OfflineIndicator.svelte';
|
||||||
import PwaUpdatePrompt from '$lib/components/PwaUpdatePrompt.svelte';
|
import PwaUpdatePrompt from '$lib/components/PwaUpdatePrompt.svelte';
|
||||||
import AuthRequiredModal from '$lib/components/auth/AuthRequiredModal.svelte';
|
import AuthRequiredModal from '$lib/components/auth/AuthRequiredModal.svelte';
|
||||||
|
import { pwaInfo } from 'virtual:pwa-info';
|
||||||
|
|
||||||
let { children } = $props();
|
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
|
// Tracks the last user id we pushed into the data layer. Comparing
|
||||||
// against this lets us short-circuit identity-update churn during auth
|
// against this lets us short-circuit identity-update churn during auth
|
||||||
// initialisation, which previously caused effect_update_depth_exceeded.
|
// initialisation, which previously caused effect_update_depth_exceeded.
|
||||||
|
|
@ -112,6 +119,10 @@
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
<svelte:head>
|
||||||
|
{@html webManifestLink}
|
||||||
|
</svelte:head>
|
||||||
|
|
||||||
{@render children()}
|
{@render children()}
|
||||||
<SyncConflictToast />
|
<SyncConflictToast />
|
||||||
<OfflineIndicator />
|
<OfflineIndicator />
|
||||||
|
|
|
||||||
|
|
@ -23,14 +23,15 @@ export default defineConfig({
|
||||||
themeColor: '#6366f1',
|
themeColor: '#6366f1',
|
||||||
registerType: 'prompt',
|
registerType: 'prompt',
|
||||||
preset: 'full',
|
preset: 'full',
|
||||||
// Disable the service worker in dev. With devEnabled=true (the
|
// SW disabled in dev. Reasons:
|
||||||
// default) vite-plugin-pwa registers a SW that aggressively
|
// 1. Workbox precache doesn't run under `vite dev`, so the
|
||||||
// precaches the route chunks — and after the first dev session
|
// `navigateFallback: '/offline'` rule fires for every
|
||||||
// the SW keeps serving the OLD JS even when Vite HMR pushes
|
// navigation → users land on the offline page even while
|
||||||
// new code, so source edits become invisible until the user
|
// online. (Observed 2026-04-14.)
|
||||||
// manually unregisters the worker in DevTools. The 2026-04-08
|
// 2. The 2026-04-08 dreams mic-button bug: a stuck dev SW
|
||||||
// dreams mic-button bug took an extra hour to track down for
|
// kept serving old JS chunks through HMR reloads.
|
||||||
// exactly this reason. Production still gets the full SW.
|
// Test install prompt + offline behavior via `pnpm build &&
|
||||||
|
// pnpm preview` instead — production SW works correctly.
|
||||||
devEnabled: false,
|
devEnabled: false,
|
||||||
shortcuts: [
|
shortcuts: [
|
||||||
{ name: 'Dashboard', short_name: 'Home', url: '/', description: 'Zum Dashboard' },
|
{ name: 'Dashboard', short_name: 'Home', url: '/', description: 'Zum Dashboard' },
|
||||||
|
|
|
||||||
|
|
@ -62,6 +62,10 @@ export function createPWAConfig(options: PWAConfigOptions): PWAConfig {
|
||||||
|
|
||||||
// Build manifest
|
// Build manifest
|
||||||
const manifest: ManifestConfig = {
|
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,
|
name,
|
||||||
short_name: shortName,
|
short_name: shortName,
|
||||||
description,
|
description,
|
||||||
|
|
|
||||||
|
|
@ -139,6 +139,13 @@ export interface ManifestIcon {
|
||||||
* Full manifest configuration
|
* Full manifest configuration
|
||||||
*/
|
*/
|
||||||
export interface ManifestConfig {
|
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;
|
name: string;
|
||||||
short_name: string;
|
short_name: string;
|
||||||
description: string;
|
description: string;
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue