/** * Push Notification Management * Handles subscription, unsubscription, and UI updates for web push notifications */ class PushNotificationManager { constructor() { this.serviceWorkerRegistration = null; this.vapidKey = null; this.isSupported = this.checkSupport(); } /** * Check if push notifications are supported */ checkSupport() { return ( 'serviceWorker' in navigator && 'PushManager' in window && 'Notification' in window ); } /** * Initialize push notification system * Must be called after page load */ async init() { if (!this.isSupported) { console.log('Push notifications not supported in this browser'); return false; } try { // Get service worker registration const registrations = await navigator.serviceWorker.getRegistrations(); this.serviceWorkerRegistration = registrations.find( reg => reg.scope === window.location.origin + '/' ); if (!this.serviceWorkerRegistration) { console.warn('Service Worker not found, retrying...'); // Try registering if not already done try { this.serviceWorkerRegistration = await navigator.serviceWorker.register('/static/service-worker.js'); } catch (err) { console.error('Failed to register Service Worker:', err); return false; } } // Fetch VAPID public key from server const keyResponse = await fetch('/api/push/vapid-key'); if (keyResponse.ok) { const keyData = await keyResponse.json(); this.vapidKey = keyData.vapid_key; } else { console.warn('Failed to fetch VAPID key'); return false; } return true; } catch (error) { console.error('Failed to initialize push notifications:', error); return false; } } /** * Request notification permission from user */ async requestPermission() { if (!this.isSupported) { console.warn('Push notifications not supported'); return false; } if (Notification.permission === 'granted') { console.log('Push notifications already permitted'); return true; } if (Notification.permission === 'denied') { console.warn('Push notifications have been denied by user'); return false; } try { const permission = await Notification.requestPermission(); return permission === 'granted'; } catch (error) { console.error('Error requesting notification permission:', error); return false; } } /** * Subscribe to push notifications */ async subscribe() { if (!this.isSupported) { console.error('Push notifications not supported'); return false; } if (!this.serviceWorkerRegistration) { console.error('Service Worker not initialized'); return false; } if (!this.vapidKey) { console.error('VAPID key not available'); return false; } try { // Check if already subscribed const existingSubscription = await this.serviceWorkerRegistration.pushManager.getSubscription(); if (existingSubscription) { console.log('Already subscribed to push notifications'); return await this.saveSubscriptionToServer(existingSubscription); } // Request permission if needed const hasPermission = await this.requestPermission(); if (!hasPermission) { console.warn('User denied notification permission'); return false; } // Subscribe to push service const subscription = await this.serviceWorkerRegistration.pushManager.subscribe({ userVisibleOnly: true, applicationServerKey: this.urlBase64ToUint8Array(this.vapidKey) }); // Save subscription to server return await this.saveSubscriptionToServer(subscription); } catch (error) { console.error('Failed to subscribe to push notifications:', error); return false; } } /** * Unsubscribe from push notifications */ async unsubscribe() { if (!this.serviceWorkerRegistration) { console.error('Service Worker not initialized'); return false; } try { const subscription = await this.serviceWorkerRegistration.pushManager.getSubscription(); if (!subscription) { console.warn('No active push subscription'); return false; } // Remove subscription on server const success = await this.removeSubscriptionFromServer(subscription); // Unsubscribe from push service if (success) { await subscription.unsubscribe(); return true; } return false; } catch (error) { console.error('Failed to unsubscribe from push notifications:', error); return false; } } /** * Check if user is subscribed to push notifications */ async isSubscribed() { if (!this.serviceWorkerRegistration) { return false; } try { const subscription = await this.serviceWorkerRegistration.pushManager.getSubscription(); return subscription !== null; } catch (error) { console.error('Failed to check subscription status:', error); return false; } } /** * Save subscription to server */ async saveSubscriptionToServer(subscription) { try { const response = await fetch('/api/push/subscribe', { method: 'POST', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify({ subscription: subscription.toJSON() }) }); if (response.ok) { const data = await response.json(); console.log('Subscription saved to server:', data.message); return true; } else { const error = await response.json(); console.error('Failed to save subscription:', error.error); return false; } } catch (error) { console.error('Error communicating with server:', error); return false; } } /** * Remove subscription from server */ async removeSubscriptionFromServer(subscription) { try { const response = await fetch('/api/push/unsubscribe', { method: 'POST', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify({ endpoint: subscription.endpoint }) }); if (response.ok) { console.log('Subscription removed from server'); return true; } else { const error = await response.json(); console.error('Failed to remove subscription:', error.error); return false; } } catch (error) { console.error('Error communicating with server:', error); return false; } } /** * Get all subscriptions for current user */ async getSubscriptions() { try { const response = await fetch('/api/push/subscriptions'); if (response.ok) { const data = await response.json(); return data.subscriptions || []; } return []; } catch (error) { console.error('Error fetching subscriptions:', error); return []; } } /** * Send test notification (admin only) */ async sendTestNotification(targetUser = null) { try { const response = await fetch('/api/push/test', { method: 'POST', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify({ target_user: targetUser }) }); if (response.ok) { const data = await response.json(); console.log('Test notification sent:', data.message); return true; } else { const error = await response.json(); console.error('Failed to send test notification:', error.error); return false; } } catch (error) { console.error('Error sending test notification:', error); return false; } } /** * Convert VAPID key from base64 string to Uint8Array * Required for subscribing to push service */ urlBase64ToUint8Array(base64String) { 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; } } // Create global instance const pushNotificationManager = new PushNotificationManager(); /** * Show notification subscription UI (typically in settings) */ function showPushNotificationSettings() { const container = document.getElementById('push-notification-settings'); if (!container) return; if (!pushNotificationManager.isSupported) { container.innerHTML = '
Push-Benachrichtigungen werden in diesem Browser nicht unterstützt.
'; return; } const html = `Erhalten Sie Benachrichtigungen für wichtige Ereignisse direkt auf Ihrem Gerät.