PageSourceSearch

https://volunteers.operationbbqrelief.org/firebase-messaging-sw.js

js operationbbqrelief.org collected 2026-10-03 19:48:00 UTC 8,300 bytes, 236 lines download raw bytes

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.