diff --git a/public/sw.js b/public/sw.js index 994d7f8..dfe02d1 100644 --- a/public/sw.js +++ b/public/sw.js @@ -1,26 +1,14 @@ // DST Collection — Service Worker -// Push Notification Handler - -const CACHE_NAME = 'dst-v1'; +// Push Notification + Install/Activate Handler // ── Install ────────────────────────────────────────────────────────────────── self.addEventListener('install', () => { self.skipWaiting(); }); -// ── Activate ───────────────────────────────────────────────────────────────── +// ── Activate ────────────────────────────────────────────────────────────────── self.addEventListener('activate', (event) => { - event.waitUntil( - (async () => { - const keys = await caches.keys(); - await Promise.all(keys.map((k) => caches.delete(k))); - - await self.registration.unregister(); - - const clientList = await self.clients.matchAll({ type: 'window' }); - clientList.forEach((client) => client.navigate(client.url)); - })(), - ); + event.waitUntil(self.clients.claim()); }); // ── Push Notification ───────────────────────────────────────────────────────── @@ -30,19 +18,18 @@ self.addEventListener('push', (event) => { let data = {}; try { data = event.data.json(); - } catch (e) { + } catch { data = { title: 'DST Collection', body: event.data.text() }; } - const title = data.title || 'DST Collection'; + const title = data.title ?? 'DST Collection'; const options = { - body: data.body || '', - icon: data.icon || '/assets/pwa-192x192.png', + body: data.body ?? '', + icon: data.icon ?? '/assets/pwa-192x192.png', badge: '/assets/pwa-64x64.png', - data: { url: data.url || '/admin/master/categories' }, + data: { url: data.url ?? '/admin/master/categories' }, tag: 'dst-notification', renotify: true, - requireInteraction: false, }; event.waitUntil(self.registration.showNotification(title, options)); @@ -52,16 +39,17 @@ self.addEventListener('push', (event) => { self.addEventListener('notificationclick', (event) => { event.notification.close(); - const targetUrl = (event.notification.data && event.notification.data.url) - ? event.notification.data.url - : '/admin/dashboard'; + const targetUrl = + (event.notification.data && event.notification.data.url) + ? event.notification.data.url + : '/admin/dashboard'; event.waitUntil( self.clients .matchAll({ type: 'window', includeUncontrolled: true }) .then((clientList) => { for (const client of clientList) { - if ('navigate' in client) { + if ('focus' in client) { return client.navigate(targetUrl).then((c) => c && c.focus()); } } diff --git a/resources/js/composables/usePushNotification.ts b/resources/js/composables/usePushNotification.ts index fc49825..7783d70 100644 --- a/resources/js/composables/usePushNotification.ts +++ b/resources/js/composables/usePushNotification.ts @@ -11,9 +11,23 @@ function urlBase64ToUint8Array(base64String: string): Uint8Array { return Uint8Array.from([...rawData].map((char) => char.charCodeAt(0))); } + +async function getSwRegistration(timeoutMs = 10_000): Promise { + return Promise.race([ + navigator.serviceWorker.ready, + new Promise((_, reject) => + setTimeout( + () => reject(new Error('[SW] Service worker not ready within timeout')), + timeoutMs, + ), + ), + ]); +} + export function usePushNotification() { const isSupported = computed( () => + typeof window !== 'undefined' && 'Notification' in window && 'serviceWorker' in navigator && 'PushManager' in window, @@ -28,13 +42,12 @@ export function usePushNotification() { async function checkSubscriptionStatus(): Promise { if (!isSupported.value) { -return; -} + return; + } try { - const registration = await navigator.serviceWorker.ready; - const subscription = - await registration.pushManager.getSubscription(); + const registration = await getSwRegistration(); + const subscription = await registration.pushManager.getSubscription(); isSubscribed.value = !!subscription; } catch { isSubscribed.value = false; @@ -43,19 +56,19 @@ return; async function subscribe(): Promise { if (!isSupported.value || isLoading.value) { -return; -} - - const permission = await Notification.requestPermission(); - - if (permission !== 'granted') { -return; -} + return; + } isLoading.value = true; try { - const registration = await navigator.serviceWorker.ready; + const permission = await Notification.requestPermission(); + + if (permission !== 'granted') { + return; + } + + const registration = await getSwRegistration(); const publicKey = vapidPublicKey ?? import.meta.env.VITE_VAPID_PUBLIC_KEY ?? ''; @@ -94,15 +107,14 @@ return; async function unsubscribe(): Promise { if (!isSupported.value || isLoading.value) { -return; -} + return; + } isLoading.value = true; try { - const registration = await navigator.serviceWorker.ready; - const subscription = - await registration.pushManager.getSubscription(); + const registration = await getSwRegistration(); + const subscription = await registration.pushManager.getSubscription(); if (!subscription) { isSubscribed.value = false; diff --git a/resources/js/pages/admin/account/Permissions.vue b/resources/js/pages/admin/account/Permissions.vue index 55ecc26..7b69adb 100644 --- a/resources/js/pages/admin/account/Permissions.vue +++ b/resources/js/pages/admin/account/Permissions.vue @@ -128,13 +128,14 @@ return; try { if (perm.key === 'notification') { - const result = await Notification.requestPermission(); + await subscribe(); - if (result === 'denied') { + if (isSubscribed.value) { + perm.state = 'granted'; + } else if (typeof Notification !== 'undefined' && Notification.permission === 'denied') { perm.state = 'denied'; - } else if (result === 'granted') { - await subscribe(); - perm.state = isSubscribed.value ? 'granted' : 'denied'; + } else { + perm.state = 'prompt'; } } else if (perm.key === 'camera') { const stream = await navigator.mediaDevices.getUserMedia({ @@ -305,10 +306,10 @@ onMounted(() => {