/** * PWA Module * * Handles Progressive Web App functionality: * - Service Worker registration * - Install prompt handling * - Offline detection * - Update notifications * * Note: The Service Worker (sw.js) handles caching. * The SharedWorker (shared-db-worker.js) handles database. * They are separate workers with different purposes. */ // ============================================================================ // Service Worker Registration // ============================================================================ let swRegistration = null; export async function registerServiceWorker() { if (!('serviceWorker' in navigator)) { console.warn('[PWA] Service Workers not supported'); return null; } try { swRegistration = await navigator.serviceWorker.register('/sw.js', { scope: '/' }); console.log('[PWA] Service Worker registered:', swRegistration.scope); // Handle updates swRegistration.addEventListener('updatefound', () => { const newWorker = swRegistration.installing; newWorker.addEventListener('statechange', () => { if (newWorker.state === 'installed' && navigator.serviceWorker.controller) { // New version available console.log('[PWA] New version available'); showUpdateNotification(); } }); }); return swRegistration; } catch (error) { console.error('[PWA] Service Worker registration failed:', error); return null; } } // ============================================================================ // Install Prompt // ============================================================================ let deferredPrompt = null; let installButton = null; /** * Initialize install prompt handling * @param {string|HTMLElement} buttonSelector - Button element or selector */ export function initInstallPrompt(buttonSelector = '#install-btn') { installButton = typeof buttonSelector === 'string' ? document.querySelector(buttonSelector) : buttonSelector; if (!installButton) { console.warn('[PWA] Install button not found:', buttonSelector); return; } // Initially hide the button installButton.style.display = 'none'; // Listen for the beforeinstallprompt event window.addEventListener('beforeinstallprompt', (e) => { e.preventDefault(); deferredPrompt = e; // Show the install button installButton.style.display = 'block'; console.log('[PWA] Install prompt ready'); }); // Handle install button click installButton.addEventListener('click', async () => { if (!deferredPrompt) { // Show manual instructions for Safari showManualInstallInstructions(); return; } deferredPrompt.prompt(); const { outcome } = await deferredPrompt.userChoice; console.log('[PWA] Install prompt outcome:', outcome); deferredPrompt = null; installButton.style.display = 'none'; }); // Hide button if app is already installed window.addEventListener('appinstalled', () => { console.log('[PWA] App installed'); deferredPrompt = null; installButton.style.display = 'none'; }); // Check if running as installed PWA if (window.matchMedia('(display-mode: standalone)').matches) { installButton.style.display = 'none'; } } function showManualInstallInstructions() { const isIOS = /iPad|iPhone|iPod/.test(navigator.userAgent); const isSafari = /^((?!chrome|android).)*safari/i.test(navigator.userAgent); let message = 'To install this app:\n\n'; if (isIOS) { message += '1. Tap the Share button (square with arrow)\n'; message += '2. Scroll down and tap "Add to Home Screen"'; } else if (isSafari) { message += '1. Click File menu\n'; message += '2. Click "Add to Dock"'; } else { message += '1. Click the menu button (three dots)\n'; message += '2. Click "Install" or "Add to Home Screen"'; } alert(message); } // ============================================================================ // Offline Detection // ============================================================================ let offlineIndicator = null; const offlineListeners = new Set(); /** * Initialize offline detection * @param {string|HTMLElement} indicatorSelector - Element to show when offline */ export function initOfflineDetection(indicatorSelector = '#offline-indicator') { offlineIndicator = typeof indicatorSelector === 'string' ? document.querySelector(indicatorSelector) : indicatorSelector; // Set initial state updateOfflineUI(!navigator.onLine); // Listen for online/offline events window.addEventListener('online', () => { console.log('[PWA] Back online'); updateOfflineUI(false); notifyOfflineListeners(false); }); window.addEventListener('offline', () => { console.log('[PWA] Gone offline'); updateOfflineUI(true); notifyOfflineListeners(true); }); } function updateOfflineUI(isOffline) { if (offlineIndicator) { offlineIndicator.style.display = isOffline ? 'block' : 'none'; } // Also toggle a class on body for CSS styling document.body.classList.toggle('is-offline', isOffline); } /** * Subscribe to offline state changes * @param {Function} listener - Callback(isOffline: boolean) * @returns {Function} Unsubscribe function */ export function onOfflineChange(listener) { offlineListeners.add(listener); // Immediately call with current state listener(!navigator.onLine); return () => offlineListeners.delete(listener); } function notifyOfflineListeners(isOffline) { for (const listener of offlineListeners) { try { listener(isOffline); } catch (e) { console.error('[PWA] Offline listener error:', e); } } } /** * Check if currently online */ export function isOnline() { return navigator.onLine; } // ============================================================================ // Update Handling // ============================================================================ let updateCallback = null; /** * Set callback for when updates are available * @param {Function} callback - Called when new version is ready */ export function onUpdateAvailable(callback) { updateCallback = callback; } function showUpdateNotification() { if (updateCallback) { updateCallback(); return; } // Default behavior if (confirm('A new version is available. Reload now?')) { applyUpdate(); } } /** * Apply pending update (reload with new version) */ export function applyUpdate() { if (swRegistration?.waiting) { swRegistration.waiting.postMessage({ type: 'SKIP_WAITING' }); } window.location.reload(); } // ============================================================================ // Communication with Service Worker // ============================================================================ /** * Send a message to the service worker * @param {Object} message - Message to send */ export function postToServiceWorker(message) { navigator.serviceWorker.controller?.postMessage(message); } /** * Request the service worker to cache specific modules * @param {string[]} moduleNames - Array of module names to cache */ export function cacheModules(moduleNames) { postToServiceWorker({ type: 'CACHE_MODULES', payload: { modules: moduleNames } }); } /** * Request the service worker to clear user-specific caches * (Call this on logout) */ export function clearUserCaches() { postToServiceWorker({ type: 'CLEAR_USER_CACHE' }); } // ============================================================================ // Auto-initialization // ============================================================================ /** * Initialize all PWA features * @param {Object} options */ export async function initPWA(options = {}) { const { installButton = '#install-btn', offlineIndicator = '#offline-indicator', autoRegisterSW = true } = options; if (autoRegisterSW) { await registerServiceWorker(); } initInstallPrompt(installButton); initOfflineDetection(offlineIndicator); console.log('[PWA] Initialized'); } // Export for direct use export default { registerServiceWorker, initInstallPrompt, initOfflineDetection, initPWA, isOnline, onOfflineChange, onUpdateAvailable, applyUpdate, postToServiceWorker, cacheModules, clearUserCaches };