// Push Notifications Utility for ParentFlow Web App // Handles Web Push subscription and management const API_BASE_URL = process.env.NEXT_PUBLIC_API_URL || 'http://maternal-api.noru1.ro'; export interface PushSubscriptionData { endpoint: string; keys: { p256dh: string; auth: string; }; } /** * Convert VAPID public key from base64 to Uint8Array */ 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; } /** * Check if push notifications are supported */ export function isPushNotificationSupported(): boolean { if (typeof window === 'undefined') return false; return 'serviceWorker' in navigator && 'PushManager' in window && 'Notification' in window; } /** * Get current notification permission status */ export function getNotificationPermission(): NotificationPermission { if (typeof window === 'undefined' || !('Notification' in window)) { return 'default'; } return Notification.permission; } /** * Request notification permission */ export async function requestNotificationPermission(): Promise { if (!isPushNotificationSupported()) { throw new Error('Push notifications are not supported in this browser'); } const permission = await Notification.requestPermission(); console.log('[Push] Permission result:', permission); return permission; } /** * Get or fetch the VAPID public key */ export async function getVapidPublicKey(token?: string): Promise { try { const headers: HeadersInit = { 'Content-Type': 'application/json', }; if (token) { headers['Authorization'] = `Bearer ${token}`; } const response = await fetch(`${API_BASE_URL}/api/v1/push/vapid-public-key`, { method: 'GET', headers, }); if (!response.ok) { throw new Error(`Failed to get VAPID key: ${response.statusText}`); } const data = await response.json(); return data.publicKey; } catch (error) { console.error('[Push] Error getting VAPID key:', error); throw error; } } /** * Register service worker for push notifications */ export async function registerPushServiceWorker(): Promise { if (!('serviceWorker' in navigator)) { throw new Error('Service Worker not supported'); } try { // Check if already registered const existingRegistration = await navigator.serviceWorker.getRegistration('/push-sw.js'); if (existingRegistration) { console.log('[Push] Service Worker already registered'); return existingRegistration; } // Register new service worker const registration = await navigator.serviceWorker.register('/push-sw.js', { scope: '/', }); console.log('[Push] Service Worker registered:', registration.scope); // Wait for the service worker to be ready await navigator.serviceWorker.ready; return registration; } catch (error) { console.error('[Push] Service Worker registration failed:', error); throw error; } } /** * Subscribe to push notifications */ export async function subscribeToPush(token: string): Promise { if (!isPushNotificationSupported()) { throw new Error('Push notifications are not supported'); } // Request permission first const permission = await requestNotificationPermission(); if (permission !== 'granted') { throw new Error('Notification permission denied'); } try { // Register service worker const registration = await registerPushServiceWorker(); // Get VAPID public key const vapidPublicKey = await getVapidPublicKey(token); const applicationServerKey = urlBase64ToUint8Array(vapidPublicKey); // Subscribe to push const subscription = await registration.pushManager.subscribe({ userVisibleOnly: true, applicationServerKey, }); console.log('[Push] Subscribed to push notifications'); // Send subscription to backend await savePushSubscription(subscription, token); return subscription; } catch (error) { console.error('[Push] Error subscribing to push:', error); throw error; } } /** * Save push subscription to backend */ export async function savePushSubscription( subscription: PushSubscription, token: string ): Promise { try { const subscriptionData = subscription.toJSON(); const response = await fetch(`${API_BASE_URL}/api/v1/push/subscriptions`, { method: 'POST', headers: { 'Content-Type': 'application/json', Authorization: `Bearer ${token}`, }, body: JSON.stringify(subscriptionData), }); if (!response.ok) { throw new Error(`Failed to save subscription: ${response.statusText}`); } console.log('[Push] Subscription saved to backend'); } catch (error) { console.error('[Push] Error saving subscription:', error); throw error; } } /** * Get current push subscription */ export async function getPushSubscription(): Promise { if (!isPushNotificationSupported()) { return null; } try { const registration = await navigator.serviceWorker.getRegistration('/push-sw.js'); if (!registration) { return null; } const subscription = await registration.pushManager.getSubscription(); return subscription; } catch (error) { console.error('[Push] Error getting subscription:', error); return null; } } /** * Unsubscribe from push notifications */ export async function unsubscribeFromPush(token: string): Promise { try { const subscription = await getPushSubscription(); if (!subscription) { console.log('[Push] No active subscription to unsubscribe'); return; } const endpoint = subscription.endpoint; // Unsubscribe locally await subscription.unsubscribe(); console.log('[Push] Unsubscribed locally'); // Remove from backend await fetch(`${API_BASE_URL}/api/v1/push/subscriptions?endpoint=${encodeURIComponent(endpoint)}`, { method: 'DELETE', headers: { Authorization: `Bearer ${token}`, }, }); console.log('[Push] Subscription removed from backend'); } catch (error) { console.error('[Push] Error unsubscribing:', error); throw error; } } /** * Check if user is subscribed to push notifications */ export async function isPushSubscribed(): Promise { const subscription = await getPushSubscription(); return subscription !== null; } /** * Send a test push notification */ export async function sendTestPushNotification(token: string): Promise { try { const response = await fetch(`${API_BASE_URL}/api/v1/push/test`, { method: 'POST', headers: { 'Content-Type': 'application/json', Authorization: `Bearer ${token}`, }, }); if (!response.ok) { throw new Error(`Failed to send test notification: ${response.statusText}`); } console.log('[Push] Test notification sent'); } catch (error) { console.error('[Push] Error sending test notification:', error); throw error; } } /** * Get push notification statistics */ export async function getPushStatistics(token: string): Promise { try { const response = await fetch(`${API_BASE_URL}/api/v1/push/statistics`, { method: 'GET', headers: { 'Content-Type': 'application/json', Authorization: `Bearer ${token}`, }, }); if (!response.ok) { throw new Error(`Failed to get statistics: ${response.statusText}`); } return await response.json(); } catch (error) { console.error('[Push] Error getting statistics:', error); throw error; } } /** * Show a local test notification (no server needed) */ export async function showLocalTestNotification(): Promise { if (getNotificationPermission() !== 'granted') { await requestNotificationPermission(); } if (getNotificationPermission() === 'granted') { const registration = await navigator.serviceWorker.getRegistration('/push-sw.js'); if (registration) { await registration.showNotification('ParentFlow Test', { body: 'This is a local test notification', icon: '/icons/icon-192x192.png', badge: '/icons/icon-72x72.png', tag: 'test', data: { url: '/' }, }); } } }