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>
93 lines
2.8 KiB
Svelte
93 lines
2.8 KiB
Svelte
<script lang="ts">
|
|
/**
|
|
* Fase 14.1 — PWA update + offline-ready notification.
|
|
*
|
|
* Driven by two writable<boolean> stores produced by `useRegisterSW()`
|
|
* (or its test stub). The component is purely presentational: state
|
|
* lives in the stores, behaviour (`onReload`) is injected.
|
|
*
|
|
* - When `needRefresh` flips true, a sticky pill appears with a
|
|
* "Reload" button. Clicking it calls `onReload()` which is wired
|
|
* in the root layout to `updateServiceWorker(true)`.
|
|
* - When `offlineReady` flips true (first install), a transient
|
|
* pill appears for 4s and then auto-dismisses.
|
|
*
|
|
* Both pills share the same bottom-center slot. If both fire at once
|
|
* the update toast wins (more actionable).
|
|
*/
|
|
import { onMount } from 'svelte';
|
|
import type { Writable } from 'svelte/store';
|
|
import * as m from '$lib/paraglide/messages';
|
|
|
|
const {
|
|
needRefresh,
|
|
offlineReady,
|
|
onReload
|
|
}: {
|
|
needRefresh: Writable<boolean>;
|
|
offlineReady: Writable<boolean>;
|
|
onReload: () => void | Promise<void>;
|
|
} = $props();
|
|
|
|
let showOfflineReady = $state(false);
|
|
let offlineTimer: ReturnType<typeof setTimeout> | null = null;
|
|
|
|
onMount(() => {
|
|
const unsub = offlineReady.subscribe((v) => {
|
|
if (!v) return;
|
|
showOfflineReady = true;
|
|
console.info('[pwa] offline-ready');
|
|
if (offlineTimer) clearTimeout(offlineTimer);
|
|
offlineTimer = setTimeout(() => {
|
|
showOfflineReady = false;
|
|
offlineReady.set(false);
|
|
}, 4_000);
|
|
});
|
|
return () => {
|
|
unsub();
|
|
if (offlineTimer) clearTimeout(offlineTimer);
|
|
};
|
|
});
|
|
|
|
async function handleReload() {
|
|
console.info('[pwa] applying update');
|
|
await onReload();
|
|
}
|
|
</script>
|
|
|
|
{#if $needRefresh}
|
|
<div
|
|
data-testid="pwa-update-toast"
|
|
role="status"
|
|
aria-live="polite"
|
|
class="pointer-events-none fixed inset-x-0 bottom-20 z-50 flex justify-center px-4 md:bottom-6"
|
|
>
|
|
<div
|
|
class="pointer-events-auto flex items-center gap-3 rounded-full bg-slate-900 px-4 py-2 text-sm text-white shadow-[0px_20px_40px_rgba(15,23,42,0.3)] dark:bg-slate-100 dark:text-slate-900"
|
|
>
|
|
<span>{m.pwa_update_available()}</span>
|
|
<button
|
|
type="button"
|
|
data-testid="pwa-update-reload"
|
|
onclick={handleReload}
|
|
class="rounded-full bg-white/15 px-3 py-1 text-xs font-semibold text-white hover:bg-white/25 dark:bg-slate-900/15 dark:text-slate-900 dark:hover:bg-slate-900/25"
|
|
>
|
|
{m.pwa_update_reload()}
|
|
</button>
|
|
</div>
|
|
</div>
|
|
{:else if showOfflineReady}
|
|
<div
|
|
data-testid="pwa-offline-ready-toast"
|
|
role="status"
|
|
aria-live="polite"
|
|
class="pointer-events-none fixed inset-x-0 bottom-20 z-50 flex justify-center px-4 md:bottom-6"
|
|
>
|
|
<div
|
|
class="pointer-events-auto flex items-center gap-2 rounded-full bg-slate-900 px-4 py-2 text-sm text-white shadow-[0px_20px_40px_rgba(15,23,42,0.3)] dark:bg-slate-100 dark:text-slate-900"
|
|
>
|
|
<span>{m.pwa_offline_ready()}</span>
|
|
</div>
|
|
</div>
|
|
{/if}
|