import { useCallback, useState } from 'react'; type PermissionStatus = 'granted' | 'denied' | 'prompt' | 'unknown'; function urlBase64ToUint8Array(base64String: string): Uint8Array { 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; } function getInitialPermission(): PermissionStatus { if (!('Notification' in window)) { return 'denied'; } return Notification.permission as PermissionStatus; } function getIsSupported(): boolean { return ( 'Notification' in window && 'serviceWorker' in navigator && 'PushManager' in window ); } export function usePushNotification() { const [permission, setPermission] = useState(getInitialPermission); const [isSupported] = useState(getIsSupported); const requestPermission = useCallback(async (): Promise => { if (!('Notification' in window)) { return 'denied'; } const result = await Notification.requestPermission(); setPermission(result as PermissionStatus); return result as PermissionStatus; }, []); const subscribe = useCallback( async (vapidPublicKey: string): Promise => { try { const registration = await navigator.serviceWorker.ready; const subscription = await registration.pushManager.subscribe({ userVisibleOnly: true, applicationServerKey: urlBase64ToUint8Array( vapidPublicKey, ) as BufferSource, }); const { endpoint } = subscription; const key = subscription.getKey('p256dh'); const auth = subscription.getKey('auth'); const response = await fetch('/api/push/subscribe', { method: 'POST', headers: { 'Content-Type': 'application/json', 'X-Requested-With': 'XMLHttpRequest', 'X-XSRF-TOKEN': decodeURIComponent( document.cookie.match(/XSRF-TOKEN=([^;]+)/)?.[1] ?? '', ), }, body: JSON.stringify({ endpoint, public_key: key ? btoa(String.fromCharCode(...new Uint8Array(key))) : null, auth_token: auth ? btoa(String.fromCharCode(...new Uint8Array(auth))) : null, content_encoding: 'aes128gcm', }), }); return response.ok; } catch (error) { console.error( 'Failed to subscribe to push notifications:', error, ); return false; } }, [], ); const unsubscribe = useCallback(async (): Promise => { try { const registration = await navigator.serviceWorker.ready; const subscription = await registration.pushManager.getSubscription(); if (!subscription) { return true; } const response = await fetch('/api/push/unsubscribe', { method: 'DELETE', headers: { 'Content-Type': 'application/json', 'X-Requested-With': 'XMLHttpRequest', 'X-XSRF-TOKEN': decodeURIComponent( document.cookie.match(/XSRF-TOKEN=([^;]+)/)?.[1] ?? '', ), }, body: JSON.stringify({ endpoint: subscription.endpoint, }), }); if (response.ok) { await subscription.unsubscribe(); } return response.ok; } catch (error) { console.error( 'Failed to unsubscribe from push notifications:', error, ); return false; } }, []); return { permission, isSupported, requestPermission, subscribe, unsubscribe, }; }