Refactor push subscription logic into a composable

This commit is contained in:
Hornwitser 2025-03-07 16:08:45 +01:00
parent 52dfde95d1
commit b2a5b67096
3 changed files with 128 additions and 97 deletions

View file

@ -1,14 +1,14 @@
<template>
<section>
Notifications are: <b>{{ subscription ? "Enabled" : "Disabled" }}</b>
Notifications are: <b>{{ subscribed ? "Enabled" : "Disabled" }}</b>
<br />
<button
:disabled="unsupported"
:disabled="!supported"
@click="onClick"
>
{{ unsupported === undefined ? "Checking for support" : null }}
{{ unsupported === true ? "Notifications are not supported :(." : null }}
{{ unsupported === false ? (subscription ? "Disable notifications" : "Enable notifications") : null }}
{{ supported === undefined ? "Checking for support" : null }}
{{ supported === false ? "Notifications are not supported :(." : null }}
{{ supported === true ? (subscribed ? "Disable notifications" : "Enable notifications") : null }}
</button>
<details>
<summary>Debug</summary>
@ -18,102 +18,18 @@
</template>
<script setup lang="ts">
function notificationUnsupported() {
return (
!("serviceWorker" in navigator)
|| !("PushManager" in window)
|| !("showNotification" in ServiceWorkerRegistration.prototype)
)
}
async function registerAndSubscribe(
vapidPublicKey: string,
onSubscribe: (subs: PushSubscription | null ) => void,
) {
try {
await navigator.serviceWorker.register("/sw.js");
await subscribe(vapidPublicKey, onSubscribe);
} catch (err) {
console.error("Failed to register service worker:", err);
}
}
async function subscribe(
vapidPublicKey: string,
onSubscribe: (subs: PushSubscription | null) => void
) {
try {
const registration = await navigator.serviceWorker.ready;
const subscription = await registration.pushManager.subscribe({
userVisibleOnly: true,
applicationServerKey: vapidPublicKey,
});
console.log("Got subscription object", subscription.toJSON());
await submitSubscription(subscription);
onSubscribe(subscription);
} catch (err) {
console.error("Failed to subscribe:" , err);
}
}
async function unsubscribe(
subscription: PushSubscription,
onUnsubscribed: () => void,
) {
const body = JSON.stringify({ subscription });
await subscription.unsubscribe();
const res = await fetch("/api/unsubscribe", {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body,
});
const result = await res.json();
console.log("/api/unsubscribe returned", result);
onUnsubscribed();
}
async function submitSubscription(subscription: PushSubscription) {
const res = await fetch("/api/subscribe", {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({ subscription }),
});
const result = await res.json();
console.log("/api/subscribe returned", result);
}
async function getSubscription(
onSubscribe: (subs: PushSubscription | null) => void,
) {
const registration = await navigator.serviceWorker.ready;
const subscription = await registration.pushManager.getSubscription();
if (subscription) {
onSubscribe(subscription);
}
}
const unsupported = ref<boolean | undefined>(undefined);
const subscription = ref<PushSubscription | null>(null);
const runtimeConfig = useRuntimeConfig();
const { refresh: refreshSession } = useAccountSession();
const { data: session, refresh: refreshSession } = await useAccountSession();
const { supported, subscription, getSubscription, subscribe, unsubscribe } = usePushNotification();
const subscribed = computed(() => Boolean(subscription.value && session.value?.push))
async function onClick() {
if (!subscription.value)
await registerAndSubscribe(runtimeConfig.public.vapidPublicKey, (subs) => { subscription.value = subs })
if (!subscribed.value)
await subscribe();
else
await unsubscribe(subscription.value, () => { subscription.value = null })
refreshSession();
await unsubscribe();
await refreshSession();
}
onMounted(() => {
unsupported.value = notificationUnsupported()
if (unsupported.value) {
return;
}
getSubscription(subs => { subscription.value = subs });
getSubscription();
})
</script>

View file

@ -0,0 +1,109 @@
function notificationSupported() {
return (
import.meta.client
&& "serviceWorker" in navigator
&& "PushManager" in window
&& "showNotification" in ServiceWorkerRegistration.prototype
);
}
export const usePushNotification = () => {
const subscription = ref<PushSubscription | null>(null);
const supported = ref<boolean | undefined>(undefined);
const runtimeConfig = useRuntimeConfig();
function checkSupport() {
return supported.value = notificationSupported();
}
async function postSubscription(subscription: PushSubscriptionJSON) {
const result = await $fetch("/api/subscribe", {
method: "POST",
body: { subscription },
});
console.log("/api/subscribe returned", result);
}
async function postUnsubscription() {
const result = await $fetch("/api/unsubscribe", {
method: "POST",
});
console.log("/api/unsubscribe returned", result);
}
async function subscribe() {
if (!checkSupport())
return;
let registration;
try {
registration = await navigator.serviceWorker.register("/sw.js");
} catch (err) {
console.error("Failed to register service worker:", err);
return;
}
// Check if we already have a subscription.
if (!subscription.value) {
subscription.value = await registration.pushManager.getSubscription();
}
// Create a new push subscription if none exists.
if (!subscription.value) {
try {
subscription.value = await registration.pushManager.subscribe({
userVisibleOnly: true,
applicationServerKey: runtimeConfig.public.vapidPublicKey,
});
} catch (err) {
console.error("Failed to subscribe:" , err);
return;
}
}
console.log("Subscribing with", subscription.value);
// Tell server about the new subscription.
try {
await postSubscription(subscription.value.toJSON());
} catch (err) {
console.log("Failed to post subscription to server", err)
return;
}
}
async function unsubscribe() {
if (!checkSupport())
return;
// Fetch subscription if it hasn't already been fetched.
if (!subscription.value) {
await getSubscription();
}
if (!subscription.value) {
return;
}
await subscription.value.unsubscribe();
subscription.value = null;
await postUnsubscription();
}
async function getSubscription() {
if (!checkSupport())
return;
const registration = await navigator.serviceWorker.getRegistration("./sw.js");
if (!registration)
return;
return subscription.value = await registration.pushManager.getSubscription();
}
return {
supported,
subscription,
getSubscription,
subscribe,
unsubscribe,
}
}

View file

@ -12,6 +12,7 @@
<script lang="ts" setup>
const { data: session, refresh: sessionRefresh } = useAccountSession();
const { getSubscription, subscribe } = usePushNotification();
const name = ref("");
const result = ref("")
@ -22,6 +23,11 @@ async function logIn() {
body: { name: name.value },
});
result.value = `Server replied: ${res.status} ${res.statusText}`;
// Resubscribe push notifications if the user was subscribed before.
const subscription = await getSubscription();
if (subscription) {
await subscribe();
}
await sessionRefresh();
} catch (err: any) {