146 lines
4.4 KiB
TypeScript
146 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,
|
|
};
|
|
}
|