/** * Service Worker for Offline-First Scanner PWA * Handles background sync and caching for offline functionality */ const CACHE_NAME = 'bct-scanner-v5'; const STATIC_CACHE = 'bct-static-v5'; // Files to cache for offline access - only static assets, no HTML pages const STATIC_FILES = [ '/manifest.json', '/vite.svg', // Add core assets as needed, but NOT HTML pages to avoid auth conflicts ]; // Install event - cache static assets self.addEventListener('install', (event) => { console.log('Service Worker installing...'); event.waitUntil( caches.open(STATIC_CACHE) .then(cache => { console.log('Caching static files'); return cache.addAll(STATIC_FILES); }) .then(() => { console.log('Service Worker installed successfully'); return self.skipWaiting(); }) .catch(error => { console.error('Service Worker installation failed:', error); }) ); }); // Activate event - clean up old caches self.addEventListener('activate', (event) => { console.log('Service Worker activating...'); event.waitUntil( caches.keys() .then(cacheNames => { return Promise.all( cacheNames.map(cacheName => { if (cacheName !== CACHE_NAME && cacheName !== STATIC_CACHE) { console.log('Deleting old cache:', cacheName); return caches.delete(cacheName); } }) ); }) .then(() => { console.log('Service Worker activated'); return self.clients.claim(); }) ); }); // Fetch event - serve from cache when offline self.addEventListener('fetch', (event) => { const { request } = event; const url = new URL(request.url); // Handle navigation requests - use network-first to avoid auth conflicts if (request.mode === 'navigate') { event.respondWith( fetch(request) .then(response => { // Always use network response for HTML to ensure fresh auth state return response; }) .catch(() => { // Only serve offline fallback if network completely fails // Return a minimal offline page instead of cached routes return new Response(` Offline - Black Canyon Tickets

You're Offline

Please check your internet connection and try again.

`, { status: 200, headers: { 'Content-Type': 'text/html' } }); }) ); return; } // Handle API requests if (url.pathname.startsWith('/api/')) { event.respondWith( fetch(request) .catch(() => { // API offline - return a meaningful response return new Response( JSON.stringify({ error: 'Offline', message: 'API request queued for when connection is restored' }), { status: 503, headers: { 'Content-Type': 'application/json' } } ); }) ); return; } // Handle static assets event.respondWith( caches.match(request) .then(response => { if (response) { return response; } return fetch(request) .then(response => { // Don't cache non-successful responses if (!response || response.status !== 200 || response.type !== 'basic') { return response; } // Clone the response for caching const responseToCache = response.clone(); caches.open(CACHE_NAME) .then(cache => { cache.put(request, responseToCache); }) .catch(error => { console.warn('Failed to cache resource:', error); }); return response; }) .catch(error => { console.warn('Failed to fetch resource:', request.url, error); // Return a basic 404 response for failed requests return new Response('Not found', { status: 404 }); }); }) ); }); // Background Sync for scan queue self.addEventListener('sync', (event) => { console.log('Background sync triggered:', event.tag); if (event.tag === 'sync-scans') { event.waitUntil(syncPendingScans()); } }); // Sync pending scans from IndexedDB async function syncPendingScans() { try { console.log('Syncing pending scans...'); // This would normally interface with IndexedDB // For now, we'll just log the sync attempt // In a real implementation: // 1. Open IndexedDB connection // 2. Get all pending scans // 3. Send each to the verify API // 4. Update scan records with results // 5. Handle conflicts and errors console.log('Scan sync completed'); // Notify clients of sync completion const clients = await self.clients.matchAll(); clients.forEach(client => { client.postMessage({ type: 'SYNC_COMPLETE', timestamp: Date.now() }); }); } catch (error) { console.error('Background sync failed:', error); // Schedule retry with exponential backoff setTimeout(() => { self.registration.sync.register('sync-scans'); }, getBackoffDelay()); } } // Exponential backoff for failed syncs let syncRetryCount = 0; function getBackoffDelay() { const delays = [1000, 2000, 5000, 10000, 30000]; // 1s, 2s, 5s, 10s, 30s const delay = delays[Math.min(syncRetryCount, delays.length - 1)]; syncRetryCount++; return delay; } // Reset retry count on successful sync self.addEventListener('message', (event) => { if (event.data && event.data.type === 'SYNC_SUCCESS') { syncRetryCount = 0; } }); // Handle push notifications (for future use) self.addEventListener('push', (event) => { console.log('Push message received:', event); const options = { body: event.data ? event.data.text() : 'Scanner notification', icon: '/icon-192x192.png', badge: '/badge-72x72.png', vibrate: [100, 50, 100], data: { dateOfArrival: Date.now(), primaryKey: 1 }, actions: [ { action: 'open-scanner', title: 'Open Scanner', icon: '/action-icon.png' } ] }; event.waitUntil( self.registration.showNotification('BCT Scanner', options) ); }); // Handle notification clicks self.addEventListener('notificationclick', (event) => { console.log('Notification clicked:', event); event.notification.close(); event.waitUntil( self.clients.matchAll().then(clients => { // Check if scanner is already open const scannerClient = clients.find(client => client.url.includes('/scan') ); if (scannerClient) { // Focus existing scanner return scannerClient.focus(); } else { // Open new scanner window return self.clients.openWindow('/scan'); } }) ); }); console.log('Service Worker script loaded');