// Custom Service Worker for Nūr — Muslim Companion // Handles push notifications for prayer times // Workbox precache manifest injection point self.__WB_MANIFEST; const CACHE_NAME = 'nur-v1'; const PRAYER_CACHE = 'prayer-times-cache'; // Install event self.addEventListener('install', (event) => { event.waitUntil( caches.open(CACHE_NAME).then((cache) => { return cache.addAll([ '/', '/index.html', '/manifest.webmanifest', '/icon-192.png', '/icon-512.png', '/favicon.svg', '/apple-touch-icon.png' ]); }) ); self.skipWaiting(); }); // Activate event self.addEventListener('activate', (event) => { event.waitUntil( caches.keys().then((cacheNames) => { return Promise.all( cacheNames .filter((name) => name !== CACHE_NAME && name !== PRAYER_CACHE) .map((name) => caches.delete(name)) ); }) ); self.clients.claim(); }); // Fetch event - network first for API, cache first for static self.addEventListener('fetch', (event) => { const url = new URL(event.request.url); // API calls - network first with cache fallback if (url.origin === 'https://api.aladhan.com' || url.origin === 'https://api.alquran.cloud') { event.respondWith( fetch(event.request) .then((response) => { const responseClone = response.clone(); caches.open(PRAYER_CACHE).then((cache) => { cache.put(event.request, responseClone); }); return response; }) .catch(() => caches.match(event.request)) ); return; } // Static assets - cache first event.respondWith( caches.match(event.request).then((cachedResponse) => { if (cachedResponse) return cachedResponse; return fetch(event.request).then((response) => { const responseClone = response.clone(); caches.open(CACHE_NAME).then((cache) => { cache.put(event.request, responseClone); }); return response; }); }) ); }); // Push event - handle push notifications self.addEventListener('push', (event) => { if (!event.data) return; const data = event.data.json(); const options = { body: data.body, icon: '/icon-192.png', badge: '/icon-192.png', vibrate: [200, 100, 200], tag: data.tag || 'prayer-notification', renotify: true, requireInteraction: true, actions: [ { action: 'open', title: 'Open App' }, { action: 'dismiss', title: 'Dismiss' } ], data: data.data || {} }; event.waitUntil( self.registration.showNotification(data.title, options) ); }); // Notification click event self.addEventListener('notificationclick', (event) => { event.notification.close(); if (event.action === 'dismiss') return; event.waitUntil( clients.matchAll({ type: 'window', includeUncontrolled: true }).then((clientList) => { // If app is already open, focus it for (const client of clientList) { if (client.url.includes(self.location.origin) && 'focus' in client) { return client.focus(); } } // Otherwise open new window if (clients.openWindow) { return clients.openWindow('/'); } }) ); }); // Background sync for periodic prayer time updates self.addEventListener('sync', (event) => { if (event.tag === 'prayer-times-sync') { event.waitUntil(syncPrayerTimes()); } }); // Periodic background sync (if supported) self.addEventListener('periodicsync', (event) => { if (event.tag === 'prayer-times-periodic') { event.waitUntil(syncPrayerTimes()); } }); async function syncPrayerTimes() { try { // Get cached prayer times or fetch new ones const cache = await caches.open(PRAYER_CACHE); const cached = await cache.match('/prayer-times'); if (cached) { const data = await cached.json(); scheduleNotifications(data); } } catch (e) { console.error('Failed to sync prayer times:', e); } } function scheduleNotifications(prayerData) { const now = Date.now(); const prayers = ['Fajr', 'Dhuhr', 'Asr', 'Maghrib', 'Isha']; prayers.forEach((prayer) => { const prayerTimeStr = prayerData.timings[prayer]; if (!prayerTimeStr) return; const [hours, minutes] = prayerTimeStr.split(':').map(Number); const prayerDate = new Date(); prayerDate.setHours(hours, minutes, 0, 0); const timeUntilPrayer = prayerDate.getTime() - now; // Schedule notification 5 minutes before prayer const notifyTime = timeUntilPrayer - 5 * 60 * 1000; if (notifyTime > 0 && notifyTime < 24 * 60 * 60 * 1000) { setTimeout(() => { self.registration.showNotification(`Time for ${prayer}`, { body: `It's time for ${prayer} prayer (${prayerTimeStr})`, icon: '/icon-192.png', badge: '/icon-192.png', vibrate: [200, 100, 200], tag: `prayer-${prayer}`, renotify: true, requireInteraction: true }); }, notifyTime); } }); } // Message event - communicate with main thread self.addEventListener('message', (event) => { if (event.data.type === 'SCHEDULE_PRAYERS') { scheduleNotifications(event.data.payload); } if (event.data.type === 'SKIP_WAITING') { self.skipWaiting(); } }); console.log('[Nūr SW] Service Worker loaded');