1/* 2 Service Worker + Firebase Cloud Messaging (FCM) 3 4This Service Worker integrates FCM with **custom push notification handling**. 5It uses **data payloads** (instead of notification payloads) to ensure that the 6`notificationclick` event is always triggered, giving full control over how 7notifications are displayed and how clicks are handled. 8 9Why use `data` instead of `notification` payloads? 10-------------------------------------------------- 11 Problem with `notification` payload: 12 - FCM automatically displays the notification via the browser. 13 - The `notificationclick` event is NOT fired. 14 - Result: You cannot control tab/window routing or customize behavior. 15 16 Solution â Use only `data` payload: 17 - The Service Worker manually shows the notification. 18 - Ensures `notificationclick` is always triggered. 19 - Provides complete control over display, routing, and user interaction. 20*/ 21 22/* Push Notification Behavior 23 24Push Notification Behavior (Cross-Browser Validated) 25 261. Foreground (App Tab Open) 27 - Notification displayed. 28 - Clicking opens a new tab with the target URL. 29 302. Background / Minimized 31 - Chrome/Firefox/Edge: Notification displayed. 32 - Clicking opens a new tab. 33 343. Closed Browser 35 - Chrome/Firefox/Edge: No notification shown while browser is closed. 36 Once reopened, the Service Worker replays the pending FCM push, and the 37 notification is displayed. 38 - Safari: Push is received even without manually reopening the browser. 39 404. Multi-Tab Scenario 41 - Notification displayed. 42 - Clicking always opens a **new tab**, leaving other tabs unaffected. 43 445. Safari (macOS & iPad) 45 - Requires **user interaction (e.g., button click)** to register Service Worker 46 and push subscription. Registration cannot happen automatically during 47 lifecycle hooks. 48 - Notification displayed. 49 - Clicking opens a **new window** (not a tab). 50 516. Incognito / Private Browsing 52 - FCM push is never delivered. 53 - User sees no notification. 54 557. Permission Denied (User Blocked Notifications) 56 - FCM push is still delivered to the Service Worker. 57 - Notification is **not displayed** (silent failure). 58 59Summary 60 - Core flows (foreground, background, closed, multi-tab) work consistently 61 in Chrome, Firefox, Edge, and Safari. 62 - Safari differs: 63 ⢠Push subscription requires explicit **user gesture** for registration. 64 ⢠Always opens in a new **window** instead of a tab. 65 ⢠Can deliver push even while browser is closed. 66- Limitations: 67 ⢠No push in Incognito/Private mode. 68 ⢠Blocked permissions â push delivered but not shown. 69*/ 70 71/* 72 * Notification Click Handling â Features & Decisions 73 * 74 * 1. Delay Fix (350ms) :- Chrome bug with notification clicks 75 In some versions of Chrome, when you immediately call clients.openWindow(targetUrl) after notificationclick, the browser sometimes opens a half-loaded or blank page. 76 This happens because Chrome is still in the process of dismissing the notification UI and handling focus. 77 Adding a short delay (350ms) 78 The setTimeout gives Chrome enough time to âsettleâ after closing the notification. 79 With the delay, the openWindow call works more reliably, ensuring the new tab loads properly. 80 81 2. Always Open a New Tab/Window 82 Behavior: Does not reuse existing tabs. 83 - Fallback: If no URL is provided, "/" is used. 84 85 for (const c of clientList) { 86 if (c.url === targetUrl && "focus" in c) { 87 return c.focus(); 88 } 89 } 90 91 This code tries to reuse an existing tab instead of opening a new one 92 by focusing it if the tabâs URL matches the targetUrl. 93 - Problem with Angular/React (SPA): 94 In single-page apps, the browser only knows the base URL (like https://app.com/). 95 Routes like /dashboard or /profile are handled inside the app. Because of this, 96 focusing a tab does not make the app change route to the targetUrl. 97 - Extra case: 98 99 If a tab already has the targetUrl open and the user refreshed it, then on a 100 push click the service worker will only focus that tab. It may look like a redirect happened, but the app actually stays on the same page after the refresh. 101 102 Project Decision:- 103 Always open a new tab/window with targetUrl for guaranteed navigation. 104 Do not attempt to reuse/focus existing tabs. 105 */ 106 107// Import Firebase scripts for App & Messaging (compat version for SW support) 108importScripts(
109 "https://www.gstatic.com/firebasejs/12.0.0/firebase-app-compat.js" 110); 111importScripts( 112 "https://www.gstatic.com/firebasejs/12.0.0/firebase-messaging-compat.js" 113); 114 115// --- Service Worker Lifecycle Events --- 116 117// Install â Activates SW immediately without waiting 118self.addEventListener("install", function (event) { 119 self.skipWaiting(); // Ensures the new SW takes control immediately, without waiting for old SW to stop. 120}); 121 122// Activate â Takes control of all clients immediately 123self.addEventListener("activate", function (event) { 124 // Claims all open clients (tabs) so the SW can control them instantly. 125 event.waitUntil(self.clients.claim()); 126}); 127 128// --- Push Event Listener --- 129// Triggered when a push notification arrives from FCM 130self.addEventListener("push", function (event) { 131 if (event.data) { 132 try { 133 let dt = event.data.json(); 134 event.waitUntil( 135 self.registration 136 .showNotification(dt.data.title, { 137 body: dt.data.body, 138 icon: dt.data.image, 139 tag: dt.fcmMessageId, // Unique tag â avoids duplicate stacking 140 data: { 141 click_action: dt.data.link, // URL to open on click 142 messageId: dt.fcmMessageId, // prevent duplicate notifications 143 customData: dt.data, 144 }, 145 }) 146 .catch((error) => { 147 throw new Error("Error showing notification: " + error); 148 }) 149 ); 150 } catch (error) { 151 throw new Error("Error parsing push data: " + error); 152 } 153 } 154}); 155 156// --- Notification Click Listener --- 157// Handles routing when user clicks a notification 158self.addEventListener("notificationclick", (event) => { 159 event.notification.close(); 160 // Default route if none provided 161 const targetUrl = event.notification.data?.click_action || "/"; 162 163 event.waitUntil( 164 (async () => { 165 // --- Chrome workaround: small delay prevents half-loaded tabs --- 166 await new Promise((resolve) => setTimeout(resolve, 350)); 167 168 // Always open a new tab/window 169 if (clients.openWindow) { 170 return clients.openWindow(targetUrl); 171 } 172 })() 173 ); 174}); 175 176// --- Message Event Listener --- 177// Used for inter-tab communication via service worker 178self.addEventListener("message", (event) => { 179 if (event.data.type === "tabs") { 180 const senderId = event.source.id; 181 self.clients 182 .matchAll({ 183 includeUncontrolled: true, 184 type: "window", 185 }) 186 .then((clients) => { 187 if (clients && clients.length) { 188 const clientCount = clients.length; // count open tabs 189 // Notify all other tabs (except sender) 190 clients.forEach((cl) => { 191 if (cl.id !== senderId) { 192 cl.postMessage({ 193 type: "tabs", 194 clientCount, 195 }); 196 } 197 }); 198 } 199 }); 200 } 201 // add Firebase config dynamically 202 if (event.data && event.data.type === "FIREBASE_CONFIG") { 203 try { 204 if (!firebase.apps.length) { 205 firebase.initializeApp(event.data.config); 206 firebase.messaging(); 207 } 208 } catch (error) { 209 throw new Error("Failed to initialize Firebase: " + error); 210 } 211 } 212}); 213 214// Test Environments 215// Cross-browser validation completed on Linux, Windows, macOS, and iPad (Safari PWA). 216// Safari behaves differently â always opens in a new window instead of a tab. Also, Push received without opening the browser if the browser is closed. 217// Linux (Ubuntu 24.04.3 LTS) 218// Chrome 139.0.7258.154 (64-bit) 219 220// Firefox 142.0.1 (64-bit) 221 222// Windows (Windows 11 Pro, 24H2) 223// Chrome 139.0.7258.139 (64-bit) 224 225// Firefox 142.0.1 (64-bit) 226 227// Edge 139.0.3405.125 (64-bit) 228 229// Mac (macOS 15.6.1) 230// Safari 18.6 (20621.3.11.11.3) 231 232// Chrome 139.0.7258.155 (arm64) 233 234// iPad (iPadOS 18.6) 235// Safari (PWA) 18.6 236// Chrome - Not Applicable
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.