web push notifications
linter / quality (push) Failing after 1m5s
tests / ci (8.3) (push) Failing after 48s
tests / ci (8.4) (push) Failing after 1m3s
tests / ci (8.5) (push) Failing after 1m2s

This commit is contained in:
Daan Meijer
2026-06-21 23:17:33 +02:00
parent 1e0782385b
commit 98dc8659ba
15 changed files with 646 additions and 16 deletions
+4
View File
@@ -49,3 +49,7 @@ initializeTheme();
// This will listen for flash toast data from the server...
initializeFlashToast();
if ('serviceWorker' in navigator) {
navigator.serviceWorker.register('/sw.js');
}
@@ -0,0 +1,54 @@
import { ref } from 'vue';
export function usePushNotifications() {
const isSubscribed = ref(false);
async function subscribe() {
if (!('serviceWorker' in navigator)) {
return;
}
const registration = await navigator.serviceWorker.ready;
const subscription = await registration.pushManager.subscribe({
userVisibleOnly: true,
applicationServerKey: urlBase64ToUint8Array(import.meta.env.VITE_VAPID_PUBLIC_KEY),
});
await axios.post('/subscriptions', subscription);
isSubscribed.value = true;
}
async function unsubscribe() {
if (!('serviceWorker' in navigator)) {
return;
}
const registration = await navigator.serviceWorker.ready;
const subscription = await registration.pushManager.getSubscription();
if (subscription) {
await axios.post('/subscriptions/delete', { endpoint: subscription.endpoint });
await subscription.unsubscribe();
}
isSubscribed.value = false;
}
function urlBase64ToUint8Array(base64String: string) {
const padding = '='.repeat((4 - (base64String.length % 4)) % 4);
const base64 = (base64String + padding).replace(/-/g, '+').replace(/_/g, '/');
const rawData = window.atob(base64);
const outputArray = new Uint8Array(rawData.length);
for (let i = 0; i < rawData.length; ++i) {
outputArray[i] = rawData.charCodeAt(i);
}
return outputArray;
}
return {
isSubscribed,
subscribe,
unsubscribe,
};
}
+11
View File
@@ -1,14 +1,25 @@
<script setup lang="ts">
import AppLayout from '@/layouts/app/AppSidebarLayout.vue';
import type { BreadcrumbItem } from '@/types';
import { usePushNotifications } from '@/composables/usePushNotifications';
const { breadcrumbs = [] } = defineProps<{
breadcrumbs?: BreadcrumbItem[];
}>();
const { isSubscribed, subscribe, unsubscribe } = usePushNotifications();
</script>
<template>
<AppLayout :breadcrumbs="breadcrumbs">
<slot />
<div class="fixed bottom-4 right-4">
<button
@click="isSubscribed ? unsubscribe() : subscribe()"
class="rounded-full bg-blue-500 px-4 py-2 text-white"
>
{{ isSubscribed ? 'Unsubscribe' : 'Subscribe' }}
</button>
</div>
</AppLayout>
</template>
+40
View File
@@ -0,0 +1,40 @@
// This file is based on the one found in the laravel-notification-channels/webpush package.
// We need this to get the VAPID_PUBLIC_KEY from the .env file
const vapidPublicKey = import.meta.env.VITE_VAPID_PUBLIC_KEY;
self.addEventListener('push', (event) => {
const payload = event.data ? event.data.json() : {};
const options = {
body: payload.body,
icon: payload.icon,
badge: payload.badge,
data: {
url: payload.url,
},
};
event.waitUntil(self.registration.showNotification(payload.title, options));
});
self.addEventListener('notificationclick', (event) => {
event.notification.close();
event.waitUntil(
clients.openWindow(event.notification.data.url)
);
});
if (vapidPublicKey) {
self.addEventListener('load', async () => {
if ('serviceWorker' in navigator) {
try {
const registration = await navigator.serviceWorker.register('/sw.js');
console.log('Service worker registered.', registration);
} catch (error) {
console.error('Error registering service worker:', error);
}
}
});
}