/** * Client-side push notification helpers. * All functions are safe to call in the browser only. */ export function isNotificationSupported(): boolean { return ( typeof window !== 'undefined' && 'serviceWorker' in navigator && 'PushManager' in window && 'Notification' in window ); } export function getNotificationPermission(): NotificationPermission | 'unsupported' { if (!isNotificationSupported()) return 'unsupported'; return Notification.permission; } export async function requestNotificationPermission(): Promise { return Notification.requestPermission(); } export async function registerServiceWorker(): Promise { if (!('serviceWorker' in navigator)) return null; try { const registration = await navigator.serviceWorker.register('/sw.js'); return registration; } catch (err) { console.error('[Push] Service worker registration failed:', err); return null; } } 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; } export async function subscribeToPush( registration: ServiceWorkerRegistration ): Promise { const vapidPublicKey = process.env.NEXT_PUBLIC_VAPID_PUBLIC_KEY; if (!vapidPublicKey) { console.error('[Push] VAPID public key not configured'); return null; } try { const subscription = await registration.pushManager.subscribe({ userVisibleOnly: true, applicationServerKey: urlBase64ToUint8Array(vapidPublicKey), }); return subscription; } catch (err) { console.error('[Push] Subscribe failed:', err); return null; } } export async function sendSubscriptionToServer( subscription: PushSubscription ): Promise { try { const res = await fetch('/api/push/subscribe', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ endpoint: subscription.endpoint, keys: { p256dh: btoa(String.fromCharCode(...new Uint8Array(subscription.getKey('p256dh')!))), auth: btoa(String.fromCharCode(...new Uint8Array(subscription.getKey('auth')!))), }, }), }); return res.ok; } catch (err) { console.error('[Push] Failed to send subscription to server:', err); return false; } } export async function unsubscribeFromPush( registration: ServiceWorkerRegistration ): Promise { try { const subscription = await registration.pushManager.getSubscription(); if (!subscription) return true; // Delete from server await fetch('/api/push/subscribe', { method: 'DELETE', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ endpoint: subscription.endpoint }), }); // Unsubscribe locally await subscription.unsubscribe(); return true; } catch (err) { console.error('[Push] Unsubscribe failed:', err); return false; } } export async function isCurrentlySubscribed(): Promise { if (!isNotificationSupported()) return false; try { const registration = await navigator.serviceWorker.ready; const subscription = await registration.pushManager.getSubscription(); return !!subscription; } catch { return false; } }