// Service Worker version const CACHE_VERSION = 'v1.0.0'; const CACHE_NAME = `game-timer-${CACHE_VERSION}`; // Files to cache const CACHE_FILES = [ '/', '/index.html', '/app.js', '/audio.js', '/deeplinks.js', '/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' ]; // Valid deep link actions const VALID_ACTIONS = ['start', 'pause', 'toggle', 'nextplayer', 'reset']; // 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(); }) ); }); // Fetch event - Serve from cache, fallback to network self.addEventListener('fetch', event => { console.log('[ServiceWorker] Fetch', event.request.url); // For navigation requests that include our deep link parameters, // skip the cache and go straight to network if (event.request.mode === 'navigate') { const url = new URL(event.request.url); // Check if request has action parameter or hash if (url.searchParams.has('action') || url.hash.includes('action=')) { console.log('[ServiceWorker] Processing deep link navigation'); // Verify the action is valid const action = url.searchParams.get('action') || new URLSearchParams(url.hash.substring(1)).get('action'); if (action && VALID_ACTIONS.includes(action)) { console.log('[ServiceWorker] Valid action found:', action); // For navigation requests with valid actions, let the request go through // so our app can handle the deep link return; } } } event.respondWith( caches.match(event.request) .then(response => { return response || fetch(event.request) .then(res => { // Check if we should cache this response if (shouldCacheResponse(event.request, res)) { return caches.open(CACHE_NAME) .then(cache => { console.log('[ServiceWorker] Caching new resource:', event.request.url); cache.put(event.request, res.clone()); return res; }); } return res; }); }) .catch(error => { console.log('[ServiceWorker] Fetch failed; returning offline page', error); // You could return a custom offline page here }) ); }); // 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; } // Handle deep links from other apps (including Flic) self.addEventListener('message', event => { if (event.data && event.data.type === 'PROCESS_ACTION') { const action = event.data.action; console.log('[ServiceWorker] Received action message:', action); // Validate the action if (!VALID_ACTIONS.includes(action)) { console.warn('[ServiceWorker] Invalid action received:', action); return; } // Broadcast the action to all clients self.clients.matchAll().then(clients => { clients.forEach(client => { client.postMessage({ type: 'ACTION', action: action }); }); }); } }); self.addEventListener('push', event => { console.log('[ServiceWorker] Push received'); const data = event.data ? event.data.json() : {}; const title = data.title || 'Game Timer Notification'; const options = { body: data.body || 'You have a new notification', icon: '/icons/android-chrome-192x192.png', badge: '/icons/android-chrome-192x192.png', data: data }; event.waitUntil( self.registration.showNotification(title, options) ); }); // 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('/'); } }) ); });