1/** 2 * PWA Manager - Handles service worker registration and install prompts 3 * @module pwa/pwa-manager 4 */ 5 6class PWAManager { 7 constructor() { 8 this.deferredPrompt = null; 9 this.isInstalled = false; 10 this.registration = null; 11 this.updateAvailable = false; 12 } 13 14 /** 15 * Initialize PWA functionality 16 */ 17 async init() { 18 // Check if already installed 19 this.isInstalled = this.checkIfInstalled(); 20 21 // Register service worker 22 await this.registerServiceWorker(); 23 24 // Set up install prompt handler 25 this.setupInstallPrompt(); 26 27 // Set up app installed handler 28 this.setupAppInstalledHandler(); 29 30 // Set up offline detection 31 this.setupOfflineDetection(); 32 33 console.log('[PWA] Initialized. Installed:', this.isInstalled); 34 } 35 36 /** 37 * Register the service worker 38 */ 39 async registerServiceWorker() { 40 if (!('serviceWorker' in navigator)) { 41 console.warn('[PWA] Service workers not supported'); 42 return null; 43 } 44 45 try { 46 this.registration = await navigator.serviceWorker.register('/sw.js', { 47 scope: '/' 48 }); 49 50 console.log('[PWA] Service worker registered with scope:', this.registration.scope); 51 52 // Check for updates 53 this.registration.addEventListener('updatefound', () => { 54 const newWorker = this.registration.installing; 55 56 newWorker.addEventListener('statechange', () => { 57 if (newWorker.state === 'installed' && navigator.serviceWorker.controller) { 58 this.updateAvailable = true; 59 this.showUpdateNotification(); 60 } 61 }); 62 }); 63 64 // Handle controller change (new SW activated) 65 navigator.serviceWorker.addEventListener('controllerchange', () => { 66 console.log('[PWA] New service worker activated'); 67 }); 68 69 return this.registration; 70 } catch (error) { 71 console.error('[PWA] Service worker registration failed:', error); 72 return null; 73 } 74 } 75 76 /** 77 * Set up the beforeinstallprompt event handler 78 */ 79 setupInstallPrompt() { 80 window.addEventListener('beforeinstallprompt', (event) => { 81 // Prevent the mini-infobar from appearing on mobile 82 event.preventDefault(); 83 84 // Store the event for later use 85 this.deferredPrompt = event; 86 87 // Show custom install button 88 this.showInstallButton(); 89 90 console.log('[PWA] Install prompt available'); 91 }); 92 } 93 94 /** 95 * Set up handler for when app is installed 96 */ 97 setupAppInstalledHandler() { 98 window.addEventListener('appinstalled', () => { 99 this.isInstalled = true; 100 this.deferredPrompt = null; 101 this.hideInstallButton(); 102 103 console.log('[PWA] App installed successfully'); 104 105 // Track installation (optional analytics) 106 this.trackInstallation(); 107 }); 108 } 109 110 /** 111 * Check if the app is already installed 112 */ 113 checkIfInstalled() { 114 // Check display-mode media query 115 if (window.matchMedia('(display-mode: standalone)').matches) { 116 return true; 117 } 118 119 // Check navigator.standalone for iOS 120 if (window.navigator.standalone === true) { 121 return true; 122 } 123 124 // Check if launched from home screen on Android 125 if (document.referrer.includes('android-app://')) { 126 return true; 127 } 128 129 return false; 130 } 131 132 /** 133 * Prompt user to install the app 134 */ 135 async promptInstall() { 136 if (!this.deferredPrompt) { 137 console.log('[PWA] No install prompt available'); 138 return false; 139 } 140 141 // Show the install prompt 142 this.deferredPrompt.prompt(); 143 144 // Wait for user response 145 const { outcome } = await this.deferredPrompt.userChoice; 146 147 console.log('[PWA] User response to install prompt:', outcome); 148 149 // Clear the deferred prompt 150 this.deferredPrompt = null; 151 152 return outcome === 'accepted'; 153 } 154 155 /** 156 * Show the install button in the UI 157 */ 158 showInstallButton() { 159 // Create install button if it doesn't exist 160 let installBtn = document.getElementById('pwaInstallBtn'); 161 162 if (!installBtn) { 163 installBtn = document.createElement('button'); 164 installBtn.id = 'pwaInstallBtn'; 165 installBtn.className = 'pwa-install-btn'; 166 installBtn.innerHTML = '<i class="bi bi-download"></i> Install App'; 167 installBtn.title = 'Install Teachable Machine as an app'; 168 installBtn.setAttribute('aria-label', 'Install app on your device'); 169 170 installBtn.addEventListener('click', () => this.promptInstall()); 171 172 // Add to footer links 173 const footerLinks = document.querySelector('.footer-links');
174 if (footerLinks) { 175 footerLinks.appendChild(installBtn); 176 } 177 } 178 179 installBtn.style.display = 'inline-flex'; 180 } 181 182 /** 183 * Hide the install button 184 */ 185 hideInstallButton() { 186 const installBtn = document.getElementById('pwaInstallBtn'); 187 if (installBtn) { 188 installBtn.style.display = 'none'; 189 } 190 } 191 192 /** 193 * Show notification when update is available 194 */ 195 showUpdateNotification() { 196 // Create update notification 197 const notification = document.createElement('div'); 198 notification.id = 'pwaUpdateNotification'; 199 notification.className = 'pwa-update-notification'; 200 notification.innerHTML = ` 201 <div class="pwa-update-content"> 202 <span><i class="bi bi-arrow-repeat"></i> A new version is available!</span> 203 <button id="pwaUpdateBtn" class="pwa-update-btn">Update</button> 204 <button id="pwaUpdateDismiss" class="pwa-update-dismiss">×</button> 205 </div> 206 `; 207 208 document.body.appendChild(notification); 209 210 // Handle update button click 211 document.getElementById('pwaUpdateBtn').addEventListener('click', () => { 212 this.applyUpdate(); 213 }); 214 215 // Handle dismiss button click 216 document.getElementById('pwaUpdateDismiss').addEventListener('click', () => { 217 notification.remove(); 218 }); 219 } 220 221 /** 222 * Apply the pending update 223 */ 224 applyUpdate() { 225 if (this.registration && this.registration.waiting) { 226 // Tell the waiting service worker to skip waiting 227 this.registration.waiting.postMessage({ type: 'SKIP_WAITING' }); 228 229 // Reload the page to use new service worker 230 window.location.reload(); 231 } 232 } 233 234 /** 235 * Clear the service worker cache 236 */ 237 async clearCache() { 238 if (this.registration && this.registration.active) { 239 this.registration.active.postMessage({ type: 'CLEAR_CACHE' }); 240 console.log('[PWA] Cache clear requested'); 241 } 242 } 243 244 /** 245 * Unregister the service worker 246 */ 247 async unregister() { 248 if (this.registration) { 249 const success = await this.registration.unregister(); 250 console.log('[PWA] Service worker unregistered:', success); 251 return success; 252 } 253 return false; 254 } 255 256 /** 257 * Track installation for analytics (optional) 258 */ 259 trackInstallation() { 260 // You can add analytics tracking here 261 console.log('[PWA] Installation tracked'); 262 } 263 264 /** 265 * Set up offline/online detection 266 */ 267 setupOfflineDetection() { 268 const indicator = document.getElementById('pwaOfflineIndicator'); 269 270 const updateOnlineStatus = () => { 271 if (indicator) { 272 if (navigator.onLine) { 273 indicator.classList.remove('visible'); 274 } else { 275 indicator.classList.add('visible'); 276 } 277 } 278 console.log('[PWA] Online status:', navigator.onLine); 279 }; 280 281 window.addEventListener('online', updateOnlineStatus); 282 window.addEventListener('offline', updateOnlineStatus); 283 284 // Check initial status 285 updateOnlineStatus(); 286 } 287 288 /** 289 * Check if the app can be installed 290 */ 291 canInstall() { 292 return this.deferredPrompt !== null && !this.isInstalled; 293 } 294 295 /** 296 * Get PWA status information 297 */ 298 getStatus() { 299 return { 300 isInstalled: this.isInstalled, 301 canInstall: this.canInstall(), 302 updateAvailable: this.updateAvailable, 303 serviceWorkerActive: this.registration?.active !== null 304 }; 305 } 306} 307 308// Create singleton instance 309const pwaManager = new PWAManager(); 310 311// Auto-initialize when DOM is ready 312if (document.readyState === 'loading') { 313 document.addEventListener('DOMContentLoaded', () => pwaManager.init()); 314} else { 315 pwaManager.init(); 316} 317 318// Export for module usage 319export { pwaManager, PWAManager };
Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.