store/resources/js/composables/usePushNotification.ts

157 lines
4.4 KiB
TypeScript

import { usePage } from '@inertiajs/vue3';
import { computed, onMounted, ref } from 'vue';
function urlBase64ToUint8Array(base64String: string): Uint8Array {
const padding = '='.repeat((4 - (base64String.length % 4)) % 4);
const base64 = (base64String + padding)
.replace(/-/g, '+')
.replace(/_/g, '/');
const rawData = atob(base64);
return Uint8Array.from([...rawData].map((char) => char.charCodeAt(0)));
}
export function usePushNotification() {
const isSupported = computed(
() =>
'Notification' in window &&
'serviceWorker' in navigator &&
'PushManager' in window,
);
const isSubscribed = ref(false);
const isLoading = ref(false);
const page = usePage();
const vapidPublicKey = (page.props as Record<string, unknown>)
.vapidPublicKey as string | undefined;
async function checkSubscriptionStatus(): Promise<void> {
if (!isSupported.value) {
return;
}
try {
const registration = await navigator.serviceWorker.ready;
const subscription =
await registration.pushManager.getSubscription();
isSubscribed.value = !!subscription;
} catch {
isSubscribed.value = false;
}
}
async function subscribe(): Promise<void> {
if (!isSupported.value || isLoading.value) {
return;
}
const permission = await Notification.requestPermission();
if (permission !== 'granted') {
return;
}
isLoading.value = true;
try {
const registration = await navigator.serviceWorker.ready;
const publicKey =
vapidPublicKey ?? import.meta.env.VITE_VAPID_PUBLIC_KEY ?? '';
const subscription = await registration.pushManager.subscribe({
userVisibleOnly: true,
applicationServerKey: urlBase64ToUint8Array(publicKey),
});
const json = subscription.toJSON();
await fetch('/push-subscriptions', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-CSRF-TOKEN':
(
document.querySelector(
'meta[name="csrf-token"]',
) as HTMLMetaElement
)?.content ?? '',
},
body: JSON.stringify({
endpoint: subscription.endpoint,
publicKey: json.keys?.p256dh ?? '',
authToken: json.keys?.auth ?? '',
}),
});
isSubscribed.value = true;
} catch (error) {
console.error('[PushNotification] Subscribe failed:', error);
} finally {
isLoading.value = false;
}
}
async function unsubscribe(): Promise<void> {
if (!isSupported.value || isLoading.value) {
return;
}
isLoading.value = true;
try {
const registration = await navigator.serviceWorker.ready;
const subscription =
await registration.pushManager.getSubscription();
if (!subscription) {
isSubscribed.value = false;
return;
}
await fetch('/push-subscriptions', {
method: 'DELETE',
headers: {
'Content-Type': 'application/json',
'X-CSRF-TOKEN':
(
document.querySelector(
'meta[name="csrf-token"]',
) as HTMLMetaElement
)?.content ?? '',
},
body: JSON.stringify({ endpoint: subscription.endpoint }),
});
await subscription.unsubscribe();
isSubscribed.value = false;
} catch (error) {
console.error('[PushNotification] Unsubscribe failed:', error);
} finally {
isLoading.value = false;
}
}
async function toggle(): Promise<void> {
if (isSubscribed.value) {
await unsubscribe();
} else {
await subscribe();
}
}
onMounted(() => {
void checkSubscriptionStatus();
});
return {
isSupported,
isSubscribed,
isLoading,
subscribe,
unsubscribe,
toggle,
};
}