feat(fase-14): PWA auto-update toast (14.1)
Migrate from `registerSW({ immediate: true })` to `useRegisterSW` from
`virtual:pwa-register/svelte`, mirror its `needRefresh` / `offlineReady`
stores into stable layout-local writables, and render a new
`UpdateToast` pill (bottom-center) that surfaces:
- "New version available" + a "Reload" button that calls
`updateServiceWorker(true)`
- "Ready to use offline" (auto-dismiss after 4s) on first install
Adds `[pwa] …` console.info telemetry on register / need-refresh /
apply. Stand-in for the unavailable `updateViaCache: 'none'` runtime
option (RegisterSWOptions doesn't expose it in vite-plugin-pwa 0.21.2):
poll `reg.update()` every 15min, which keeps a too-aggressively-cached
SW file from pinning users on the previous build for up to 24h.
`apps/web/src/global.d.ts` declares the virtual module's surface inline
so svelte-check picks it up without a relative path into node_modules;
the same file pre-declares the Fase-14.3 `__APP_VERSION__` triplet.
PU-01 stubs the registrar via `window.__pwaRegisterStub` (the layout
checks for it before calling the real `useRegisterSW`) so the toast
flow can be driven from a Playwright test without a second build.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
@@ -1,6 +1,7 @@
|
||||
<script lang="ts">
|
||||
import '../app.css';
|
||||
import { onMount } from 'svelte';
|
||||
import { writable, type Writable } from 'svelte/store';
|
||||
import { goto } from '$app/navigation';
|
||||
import { page } from '$app/stores';
|
||||
import { getSupabase } from '$lib/supabase';
|
||||
@@ -18,6 +19,16 @@
|
||||
import { initTheme } from '$lib/theme';
|
||||
import { setLanguageTag } from '$lib/paraglide/runtime';
|
||||
import { detectLanguage } from '$lib/utils/accept-language';
|
||||
import UpdateToast from '$lib/components/UpdateToast.svelte';
|
||||
|
||||
// Fase 14.1: stores driven by `useRegisterSW()` (or by a test stub
|
||||
// injected via window.__pwaRegisterStub). We expose stable writable
|
||||
// proxies that mirror the upstream values — this avoids needing
|
||||
// runes in the (legacy-mode) root layout AND keeps the reference
|
||||
// passed into <UpdateToast /> stable across the dynamic-import gap.
|
||||
const needRefresh: Writable<boolean> = writable(false);
|
||||
const offlineReady: Writable<boolean> = writable(false);
|
||||
let pwaReload: () => void | Promise<void> = () => {};
|
||||
|
||||
onMount(() => {
|
||||
// Fase 9.1: hydrate the theme stores from localStorage and start
|
||||
@@ -34,11 +45,69 @@
|
||||
(window as unknown as { __sb?: unknown }).__sb = getSupabase();
|
||||
}
|
||||
|
||||
// Fase 6.1: register the PWA service worker. `@vite-pwa/sveltekit` does
|
||||
// not auto-register — the virtual module is a no-op unless called.
|
||||
void import('virtual:pwa-register').then(({ registerSW }) => {
|
||||
registerSW({ immediate: true });
|
||||
});
|
||||
// Fase 14.1: register the PWA service worker via the Svelte-flavoured
|
||||
// virtual module so we get writable stores for needRefresh /
|
||||
// offlineReady (UpdateToast subscribes to them).
|
||||
//
|
||||
// Note on `updateViaCache: 'none'`: the plan asks for this on the
|
||||
// registration. `vite-plugin-pwa` 0.21.2's RegisterSWOptions doesn't
|
||||
// expose it (workbox-window 7 doesn't either). Mitigated below by an
|
||||
// explicit `reg.update()` poll every 15 minutes — covers the worst
|
||||
// case (a stale SW file the browser cached too aggressively) without
|
||||
// downgrading the plugin.
|
||||
//
|
||||
// The `__pwaRegisterStub` window hook lets Playwright inject a fake
|
||||
// registrar without spinning up a second build (PU-01). We mirror
|
||||
// the upstream stores into our stable `needRefresh` / `offlineReady`
|
||||
// writables so the references handed to <UpdateToast/> never change.
|
||||
const pwaUnsubs: Array<() => void> = [];
|
||||
const wirePwaStores = (r: {
|
||||
needRefresh: Writable<boolean>;
|
||||
offlineReady: Writable<boolean>;
|
||||
updateServiceWorker: (reload?: boolean) => Promise<void>;
|
||||
}) => {
|
||||
pwaReload = () => r.updateServiceWorker(true);
|
||||
pwaUnsubs.push(r.needRefresh.subscribe((v) => needRefresh.set(v)));
|
||||
pwaUnsubs.push(r.offlineReady.subscribe((v) => offlineReady.set(v)));
|
||||
};
|
||||
const stub = (window as unknown as {
|
||||
__pwaRegisterStub?: () => {
|
||||
needRefresh: Writable<boolean>;
|
||||
offlineReady: Writable<boolean>;
|
||||
updateServiceWorker: (reload?: boolean) => Promise<void>;
|
||||
};
|
||||
}).__pwaRegisterStub;
|
||||
if (stub) {
|
||||
console.info('[pwa] registered (stub)');
|
||||
wirePwaStores(stub());
|
||||
} else {
|
||||
void import('virtual:pwa-register/svelte').then(({ useRegisterSW }) => {
|
||||
const r = useRegisterSW({
|
||||
immediate: true,
|
||||
onRegisteredSW(swUrl: string, reg: ServiceWorkerRegistration | undefined) {
|
||||
console.info('[pwa] registered', swUrl);
|
||||
// Stand-in for `updateViaCache: 'none'`: poll the SW
|
||||
// for an update every 15 minutes. Without this a
|
||||
// stale cached SW file could keep the user on the
|
||||
// previous build for up to 24h in some browsers.
|
||||
if (reg) {
|
||||
setInterval(() => {
|
||||
reg.update().catch(() => {
|
||||
/* offline / transient */
|
||||
});
|
||||
}, 15 * 60 * 1000);
|
||||
}
|
||||
},
|
||||
onRegisterError(err: unknown) {
|
||||
console.info('[pwa] register error', err);
|
||||
},
|
||||
onNeedRefresh() {
|
||||
console.info('[pwa] update available');
|
||||
}
|
||||
});
|
||||
wirePwaStores(r);
|
||||
});
|
||||
}
|
||||
|
||||
const supabase = getSupabase();
|
||||
|
||||
@@ -117,6 +186,7 @@
|
||||
collectiveUnsub();
|
||||
void teardownFeatureSubscriptions();
|
||||
clearServerAdminFlag();
|
||||
for (const u of pwaUnsubs) u();
|
||||
};
|
||||
});
|
||||
|
||||
@@ -209,4 +279,5 @@
|
||||
|
||||
<ParaglideJS {i18n}>
|
||||
<slot />
|
||||
<UpdateToast {needRefresh} {offlineReady} onReload={pwaReload} />
|
||||
</ParaglideJS>
|
||||
|
||||
Reference in New Issue
Block a user