const VAPID_PUBLIC_KEY = import.meta.env.VITE_PUBLIC_VAPID_KEY as string; const VITE_BASE_URL = import.meta.env.VITE_BASE_URL as string; function urlBase64ToUint8Array(base64String: string) { 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 registerServiceWorker() { if (!('serviceWorker' in navigator)) { throw new Error('Service workers are not supported in this browser'); } let registration: ServiceWorkerRegistration; try { // Check if service worker is already registered const existingRegistration = await navigator.serviceWorker.getRegistration('/service-worker.js'); if (existingRegistration) { // Service worker already registered, wait for it to be ready registration = await navigator.serviceWorker.ready; } else { // Register new service worker registration = await navigator.serviceWorker.register('/service-worker.js'); // Wait for the service worker to be ready (may take a moment) registration = await navigator.serviceWorker.ready; } } catch (error: any) { throw new Error(`Failed to register service worker: ${error?.message || 'Unknown error'}`); } return registration; } export async function subscribeUserToPush(register: ServiceWorkerRegistration) { if (!VAPID_PUBLIC_KEY) { throw new Error('Missing VAPID public key configuration'); } // Check if already subscribed let subscription: PushSubscription; try { const existingSubscription = await register.pushManager.getSubscription(); if (existingSubscription) { // Already subscribed, check if it's still valid subscription = existingSubscription; } else { // Subscribe to push subscription = await register.pushManager.subscribe({ userVisibleOnly: true, applicationServerKey: urlBase64ToUint8Array(VAPID_PUBLIC_KEY) }); } } catch (error: any) { throw new Error(`Failed to subscribe to push notifications: ${error?.message || 'Unknown error'}`); } // Convert subscription to JSON format for backend const subscriptionJson = subscription.toJSON(); // Attach auth token if available const token = (window as any)?.localStorage?.getItem?.('accessToken') || (document?.cookie || '').match(/accessToken=([^;]+)/)?.[1] || ''; if (!token) { throw new Error('Authentication token not found. Please log in again.'); } // Send subscription to backend try { const response = await fetch(`${VITE_BASE_URL}/api/v1/workflows/notifications/subscribe`, { method: 'POST', headers: { 'Content-Type': 'application/json', 'Authorization': `Bearer ${token}`, }, body: JSON.stringify(subscriptionJson) }); if (!response.ok) { const errorData = await response.json().catch(() => ({ error: 'Unknown error' })); throw new Error(errorData?.error || errorData?.message || `Server error: ${response.status}`); } const result = await response.json(); if (!result.success) { throw new Error(result.error || 'Failed to save subscription'); } } catch (error: any) { // If it's already our error, rethrow it if (error instanceof Error && error.message.includes('Failed')) { throw error; } throw new Error(`Failed to save subscription to server: ${error?.message || 'Network error'}`); } return subscription; } export async function setupPushNotifications() { // Check if notifications are supported if (!('Notification' in window)) { throw new Error('Notifications are not supported in this browser'); } // Check permission status let permission = Notification.permission; if (permission === 'denied') { throw new Error('Notification permission was denied. Please enable notifications in your browser settings and try again.'); } if (permission === 'default') { // Request permission if not already requested permission = await Notification.requestPermission(); if (permission !== 'granted') { throw new Error('Notification permission was denied. Please enable notifications in your browser settings and try again.'); } } // Final check - permission should be 'granted' at this point if (permission !== 'granted') { throw new Error('Notification permission is required. Please grant permission and try again.'); } // Register service worker (or get existing) let reg: ServiceWorkerRegistration; try { reg = await registerServiceWorker(); } catch (error: any) { throw new Error(`Service worker registration failed: ${error?.message || 'Unknown error'}`); } // Subscribe to push try { await subscribeUserToPush(reg); } catch (error: any) { throw error; // Re-throw with detailed message } }