feat: enhance push notification handling and service worker registration with improved error handling and subscription management
Some checks are pending
linter / quality (push) Waiting to run
tests / ci (8.3) (push) Waiting to run
tests / ci (8.4) (push) Waiting to run
tests / ci (8.5) (push) Waiting to run

This commit is contained in:
Yoga Pangestu 2026-07-08 11:28:43 +07:00
parent 452c1a4209
commit 2efc13643b
3 changed files with 52 additions and 51 deletions

View File

@ -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());
}
}

View File

@ -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<ServiceWorkerRegistration> {
return Promise.race([
navigator.serviceWorker.ready,
new Promise<never>((_, 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<void> {
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<void> {
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<void> {
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;

View File

@ -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(() => {
<Button
v-else-if="perm.state === 'prompt'"
size="sm"
:disabled="perm.requesting"
:disabled="perm.requesting || (perm.key === 'notification' && isLoading)"
@click="requestPermission(perm)"
>
{{ perm.requesting ? 'Meminta...' : 'Izinkan' }}
{{ (perm.requesting || (perm.key === 'notification' && isLoading)) ? 'Meminta...' : 'Izinkan' }}
</Button>
<span