1// Give the service worker access to Firebase Messaging. 2// Service workers require the compat version (modular SDK doesn't support importScripts) 3importScripts('https://www.gstatic.com/firebasejs/10.14.1/firebase-app-compat.js'); 4importScripts('https://www.gstatic.com/firebasejs/10.14.1/firebase-messaging-compat.js'); 5 6const SW_ENV = 'prod'; 7const CURRENT_SITE = 'Gala'; 8const PUSH_HOST = 'https\u003A\/\/push.lefigaro.fr'; 9 10// â ï¸ CRITICAL: Install and activate listeners for immediate service worker updates 11// Without these, old versions may continue running until the browser forces an update 12self.addEventListener('install', (event) => { 13 event.waitUntil(self.skipWaiting()); 14}); 15 16self.addEventListener('activate', (event) => { 17 // Claim all clients immediately so new version takes control 18 // Also clean up old data if needed 19 event.waitUntil( 20 self.clients.claim().then(() => { 21 if (typeof log === 'function') { 22 log('Service worker activated and claimed clients'); 23 } 24 }) 25 ); 26}); 27 28// â ï¸ CRITICAL: Define notificationclick BEFORE Firebase initialization 29// Firebase may otherwise overwrite custom notification click behavior 30// Handle notification click events 31self.addEventListener('notificationclick', event => { 32 event.notification.close(); 33 34 event.waitUntil((async () => { 35 try { 36 const { data: jsonData = '{}' } = event.notification; 37 const data = JSON.parse(jsonData); 38 const { push_id: pushId = null, deviceId: dataDeviceId = null } = data; 39 40 // Fall back to IndexedDB when deviceId is not present in notification data 41 // (service workers cannot access localStorage directly) 42 const deviceId = dataDeviceId ?? await getDeviceIdFromDB(); 43 44 const tasks = []; 45 46 if (pushId) { 47 tasks.push(reportPushOpen(pushId, deviceId)); 48 } 49 50 if (data.url) { 51 tasks.push(handleNotificationClick(data.url)); 52 } 53 54 if (tasks.length > 0) { 55 await Promise.all(tasks); 56 } 57 } catch (error) { 58 // Log will be available after helper functions are defined 59 if (typeof log === 'function') { 60 log('Notification clicking failed.', { error }, 'warn'); 61 } 62 } 63 })()); 64}); 65 66self.addEventListener('notificationclose', event => { 67 if (typeof log === 'function') { 68 log('Notification closed.', event); 69 } 70}); 71 72// Notification permission state (updated via postMessage from main thread) 73const NOTIFICATIONS_GRANTED = 'granted'; 74const PERMISSION_DB_NAME = 'firebase-notification-permissions'; 75const PERMISSION_STORE_NAME = 'permissions'; 76const PERMISSION_KEY = 'app-permission-state'; 77const DEVICE_ID_DB_KEY = 'device-id'; 78let cachedNotificationPermission = null; // Will be set by main thread via postMessage 79 80// In-memory deduplication of push_ids to prevent showing the same notification twice 81// (e.g. backend retry after a failed FCM delivery, or duplicate FCM dispatch) 82const processedPushIds = new Set(); 83const PUSH_ID_TTL_MS = 60 * 1000; // 60s â well beyond any realistic retry window 84 85/** 86 * Opens (or creates) the IndexedDB database used to persist notification permission state 87 * across service worker restarts and browser closes. 88 * 89 * @returns {Promise<IDBDatabase>} The opened database instance 90 */ 91async function openPermissionDB() { 92 return new Promise((resolve, reject) => { 93 const request = indexedDB.open(PERMISSION_DB_NAME, 1); 94 95 request.onerror = () => reject(request.error); 96 request.onsuccess = () => resolve(request.result); 97 98 request.onupgradeneeded = (event) => { 99 const db = event.target.result; 100 if (!db.objectStoreNames.contains(PERMISSION_STORE_NAME)) { 101 db.createObjectStore(PERMISSION_STORE_NAME); 102 if (typeof log === 'function') { 103 log('IndexedDB created for permission storage.', { dbName: PERMISSION_DB_NAME }); 104 } 105 } 106 }; 107 }); 108} 109 110/** 111 * Persists a value to IndexedDB under the given key so it survives service worker restarts. 112 * 113 * @param {string}
113 key - The key to store the value under 114 * @param {string} value - The value to persist 115 * 116 * @returns {Promise<void>} 117 */ 118async function saveValueToDB(key, value) { 119 let db; 120 try { 121 db = await openPermissionDB(); 122 const transaction = db.transaction([PERMISSION_STORE_NAME], 'readwrite'); 123 const store = transaction.objectStore(PERMISSION_STORE_NAME); 124 store.put(value, key); 125 126 return new Promise((resolve, reject) => { 127 transaction.oncomplete = () => { 128 db.close(); 129 if (typeof log === 'function') { 130 log('Value saved to IndexedDB.', { key, value }); 131 } 132 resolve(); 133 }; 134 transaction.onerror = () => { 135 db.close(); 136 reject(transaction.error); 137 }; 138 transaction.onabort = () => { 139 db.close(); 140 reject(new Error('Transaction aborted')); 141 }; 142 }); 143 } catch (error) { 144 if (db) { 145 db.close(); 146 } 147 if (typeof log === 'function') { 148 log('Failed to save value to IndexedDB', { key, error }, 'warn'); 149 } 150 throw error; 151 } 152} 153 154/** 155 * Retrieves a persisted value from IndexedDB by key. 156 * Returns null if no value has been stored yet. 157 * 158 * @param {string} key - The key to retrieve 159 * 160 * @returns {Promise<string|null>} The stored value, or null if not found 161 */ 162async function getValueFromDB(key) { 163 let db; 164 try { 165 db = await openPermissionDB(); 166 const transaction = db.transaction([PERMISSION_STORE_NAME], 'readonly'); 167 const store = transaction.objectStore(PERMISSION_STORE_NAME); 168 const request = store.get(key); 169 170 return new Promise((resolve, reject) => { 171 let result; 172 173 request.onsuccess = () => { 174 result = request.result || null; 175 }; 176 177 request.onerror = () => { 178 reject(request.error); 179 }; 180 181 transaction.oncomplete = () => { 182 db.close(); 183 if (typeof log === 'function') { 184 log('Value retrieved from IndexedDB.', { key, value: result }); 185 } 186 resolve(result); 187 }; 188 189 transaction.onerror = () => { 190 db.close(); 191 reject(transaction.error); 192 }; 193 194 transaction.onabort = () => { 195 db.close(); 196 reject(new Error('Transaction aborted')); 197 }; 198 }); 199 } catch (error) { 200 if (db) { 201 db.close(); 202 } 203 if (typeof log === 'function') { 204 log('Failed to read value from IndexedDB', { key, error }, 'warn'); 205 } 206 return null; 207 } 208} 209 210const savePermissionToDB = (permission) => saveValueToDB(PERMISSION_KEY, permission); 211const getPermissionFromDB = () => getValueFromDB(PERMISSION_KEY); 212const saveDeviceIdToDB = (deviceId) => saveValueToDB(DEVICE_ID_DB_KEY, deviceId); 213const getDeviceIdFromDB = () => getValueFromDB(DEVICE_ID_DB_KEY); 214 215/** 216 * Strips HTML tags and decodes common HTML entities from a string. 217 * Used to sanitize notification title and body before display. 218 * 219 * @param {string} input - The raw string potentially containing HTML 220 * 221 * @returns {string} The sanitized plain-text string 222 */ 223function stripTags(input) { 224 if (!input) return ''; 225 // Remove HTML tags and decode common HTML entities for security 226 return input 227 .replace(/<\/?([a-z][a-z0-9]*)\b[^>]*>/gi, '') 228 .replace(/</g, '<') 229 .replace(/>/g, '>') 230 .replace(/&/g, '&') 231 .replace(/"/g, '"') 232 .replace(/'/g, "'") 233 .trim(); 234} 235 236/** 237 * Conditional logger â only emits output when the Twig `debug` flag is truthy. 238 * Delegates to `console[level]` so callers can use 'log', 'warn', or 'error'. 239 * 240 * @param {string} message - The message to log 241 * @param {object} [context={}] - Additional context object to log alongside the message 242 * @param {string} [level='log'] - The console method to use ('log', 'warn', 'error') 243 */ 244function log(message, context = {}, level = 'log') { 245 } 246 247/** 248 * Removes keys whose value is falsy or the string 'null' from an object in place. 249 * Used to strip unset optional notification fields before passing them to showNotification. 250 * 251 * @param {object} obj - The object to mutate 252 * 253 * @returns {object} The same object with empty values removed 254 */ 255function removeEmptyValues(obj) {
256 const keysToDelete = Object.keys(obj).filter(propName => !obj[propName] || obj[propName] === 'null'); 257 keysToDelete.forEach(key => delete obj[key]); 258 return obj; 259} 260 261/** 262 * Wrapper around `self.registration.showNotification` that adds structured logging 263 * and re-throws errors so callers can apply their own fallback strategy. 264 * 265 * @param {string} title - The notification title 266 * @param {NotificationOptions} options - Standard Web Notification options object 267 * @param {object} [context={}] - Additional context forwarded to the logger 268 * 269 * @returns {Promise<void>} 270 */ 271async function showNotificationSafely(title, options, context = {}) { 272 try { 273 log('Attempting to show notification', { title, options, context }); 274 275 const notificationPromise = self.registration.showNotification(title, options); 276 277 const result = await notificationPromise; 278 279 log('Notification successfully shown', { title, tag: options?.tag, context }); 280 281 return result; 282 } catch (error) { 283 log('Failed to show notification', { title, options, error, context }, 'error'); 284 285 // Re-throw so caller knows it failed 286 throw error; 287 } 288} 289 290/** 291 * Reports a push notification open event to the push service. 292 * Fires-and-forgets a GET with the push ID and device ID so the backend can 293 * track open rates. Failures are logged but never rethrown so a network error 294 * never blocks the navigation. 295 * 296 * @param {string|null} pushId - The unique push identifier (payload.data.push_id) 297 * @param {string|null} deviceId - The persistent browser device ID 298 * 299 * @returns {Promise<void>} - Resolves when the push open report is sent (or fails), but does not block on the result 300 */ 301async function reportPushOpen(pushId, deviceId) { 302 try { 303 const url = `${PUSH_HOST}/service/pushopen/${encodeURIComponent(pushId)}${deviceId ? `/${encodeURIComponent(deviceId)}` : ''}`; 304 const response = await fetch(url, { 305 method: 'GET', 306 mode: 'no-cors', 307 }); 308 309 log('Push open reported successfully (no-cors, response opaque).', { response, pushId, deviceId }); 310 } catch (error) { 311 log('Failed to report push open (no-cors, response opaque).', { error, pushId, deviceId }, 'warn'); 312 } 313} 314 315/** 316 * Handles a notification click by focusing an existing window already at the target URL 317 * or opening a new one. Uses origin + pathname comparison to tolerate trailing slashes, 318 * query strings, and hash fragments. 319 * 320 * @param {string} url - The URL to open or focus 321 * 322 * @returns {Promise<void>} 323 */ 324async function handleNotificationClick(url) { 325 try { 326 const windowClients = await clients.matchAll({ type: 'window' }); 327 328 // If a Window tab matching the targeted URL already exists, focus that. 329 // Use origin + pathname comparison instead of strict URL equality to handle 330 // cases where the tab URL differs only by trailing slash, query string or hash. 331 let targetUrl; 332 try { 333 targetUrl = new URL(url, self.registration.scope); 334 } catch { 335 targetUrl = null; 336 } 337 338 const existingWindow = windowClients.find(windowClient => { 339 if (!targetUrl) return windowClient.url === url; 340 try { 341 const clientUrl = new URL(windowClient.url); 342 return clientUrl.origin === targetUrl.origin && clientUrl.pathname === targetUrl.pathname; 343 } catch { 344 return false; 345 } 346 }); 347 348 if (existingWindow) { 349 log('Focusing existing window for notification URL.', { url }); 350 return existingWindow.focus(); 351 } 352 353 // Otherwise, open a new tab to the applicable URL and focus it 354 log('Opening new window for notification URL.', { url }); 355 const newWindow = await clients.openWindow(url); 356 if (newWindow) { 357 log('New window opened and focused.', { url }); 358 return newWindow.focus(); 359 } else { 360 // clients.openWindow() returns null on iOS Safari and Chrome Android 361 // even when the tab was successfully opened. This is expected behaviour â 362 // the browser opened the window but didn't return a client reference. 363 log('Window opened (no client reference returned â expected on mobile browsers).', { url }); 364 } 365 } catch (error) { 366 log('Error handling notification click', { error, url }, 'error'); 367 } 368} 369 370/** 371 * Resolves the website-level notification permission from the two available runtime sources. 372 * 373 * Prefers the LIVE `cachedNotificationPermission` global (set via postMessage from the 374 * main thread â most up-to-date) and falls back to IndexedDB when the postMessage 375 * hasn't arrived yet (`cachedNotificationPermission === null`, e.g. first push after SW 376 * restart before any tab sends an update). 377 * 378 * Reading `cachedNotificationPermission` live (not a captured snapshot) avoids a race 379 * where a postMessage arrives during an earlier `await` and a boolean snapshot would 380 * already be stale by the time this function is invoked. 381 *
382 * Note: this function only resolves the *website* permission. The caller is responsible 383 * for also AND-ing the result with the browser-level `Notification.permission`. 384 * 385 * A null persisted value is treated as UNKNOWN (never seeded or evicted), not denied, 386 * so an evicted IndexedDB after long inactivity no longer suppresses legitimate pushes. 387 * 388 * @param {string|null} persistedPermission - IndexedDB permission, or null when unknown/evicted 389 * 390 * @returns {boolean} True if the website permission is considered granted 391 */ 392function resolveWebsitePermission(persistedPermission) { 393 if (cachedNotificationPermission !== null) { return cachedNotificationPermission === NOTIFICATIONS_GRANTED; } 394 395 if (persistedPermission === null || persistedPermission === undefined) { return true; } 396 397 return persistedPermission === NOTIFICATIONS_GRANTED; 398} 399 400/** 401 * Syncs both the in-memory cache and IndexedDB to GRANTED when both permissions are confirmed. 402 * 403 * @param {boolean} shouldSync - Whether syncing is necessary (both granted but state was stale) 404 * @param {boolean} previousCachedState - Value of isCachedPermissionGranted before this call (for logging) 405 * @param {boolean} previousPersistedState - Value of dbPermission before this call (for logging) 406 * @returns {Promise<void>} 407 */ 408async function syncPermissionStates(shouldSync, previousCachedState, previousPersistedState) { 409 if (!shouldSync) { return; } 410 cachedNotificationPermission = NOTIFICATIONS_GRANTED; 411 try { 412 await savePermissionToDB(NOTIFICATIONS_GRANTED); 413 log('Both permissions granted - synced cached and persisted states.', { previousCachedState, previousPersistedState }); 414 } catch (error) { 415 log('Failed to sync persisted permission state', { error }, 'warn'); 416 } 417} 418 419// Initialize Firebase 420let messaging; 421let isFirebaseReady = false; 422 423try { 424 const firebaseApp = firebase.initializeApp({ 425 "apiKey": "AIzaSyCPqOjO6lCw33tbMcV3mKEIEd66L98TXZI", 426 "appId": "1:951535788597:web:e202f58aaf88833378c77c", 427 "authDomain": "figaro-gala.firebaseapp.com", 428 "databaseURL": null, 429 "measurementId": "G-Y2L88FMH61", 430 "messagingSenderId": 951535788597, 431 "projectId": "figaro-gala", 432 "storageBucket": "figaro-gala.appspot.com", 433 "vapidKey": "BGYKS1cusC2RcQabjpOG0CZXw-QtDyZ5GjJ_vIVyes6ouFsuvAxJ_77b7PN-Z1B6XcmAzepMpIS60uDr82CZlOE" 434}); 435 436 if (!firebaseApp) { 437 throw new Error('Firebase app initialization returned null'); 438 } 439 440 messaging = firebase.messaging(); 441 442 if (!messaging) { 443 throw new Error('Firebase messaging initialization returned null'); 444 } 445 446 isFirebaseReady = true; 447 log('Firebase initialized successfully', {}); 448} catch (error) { 449 log('Firebase initialization failed.', { error }, 'error'); 450} 451 452// Listen for permission updates from the main thread 453self.addEventListener('message', (event) => { 454 if (event.data && event.data.type === 'NOTIFICATION_PERMISSION_UPDATE') { 455 cachedNotificationPermission = event.data.permission; 456 457 event.waitUntil( 458 savePermissionToDB(cachedNotificationPermission) 459 .then(() => { 460 log('Received permission update from main thread.', { 461 permission: cachedNotificationPermission, 462 persistedToIndexedDB: true 463 }); 464 }) 465 .catch((error) => { 466 // In-memory state is still authoritative for the current session 467 log('Failed to persist permission to IndexedDB', { error, permission: cachedNotificationPermission }, 'error'); 468 }) 469 ); 470 } 471 472 if (event.data && event.data.type === 'DEVICE_ID_UPDATE') { 473 const { deviceId } = event.data; 474 event.waitUntil( 475 saveDeviceIdToDB(deviceId) 476 .then(() => { 477 log('Received device ID update from main thread.', { deviceId, persistedToIndexedDB: true }); 478 }) 479 .catch((error) => { 480 // In-memory state is still authoritative for the current session 481 log('Failed to persist device ID to IndexedDB', { error, deviceId }, 'error'); 482 }) 483 ); 484 } 485}); 486 487// Check if Firebase Messaging is supported 488if (isFirebaseReady && messaging) {
489 // Handle background messages using Firebase's onBackgroundMessage 490 messaging.onBackgroundMessage(async (payload) => { 491 // Gate display on the browser permission only (read directly by the SW). The former 492 // websitePermission layer suppressed real pushes when a subscribed user's opt-in was evicted. 493 // Notification is unavailable in this scope only in non-compliant environments â treat as not granted. 494 const browserPermission = self.Notification?.permission === NOTIFICATIONS_GRANTED; 495 496 // Persisted/cached website state kept for logging and self-healing only; it no longer gates display. 497 const persistedPermission = await getPermissionFromDB(); 498 const websitePermission = resolveWebsitePermission(persistedPermission); 499 500 /* 501 * Only self-heal towards GRANTED when the persisted state is truly unknown 502 * (never seeded, or evicted â persistedPermission === null/undefined). An explicit 503 * 'denied'/'default' reflects a real opt-out and must never be overwritten just 504 * because the browser-level permission still shows granted (e.g. during the narrow 505 * crash-recovery window where a stale subscription outlives an in-site unsubscribe). 506 */ 507 const isPersistedPermissionUnknown = 508 persistedPermission === null || persistedPermission === undefined; 509 await syncPermissionStates(browserPermission && isPersistedPermissionUnknown, cachedNotificationPermission === NOTIFICATIONS_GRANTED, persistedPermission === NOTIFICATIONS_GRANTED); 510 const hasPermission = browserPermission; 511 512 if (!hasPermission) { 513 // hasPermission === browserPermission here, so browserPermission is always false â 514 // the only possible reason for this branch is the browser-level permission itself. 515 log('Notification permission denied. Ignoring background message.', { 516 browserPermission, 517 browserState: self.Notification ? self.Notification.permission : 'unavailable', 518 cachedPermissionState: cachedNotificationPermission, 519 persistedPermissionState: persistedPermission, 520 websitePermission, 521 reason: 'browser permission not granted' 522 }, 'warn'); 523 524 // CHROME REQUIREMENT: Must show a notification when receiving push message 525 // Per Chrome docs: "You always have to show a notification when you receive a push message" 526 // If we don't return a promise that resolves to a shown notification, 527 // Chrome will display: "This site has been updated in the background" 528 // 529 // Solution: Show minimal silent notification that auto-dismisses 530 return showNotificationSafely(`Le site ${CURRENT_SITE} a été mis à jour en arrière-plan`, { 531 silent: true, 532 tag: 'background-sync', 533 requireInteraction: false 534 }, { reason: 'permission-denied-silent' }); 535 } 536 537 log('Notification permission granted. Processing background message.', { 538 browserPermission, 539 cachedPermissionState: cachedNotificationPermission, 540 persistedPermissionState: persistedPermission, 541 websitePermission, 542 permissionSource: cachedNotificationPermission !== null ? 'cached (in-memory)' : 'persisted (IndexedDB â cache not seeded yet)' 543 }); 544 545 log('Received background message.', { payload }); 546 547 // Extract data from payload - check both data and notification fields 548 try { 549 // Firebase Payload Structure 550 551 /** 552 * Firebase Cloud Messaging can have a data object that contain custom data fields 553 * `payload.data` - Custom fields you control 554 * 555 * @var {Object} payload - The notification payload 556 * @var {Object} payload.data - The notification payload data 557 * @var {string} payload.data.title - The notification title 558 * @var {string} payload.data.body - The notification body 559 * @var {string} payload.data.url - The notification destination URL 560 * @var {string} payload.data.image - The notification image 561 * @var {string} payload.data.topic - The notification topic 562 * @var {string} payload.data.type - The notification type 563 * @var {string} payload.data.environment - The notification environment 564 * @var {string} payload.data.overline - The notification overline 565 * @var {string} payload.data.push_id - Unique push identifier for deduplication (in-memory
565Set, TTL 60s) 566 */ 567 const data = payload.data || {}; 568 569 /** 570 * Firebase Cloud Messaging expects the payload to contain a notification object 571 * `payload.notification` - Firebase's built-in notification fields 572 * 573 * @var {Object} payload - The notification payload 574 * @var {Object} payload.notification.title - The notification title 575 * @var {string} payload.notification.body - The notification body 576 * @var {string} payload.notification.icon - The notification icon 577 * 578 */ 579 const notification = payload.notification || {}; 580 581 if (data.environment && data.environment !== SW_ENV) { 582 log('Environments are not aligned. Discarding message.', { expected: SW_ENV, received: data.environment }, 'error'); 583 584 // Wrong environment = configuration error, discard the notification content. 585 // CHROME REQUIREMENT: A bare `return` triggers the default "This site has been 586 // updated in the background" notification. Show a silent one instead. 587 return showNotificationSafely(`Le site ${CURRENT_SITE} a été mis à jour en arrière-plan`, { 588 silent: true, 589 tag: 'environment-mismatch', 590 requireInteraction: false 591 }, { reason: 'environment-mismatch', expected: SW_ENV, received: data.environment }); 592 } 593 594 // Deduplicate by push_id: if the backend retried a failed FCM delivery we would 595 // otherwise show the same notification twice. The Set is in-memory (survives SW 596 // lifetime, not page reloads) with a 60s TTL â enough to cover any retry window. 597 const { push_id: pushId } = data; 598 if (pushId) { 599 if (processedPushIds.has(pushId)) { 600 log('Duplicate push_id detected, skipping notification.', { pushId }, 'warn'); 601 return showNotificationSafely(`Le site ${CURRENT_SITE} a été mis à jour en arrière-plan`, { 602 silent: true, 603 tag: 'background-sync', 604 requireInteraction: false 605 }, { reason: 'duplicate-push-id', pushId }); 606 } 607 processedPushIds.add(pushId); 608 setTimeout(() => processedPushIds.delete(pushId), PUSH_ID_TTL_MS); 609 } 610 611 // LEGACY PAYLOAD: When payload.notification is present, the browser shows the notification 612 // automatically via the FCM compat SDK â this cannot be prevented. 613 // onBackgroundMessage is still called, but showing a second notification here would 614 // result in a duplicate. Return early; Chrome's "must show notification" requirement 615 // is already satisfied by the automatic one. 616 const hasNotificationField = Object.keys(notification).length > 0; 617 618 log('Processing notification payload.', { 619 hasDataField: Object.keys(data).length > 0, 620 hasNotificationField, 621 notificationFields: Object.keys(notification), 622 dataFields: Object.keys(data) 623 }); 624 625 if (hasNotificationField) { 626 log('Legacy payload: notification shown automatically by browser. Skipping manual display.', { 627 notificationFields: Object.keys(notification) 628 }); 629 return; 630 } 631 632 // Only data-only payloads reach this point (legacy notification payloads returned early above). 633 // Use custom data fields first, fall back to notification fields as a safety net. 634 const title = data.overline || data.title || notification.title || 'Notification'; 635 const body = data.overline ? (data.title || notification.body) : (data.body || notification.body || null); 636 637 const notificationTitle = stripTags(title); 638 const notificationOptions = removeEmptyValues({ 639 // Use push_id as tag when available: it uniquely identifies this exact push, 640 // so distinct articles on the same topic each get their own notification slot, 641 // and the same push can never stack if somehow delivered twice. 642 // Falls back to url -> topic for older payloads without push_id. 643 tag: pushId || data.url || data.topic || 'push-notification', 644 badge: data.badge || '/notifications-badge.png', 645 body: body ? stripTags(body) : null, 646 data: JSON.stringify(data), 647 icon: data.image || data.icon || notification.icon || '/notifications-icon.png', 648 image: data.image || notification.image || null, 649 }); 650 651 log('Showing notification.', { notificationTitle, notificationOptions }); 652 return showNotificationSafely(notificationTitle, notificationOptions, { source: 'background-message' }); 653 } catch (error) { 654 log('Error processing background message', { error, payload }, 'error'); 655 656 // CRITICAL: Show a silent notification to prevent Chrome's default notification 657 // Even if processing fails, we must show SOMETHING or Chrome shows "This site has been updated in the background"
658 return showNotificationSafely(`Le site ${CURRENT_SITE} a été mis à jour en arrière-plan`, { 659 silent: true, 660 tag: 'error-fallback', 661 requireInteraction: false 662 }, { reason: 'error-fallback', error: error.message }).catch((fallbackError) => { 663 log('Even fallback notification failed!', { fallbackError, originalError: error }, 'error'); 664 // Last resort: show minimal notification 665 return self.registration.showNotification(`Le site ${CURRENT_SITE} a été mis à jour en arrière-plan`, { 666 silent: true 667 }); 668 }); 669 } 670 }); 671}
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.