/** * Service Worker for Push Notifications * LocalGreenChain PWA Support */ // Cache name versioning const CACHE_NAME = 'lgc-cache-v1'; // Files to cache for offline support const STATIC_CACHE = [ '/', '/offline', '/icons/icon-192x192.png' ]; // Install event - cache static assets self.addEventListener('install', (event) => { console.log('[SW] Installing service worker...'); event.waitUntil( caches.open(CACHE_NAME) .then((cache) => { console.log('[SW] Caching static assets'); return cache.addAll(STATIC_CACHE); }) .catch((error) => { console.log('[SW] Failed to cache:', error); }) ); self.skipWaiting(); }); // Activate event - clean up old caches self.addEventListener('activate', (event) => { console.log('[SW] Activating service worker...'); event.waitUntil( caches.keys().then((cacheNames) => { return Promise.all( cacheNames .filter((name) => name !== CACHE_NAME) .map((name) => { console.log('[SW] Deleting old cache:', name); return caches.delete(name); }) ); }) ); self.clients.claim(); }); // Push event - handle incoming push notifications self.addEventListener('push', (event) => { console.log('[SW] Push notification received'); let data = { title: 'LocalGreenChain', body: 'You have a new notification', icon: '/icons/icon-192x192.png', badge: '/icons/badge-72x72.png', data: {} }; if (event.data) { try { data = { ...data, ...event.data.json() }; } catch (e) { data.body = event.data.text(); } } const options = { body: data.body, icon: data.icon, badge: data.badge, vibrate: [100, 50, 100], data: data.data, actions: data.actions || [ { action: 'view', title: 'View', icon: '/icons/check.png' }, { action: 'dismiss', title: 'Dismiss', icon: '/icons/close.png' } ], tag: data.tag || 'lgc-notification', renotify: true, requireInteraction: data.requireInteraction || false }; event.waitUntil( self.registration.showNotification(data.title, options) ); }); // Notification click event - handle user interaction self.addEventListener('notificationclick', (event) => { console.log('[SW] Notification clicked:', event.action); event.notification.close(); if (event.action === 'dismiss') { return; } // Default action or 'view' action const urlToOpen = event.notification.data?.url || '/notifications'; event.waitUntil( clients.matchAll({ type: 'window', includeUncontrolled: true }) .then((windowClients) => { // Check if there's already a window open for (const client of windowClients) { if (client.url.includes(self.location.origin) && 'focus' in client) { client.navigate(urlToOpen); return client.focus(); } } // Open new window if none found if (clients.openWindow) { return clients.openWindow(urlToOpen); } }) ); }); // Notification close event self.addEventListener('notificationclose', (event) => { console.log('[SW] Notification closed'); // Track notification dismissal if needed const notificationData = event.notification.data; if (notificationData?.trackDismissal) { // Could send analytics here } }); // Fetch event - network-first with cache fallback self.addEventListener('fetch', (event) => { // Skip non-GET requests if (event.request.method !== 'GET') return; // Skip API requests if (event.request.url.includes('/api/')) return; event.respondWith( fetch(event.request) .then((response) => { // Clone the response for caching const responseClone = response.clone(); caches.open(CACHE_NAME).then((cache) => { cache.put(event.request, responseClone); }); return response; }) .catch(() => { // Return cached response if available return caches.match(event.request) .then((cachedResponse) => { if (cachedResponse) { return cachedResponse; } // Return offline page for navigation requests if (event.request.mode === 'navigate') { return caches.match('/offline'); } return new Response('Offline', { status: 503 }); }); }) ); }); // Message event - handle messages from client self.addEventListener('message', (event) => { console.log('[SW] Message received:', event.data); if (event.data && event.data.type === 'SKIP_WAITING') { self.skipWaiting(); } if (event.data && event.data.type === 'GET_VERSION') { event.ports[0].postMessage({ version: CACHE_NAME }); } }); // Periodic sync for background updates (if supported) self.addEventListener('periodicsync', (event) => { if (event.tag === 'check-notifications') { event.waitUntil(checkForNewNotifications()); } }); async function checkForNewNotifications() { try { const response = await fetch('/api/notifications?unreadOnly=true&limit=1'); const data = await response.json(); if (data.success && data.data.unreadCount > 0) { self.registration.showNotification('LocalGreenChain', { body: `You have ${data.data.unreadCount} unread notification(s)`, icon: '/icons/icon-192x192.png', badge: '/icons/badge-72x72.png' }); } } catch (error) { console.log('[SW] Failed to check notifications:', error); } } console.log('[SW] Service worker loaded');