Implemented guest-only PWA using vite-plugin-pwa (the actual published package; @vite-pwa/plugin isn’t on npm) with

injectManifest, a new typed SW source, runtime caching, and a non‑blocking update toast with an action button. The
  guest shell now links a dedicated manifest and theme color, and background upload sync is managed in a single
  PwaManager component.

  Key changes (where/why)

  - vite.config.ts: added VitePWA injectManifest config, guest manifest, and output to /public so the SW can control /
    scope.
  - resources/js/guest/guest-sw.ts: new Workbox SW (precache + runtime caching for guest navigation, GET /api/v1/*,
    images, fonts) and preserves push/sync/notification logic.
  - resources/js/guest/components/PwaManager.tsx: registers SW, shows update/offline toasts, and processes the upload
    queue on sync/online.
  - resources/js/guest/components/ToastHost.tsx: action-capable toasts so update prompts can include a CTA.
  - resources/js/guest/i18n/messages.ts: added common.updateAvailable, common.updateAction, common.offlineReady.
  - resources/views/guest.blade.php: manifest + theme color + apple touch icon.
  - .gitignore: ignore generated public/guest-sw.js and public/guest.webmanifest; public/guest-sw.js removed since it’s
    now build output.
This commit is contained in:
Codex Agent
2025-12-27 10:59:44 +01:00
parent efc173cf5d
commit 3e3a2c49d6
30 changed files with 3862 additions and 812 deletions

View File

@@ -1,66 +0,0 @@
self.addEventListener('install', () => {
self.skipWaiting();
});
self.addEventListener('activate', (event) => {
event.waitUntil(self.clients.claim());
});
self.addEventListener('sync', (event) => {
if (event.tag === 'upload-queue') {
event.waitUntil(
(async () => {
const clients = await self.clients.matchAll({ includeUncontrolled: true, type: 'window' });
clients.forEach((client) => client.postMessage({ type: 'sync-queue' }));
})()
);
}
});
self.addEventListener('push', (event) => {
const payload = event.data?.json?.() ?? {};
event.waitUntil(
(async () => {
const title = payload.title ?? 'Neue Nachricht';
const options = {
body: payload.body ?? '',
icon: '/icons/icon-192.png',
badge: '/icons/badge.png',
data: payload.data ?? {},
};
await self.registration.showNotification(title, options);
const clients = await self.clients.matchAll({ type: 'window', includeUncontrolled: true });
clients.forEach((client) => client.postMessage({ type: 'guest-notification-refresh' }));
})()
);
});
self.addEventListener('notificationclick', (event) => {
event.notification.close();
const targetUrl = event.notification.data?.url || '/';
event.waitUntil(
self.clients.matchAll({ type: 'window', includeUncontrolled: true }).then((clientList) => {
for (const client of clientList) {
if ('focus' in client) {
client.navigate(targetUrl);
return client.focus();
}
}
if (self.clients.openWindow) {
return self.clients.openWindow(targetUrl);
}
})
);
});
self.addEventListener('pushsubscriptionchange', (event) => {
event.waitUntil(
self.clients.matchAll({ type: 'window', includeUncontrolled: true }).then((clientList) => {
clientList.forEach((client) => client.postMessage({ type: 'push-subscription-change' }));
})
);
});