feat: enhance push notification handling and service worker registration with improved error handling and subscription management
This commit is contained in:
parent
452c1a4209
commit
2efc13643b
34
public/sw.js
34
public/sw.js
@ -1,26 +1,14 @@
|
|||||||
// DST Collection — Service Worker
|
// DST Collection — Service Worker
|
||||||
// Push Notification Handler
|
// Push Notification + Install/Activate Handler
|
||||||
|
|
||||||
const CACHE_NAME = 'dst-v1';
|
|
||||||
|
|
||||||
// ── Install ──────────────────────────────────────────────────────────────────
|
// ── Install ──────────────────────────────────────────────────────────────────
|
||||||
self.addEventListener('install', () => {
|
self.addEventListener('install', () => {
|
||||||
self.skipWaiting();
|
self.skipWaiting();
|
||||||
});
|
});
|
||||||
|
|
||||||
// ── Activate ─────────────────────────────────────────────────────────────────
|
// ── Activate ──────────────────────────────────────────────────────────────────
|
||||||
self.addEventListener('activate', (event) => {
|
self.addEventListener('activate', (event) => {
|
||||||
event.waitUntil(
|
event.waitUntil(self.clients.claim());
|
||||||
(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));
|
|
||||||
})(),
|
|
||||||
);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
// ── Push Notification ─────────────────────────────────────────────────────────
|
// ── Push Notification ─────────────────────────────────────────────────────────
|
||||||
@ -30,19 +18,18 @@ self.addEventListener('push', (event) => {
|
|||||||
let data = {};
|
let data = {};
|
||||||
try {
|
try {
|
||||||
data = event.data.json();
|
data = event.data.json();
|
||||||
} catch (e) {
|
} catch {
|
||||||
data = { title: 'DST Collection', body: event.data.text() };
|
data = { title: 'DST Collection', body: event.data.text() };
|
||||||
}
|
}
|
||||||
|
|
||||||
const title = data.title || 'DST Collection';
|
const title = data.title ?? 'DST Collection';
|
||||||
const options = {
|
const options = {
|
||||||
body: data.body || '',
|
body: data.body ?? '',
|
||||||
icon: data.icon || '/assets/pwa-192x192.png',
|
icon: data.icon ?? '/assets/pwa-192x192.png',
|
||||||
badge: '/assets/pwa-64x64.png',
|
badge: '/assets/pwa-64x64.png',
|
||||||
data: { url: data.url || '/admin/master/categories' },
|
data: { url: data.url ?? '/admin/master/categories' },
|
||||||
tag: 'dst-notification',
|
tag: 'dst-notification',
|
||||||
renotify: true,
|
renotify: true,
|
||||||
requireInteraction: false,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
event.waitUntil(self.registration.showNotification(title, options));
|
event.waitUntil(self.registration.showNotification(title, options));
|
||||||
@ -52,7 +39,8 @@ self.addEventListener('push', (event) => {
|
|||||||
self.addEventListener('notificationclick', (event) => {
|
self.addEventListener('notificationclick', (event) => {
|
||||||
event.notification.close();
|
event.notification.close();
|
||||||
|
|
||||||
const targetUrl = (event.notification.data && event.notification.data.url)
|
const targetUrl =
|
||||||
|
(event.notification.data && event.notification.data.url)
|
||||||
? event.notification.data.url
|
? event.notification.data.url
|
||||||
: '/admin/dashboard';
|
: '/admin/dashboard';
|
||||||
|
|
||||||
@ -61,7 +49,7 @@ self.addEventListener('notificationclick', (event) => {
|
|||||||
.matchAll({ type: 'window', includeUncontrolled: true })
|
.matchAll({ type: 'window', includeUncontrolled: true })
|
||||||
.then((clientList) => {
|
.then((clientList) => {
|
||||||
for (const client of clientList) {
|
for (const client of clientList) {
|
||||||
if ('navigate' in client) {
|
if ('focus' in client) {
|
||||||
return client.navigate(targetUrl).then((c) => c && c.focus());
|
return client.navigate(targetUrl).then((c) => c && c.focus());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -11,9 +11,23 @@ function urlBase64ToUint8Array(base64String: string): Uint8Array {
|
|||||||
return Uint8Array.from([...rawData].map((char) => char.charCodeAt(0)));
|
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() {
|
export function usePushNotification() {
|
||||||
const isSupported = computed(
|
const isSupported = computed(
|
||||||
() =>
|
() =>
|
||||||
|
typeof window !== 'undefined' &&
|
||||||
'Notification' in window &&
|
'Notification' in window &&
|
||||||
'serviceWorker' in navigator &&
|
'serviceWorker' in navigator &&
|
||||||
'PushManager' in window,
|
'PushManager' in window,
|
||||||
@ -28,13 +42,12 @@ export function usePushNotification() {
|
|||||||
|
|
||||||
async function checkSubscriptionStatus(): Promise<void> {
|
async function checkSubscriptionStatus(): Promise<void> {
|
||||||
if (!isSupported.value) {
|
if (!isSupported.value) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const registration = await navigator.serviceWorker.ready;
|
const registration = await getSwRegistration();
|
||||||
const subscription =
|
const subscription = await registration.pushManager.getSubscription();
|
||||||
await registration.pushManager.getSubscription();
|
|
||||||
isSubscribed.value = !!subscription;
|
isSubscribed.value = !!subscription;
|
||||||
} catch {
|
} catch {
|
||||||
isSubscribed.value = false;
|
isSubscribed.value = false;
|
||||||
@ -43,19 +56,19 @@ return;
|
|||||||
|
|
||||||
async function subscribe(): Promise<void> {
|
async function subscribe(): Promise<void> {
|
||||||
if (!isSupported.value || isLoading.value) {
|
if (!isSupported.value || isLoading.value) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const permission = await Notification.requestPermission();
|
|
||||||
|
|
||||||
if (permission !== 'granted') {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
isLoading.value = true;
|
isLoading.value = true;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const registration = await navigator.serviceWorker.ready;
|
const permission = await Notification.requestPermission();
|
||||||
|
|
||||||
|
if (permission !== 'granted') {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const registration = await getSwRegistration();
|
||||||
const publicKey =
|
const publicKey =
|
||||||
vapidPublicKey ?? import.meta.env.VITE_VAPID_PUBLIC_KEY ?? '';
|
vapidPublicKey ?? import.meta.env.VITE_VAPID_PUBLIC_KEY ?? '';
|
||||||
|
|
||||||
@ -94,15 +107,14 @@ return;
|
|||||||
|
|
||||||
async function unsubscribe(): Promise<void> {
|
async function unsubscribe(): Promise<void> {
|
||||||
if (!isSupported.value || isLoading.value) {
|
if (!isSupported.value || isLoading.value) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
isLoading.value = true;
|
isLoading.value = true;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const registration = await navigator.serviceWorker.ready;
|
const registration = await getSwRegistration();
|
||||||
const subscription =
|
const subscription = await registration.pushManager.getSubscription();
|
||||||
await registration.pushManager.getSubscription();
|
|
||||||
|
|
||||||
if (!subscription) {
|
if (!subscription) {
|
||||||
isSubscribed.value = false;
|
isSubscribed.value = false;
|
||||||
|
|||||||
@ -128,13 +128,14 @@ return;
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
if (perm.key === 'notification') {
|
if (perm.key === 'notification') {
|
||||||
const result = await Notification.requestPermission();
|
|
||||||
|
|
||||||
if (result === 'denied') {
|
|
||||||
perm.state = 'denied';
|
|
||||||
} else if (result === 'granted') {
|
|
||||||
await subscribe();
|
await subscribe();
|
||||||
perm.state = isSubscribed.value ? 'granted' : 'denied';
|
|
||||||
|
if (isSubscribed.value) {
|
||||||
|
perm.state = 'granted';
|
||||||
|
} else if (typeof Notification !== 'undefined' && Notification.permission === 'denied') {
|
||||||
|
perm.state = 'denied';
|
||||||
|
} else {
|
||||||
|
perm.state = 'prompt';
|
||||||
}
|
}
|
||||||
} else if (perm.key === 'camera') {
|
} else if (perm.key === 'camera') {
|
||||||
const stream = await navigator.mediaDevices.getUserMedia({
|
const stream = await navigator.mediaDevices.getUserMedia({
|
||||||
@ -305,10 +306,10 @@ onMounted(() => {
|
|||||||
<Button
|
<Button
|
||||||
v-else-if="perm.state === 'prompt'"
|
v-else-if="perm.state === 'prompt'"
|
||||||
size="sm"
|
size="sm"
|
||||||
:disabled="perm.requesting"
|
:disabled="perm.requesting || (perm.key === 'notification' && isLoading)"
|
||||||
@click="requestPermission(perm)"
|
@click="requestPermission(perm)"
|
||||||
>
|
>
|
||||||
{{ perm.requesting ? 'Meminta...' : 'Izinkan' }}
|
{{ (perm.requesting || (perm.key === 'notification' && isLoading)) ? 'Meminta...' : 'Izinkan' }}
|
||||||
</Button>
|
</Button>
|
||||||
|
|
||||||
<span
|
<span
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user