// Service Worker version const CACHE_VERSION = 'v1.0.0'; const CACHE_NAME = `game-timer-${CACHE_VERSION}`; // Files to cache const CACHE_FILES = [ './', './index.html', '../src/js/app.js', '../src/js/ui/audio.js', './css/styles.css', './manifest.json', './icons/android-chrome-192x192.png', './icons/android-chrome-512x512.png', './icons/apple-touch-icon.png', './icons/favicon-32x32.png', './icons/favicon-16x16.png' ]; // Install event - Cache files self.addEventListener('install', event => { console.log('[ServiceWorker] Install'); event.waitUntil( caches.open(CACHE_NAME) .then(cache => { console.log('[ServiceWorker] Caching app shell'); return cache.addAll(CACHE_FILES); }) .then(() => { console.log('[ServiceWorker] Skip waiting on install'); return self.skipWaiting(); }) ); }); // Activate event - Clean old caches self.addEventListener('activate', event => { console.log('[ServiceWorker] Activate'); event.waitUntil( caches.keys().then(keyList => { return Promise.all(keyList.map(key => { if (key !== CACHE_NAME) { console.log('[ServiceWorker] Removing old cache', key); return caches.delete(key); } })); }) .then(() => { console.log('[ServiceWorker] Claiming clients'); return self.clients.claim(); }) ); }); // Helper function to determine if a response should be cached function shouldCacheResponse(request, response) { // Only cache GET requests if (request.method !== 'GET') return false; // Don't cache errors if (!response || response.status !== 200) return false; // Check if URL should be cached const url = new URL(request.url); // Don't cache query parameters (except common ones for content) if (url.search && !url.search.match(/\?(v|version|cache)=/)) return false; return true; } self.addEventListener('push', event => { console.log('[ServiceWorker] Push received'); let pushData = { title: 'Flic Action', body: 'Button pressed!', data: { action: 'Unknown', button: 'Unknown', batteryLevel: undefined } }; // --- Attempt to parse data payload --- if (event.data) { try { const parsedData = event.data.json(); console.log('[ServiceWorker] Push data:', parsedData); // Use parsed data for notification and message pushData = { title: parsedData.title || pushData.title, body: parsedData.body || pushData.body, data: parsedData.data || pushData.data // Expecting { action: 'SingleClick', button: 'game-button', batteryLevel: 75 } }; } catch (e) { console.error('[ServiceWorker] Error parsing push data:', e); // Use default notification if parsing fails pushData.body = event.data.text() || pushData.body; // Fallback to text } } else { console.log('[ServiceWorker] Push event but no data'); } // --- Send message to client(s) --- const messagePayload = { type: 'flic-action', // Custom message type action: pushData.data.action, // e.g., 'SingleClick', 'DoubleClick', 'Hold' button: pushData.data.button, // e.g., the button name timestamp: pushData.data.timestamp, // e.g., the timestamp of the action batteryLevel: pushData.data.batteryLevel // e.g., the battery level percentage }; // Send message to all open PWA windows controlled by this SW event.waitUntil( self.clients.matchAll({ type: 'window', // Only target window clients includeUncontrolled: true // Include clients that might not be fully controlled yet }).then(clientList => { if (!clientList || clientList.length === 0) { console.log('[ServiceWorker] No client windows found to send message to.'); // If no window is open, we MUST show a notification return self.registration.showNotification(pushData.title, { body: pushData.body, icon: './icons/android-chrome-192x192.png', // Optional: path to an icon data: pushData.data // Pass data if needed when notification is clicked }); } // Post message to each client let messageSent = false; clientList.forEach(client => { console.log(`[ServiceWorker] Posting message to client: ${client.id}`, messagePayload); client.postMessage(messagePayload); messageSent = true; // Mark that we at least tried to send a message }); // Decide whether to still show a notification even if a window is open. // Generally good practice unless you are SURE the app will handle it visibly. // You might choose *not* to show a notification if a client was found and focused. // For simplicity here, we'll still show one. Adjust as needed. if (!messageSent) { // Only show notification if no message was sent? Or always show? return self.registration.showNotification(pushData.title, { body: pushData.body, icon: './icons/android-chrome-192x192.png', data: pushData.data }); } }) ); // --- Show a notification (Important!) --- // Push notifications generally REQUIRE showing a notification to the user // unless the PWA is already in the foreground AND handles the event visually. // It's safer to always show one unless you have complex foreground detection. /* This part is now handled inside the clients.matchAll promise */ /* const notificationOptions = { body: pushData.body, icon: './icons/android-chrome-192x192.png', // Optional: path to an icon data: pushData.data // Attach data if needed when notification is clicked }; event.waitUntil( self.registration.showNotification(pushData.title, notificationOptions) ); */ }); // This helps with navigation after app is installed self.addEventListener('notificationclick', event => { console.log('[ServiceWorker] Notification click received'); event.notification.close(); // Handle the notification click event.waitUntil( self.clients.matchAll({ type: 'window' }) .then(clientList => { for (const client of clientList) { if (client.url.startsWith(self.location.origin) && 'focus' in client) { return client.focus(); } } if (self.clients.openWindow) { return self.clients.openWindow('./'); } }) ); });