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:
157
resources/js/guest/guest-sw.ts
Normal file
157
resources/js/guest/guest-sw.ts
Normal file
@@ -0,0 +1,157 @@
|
||||
/* eslint-disable no-restricted-globals */
|
||||
/// <reference lib="webworker" />
|
||||
|
||||
import { clientsClaim } from 'workbox-core';
|
||||
import { CacheableResponsePlugin } from 'workbox-cacheable-response';
|
||||
import { ExpirationPlugin } from 'workbox-expiration';
|
||||
import { precacheAndRoute, cleanupOutdatedCaches } from 'workbox-precaching';
|
||||
import { registerRoute } from 'workbox-routing';
|
||||
import { CacheFirst, NetworkFirst, StaleWhileRevalidate } from 'workbox-strategies';
|
||||
|
||||
declare const self: ServiceWorkerGlobalScope & {
|
||||
__WB_MANIFEST: Array<import('workbox-precaching').ManifestEntry>;
|
||||
};
|
||||
|
||||
clientsClaim();
|
||||
precacheAndRoute(self.__WB_MANIFEST);
|
||||
cleanupOutdatedCaches();
|
||||
|
||||
const isGuestNavigation = (pathname: string) => {
|
||||
if (pathname === '/event') {
|
||||
return true;
|
||||
}
|
||||
if (pathname.startsWith('/e/')) {
|
||||
return true;
|
||||
}
|
||||
if (pathname.startsWith('/g/')) {
|
||||
return true;
|
||||
}
|
||||
if (pathname.startsWith('/share/')) {
|
||||
return true;
|
||||
}
|
||||
if (pathname.startsWith('/help')) {
|
||||
return true;
|
||||
}
|
||||
if (pathname.startsWith('/legal')) {
|
||||
return true;
|
||||
}
|
||||
if (pathname.startsWith('/settings')) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
};
|
||||
|
||||
registerRoute(
|
||||
({ request, url }) =>
|
||||
request.mode === 'navigate' && url.origin === self.location.origin && isGuestNavigation(url.pathname),
|
||||
new NetworkFirst({
|
||||
cacheName: 'guest-pages',
|
||||
networkTimeoutSeconds: 5,
|
||||
plugins: [
|
||||
new CacheableResponsePlugin({ statuses: [0, 200] }),
|
||||
new ExpirationPlugin({ maxEntries: 40, maxAgeSeconds: 60 * 60 * 24 * 7 }),
|
||||
],
|
||||
})
|
||||
);
|
||||
|
||||
registerRoute(
|
||||
({ request, url }) =>
|
||||
request.method === 'GET' &&
|
||||
url.origin === self.location.origin &&
|
||||
url.pathname.startsWith('/api/v1/'),
|
||||
new NetworkFirst({
|
||||
cacheName: 'guest-api',
|
||||
networkTimeoutSeconds: 6,
|
||||
plugins: [
|
||||
new CacheableResponsePlugin({ statuses: [0, 200] }),
|
||||
new ExpirationPlugin({ maxEntries: 80, maxAgeSeconds: 60 * 60 * 24 }),
|
||||
],
|
||||
})
|
||||
);
|
||||
|
||||
registerRoute(
|
||||
({ request, url }) => request.destination === 'image' && url.origin === self.location.origin,
|
||||
new CacheFirst({
|
||||
cacheName: 'guest-images',
|
||||
plugins: [
|
||||
new CacheableResponsePlugin({ statuses: [0, 200] }),
|
||||
new ExpirationPlugin({ maxEntries: 200, maxAgeSeconds: 60 * 60 * 24 * 30 }),
|
||||
],
|
||||
})
|
||||
);
|
||||
|
||||
registerRoute(
|
||||
({ request, url }) => request.destination === 'font' && url.origin === self.location.origin,
|
||||
new StaleWhileRevalidate({
|
||||
cacheName: 'guest-fonts',
|
||||
plugins: [
|
||||
new CacheableResponsePlugin({ statuses: [0, 200] }),
|
||||
new ExpirationPlugin({ maxEntries: 30, maxAgeSeconds: 60 * 60 * 24 * 365 }),
|
||||
],
|
||||
})
|
||||
);
|
||||
|
||||
self.addEventListener('message', (event) => {
|
||||
if (event.data?.type === 'SKIP_WAITING') {
|
||||
self.skipWaiting();
|
||||
}
|
||||
});
|
||||
|
||||
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: '/apple-touch-icon.png',
|
||||
badge: '/apple-touch-icon.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' }));
|
||||
})
|
||||
);
|
||||
});
|
||||
Reference in New Issue
Block a user