// Service Worker for PWA functionality // Caching, offline support, and push notifications const CACHE_NAME = 'route-commerce-v1'; const STATIC_ASSETS = [ '/', '/offline', '/manifest.json', '/favicon.svg', ]; // Install event - cache static assets self.addEventListener('install', (event) => { event.waitUntil( caches.open(CACHE_NAME).then((cache) => { return cache.addAll(STATIC_ASSETS); }) ); self.skipWaiting(); }); // Activate event - clean up old caches self.addEventListener('activate', (event) => { event.waitUntil( caches.keys().then((cacheNames) => { return Promise.all( cacheNames .filter((name) => name !== CACHE_NAME) .map((name) => caches.delete(name)) ); }) ); self.clients.claim(); }); // Fetch event - network first, fallback to cache self.addEventListener('fetch', (event) => { const { request } = event; const url = new URL(request.url); // Skip non-GET requests if (request.method !== 'GET') return; // Skip external requests if (url.origin !== self.location.origin) return; // API requests - network only if (url.pathname.startsWith('/api/')) { event.respondWith( fetch(request).catch(() => { return new Response( JSON.stringify({ error: 'Offline', cached: false }), { headers: { 'Content-Type': 'application/json' } } ); }) ); return; } // Static assets - cache first if ( url.pathname.match(/\.(js|css|png|jpg|jpeg|svg|gif|woff2?)$/) || url.pathname.startsWith('/_next/static/') ) { event.respondWith( caches.match(request).then((cached) => { return cached || fetch(request).then((response) => { const clone = response.clone(); caches.open(CACHE_NAME).then((cache) => { cache.put(request, clone); }); return response; }); }) ); return; } // Pages - network first, fallback to cache event.respondWith( fetch(request) .then((response) => { const clone = response.clone(); caches.open(CACHE_NAME).then((cache) => { cache.put(request, clone); }); return response; }) .catch(() => { return caches.match(request).then((cached) => { if (cached) return cached; // Return offline page for navigation requests if (request.mode === 'navigate') { return caches.match('/offline'); } return new Response('Offline', { status: 503 }); }); }) ); }); // Push notification handler self.addEventListener('push', (event) => { if (!event.data) return; const data = event.data.json(); const { title, body, icon, url, tag } = data; const options = { body, icon: icon || '/icons/icon-192x192.png', badge: '/icons/badge-72x72.png', tag: tag || 'default', data: { url }, actions: [ { action: 'view', title: 'View' }, { action: 'dismiss', title: 'Dismiss' }, ], vibrate: [100, 50, 100], requireInteraction: true, }; event.waitUntil( self.registration.showNotification(title, options) ); }); // Notification click handler self.addEventListener('notificationclick', (event) => { event.notification.close(); const url = event.notification.data?.url || '/'; if (event.action === 'view' || !event.action) { event.waitUntil( self.clients.matchAll({ type: 'window' }).then((clients) => { // Focus existing window if available for (const client of clients) { if (client.url === url && 'focus' in client) { return client.focus(); } } // Open new window if (self.clients.openWindow) { return self.clients.openWindow(url); } }) ); } }); // Background sync for offline actions self.addEventListener('sync', (event) => { if (event.tag === 'sync-orders') { event.waitUntil(syncOrders()); } else if (event.tag === 'sync-water-logs') { event.waitUntil(syncWaterLogs()); } }); async function syncOrders() { // Sync pending orders from IndexedDB console.log('Syncing orders...'); } async function syncWaterLogs() { // Sync pending water logs from IndexedDB console.log('Syncing water logs...'); } // Message handler for cache invalidation self.addEventListener('message', (event) => { if (event.data === 'skipWaiting') { self.skipWaiting(); } else if (event.data === 'clear-cache') { caches.delete(CACHE_NAME); } });