PageSourceSearch

https://easybook.ai/firebase-messaging-sw.js

js easybook.ai collected 2026-10-05 14:39:16 UTC 6,244 bytes, 201 lines download raw bytes

1/**
2 * Firebase Cloud Messaging Service Worker
3 * Handles background push notifications
4 */
5
6// Import Firebase scripts for service worker
7importScripts("https://www.gstatic.com/firebasejs/10.12.0/firebase-app-compat.js");
8importScripts("https://www.gstatic.com/firebasejs/10.12.0/firebase-messaging-compat.js");
9
10// Initialize Firebase in the service worker
11// Note: These values must match your Firebase config
12firebase.initializeApp({
13  apiKey: "AIzaSyCXtGKZhjCk6kUtw7F9-sxZUUH_yxEbyf4",
14  authDomain: "easybook-ai.firebaseapp.com",
15  projectId: "easybook-ai",
16  storageBucket: "easybook-ai.firebasestorage.app",
17  messagingSenderId: "915929748056",
18  appId: "1:915929748056:web:c10ffc6c90f42c7efef02d",
19});
20
21const messaging = firebase.messaging();
22
23const PUSH_CACHE = "easybook-push";
24const PUSH_STRINGS_URL = "/push-strings.json";
25const PENDING_MFA_APPROVAL_URL = "/pending-mfa-approval.json";
26const NEUTRAL_APPROVAL_LABELS = { approve: "\u2713", deny: "\u2715" };
27
28function readPushStrings() {
29  return caches
30    .open(PUSH_CACHE)
31    .then((cache) => cache.match(PUSH_STRINGS_URL))
32    .then((response) => (response ? response.json() : {}))
33    .catch(() => ({}));
34}
35
36function titleOf(payload, strings) {
37  return payload.notification?.title || payload.data?.title || strings.defaultTitle || "EasyBook";
38}
39
40function bodyOf(payload) {
41  return payload.notification?.body || payload.data?.body || "";
42}
43
44function payloadActions(payload) {
45  try {
46    const actions = JSON.parse(payload.data?.actions || "[]");
47    return Array.isArray(actions) ? actions : [];
48  } catch {
49    return [];
50  }
51}
52
53function approvalLabels(strings) {
54  if (strings.approve && strings.deny) return { approve: strings.approve, deny: strings.deny };
55  return NEUTRAL_APPROVAL_LABELS;
56}
57
58function stashPendingApproval(data) {
59  const entry = {
60    approvalCode: data.approvalCode,
61    requestIp: data.requestIp,
62    expiresAt: data.expiresAt,
63    location: data.location,
64    userAgent: data.userAgent,
65  };
66  return caches
67    .open(PUSH_CACHE)
68    .then((cache) =>
69      cache.put(
70        PENDING_MFA_APPROVAL_URL,
71        new Response(JSON.stringify(entry), { headers: { "Content-Type": "application/json" } })
72      )
73    )
74    .catch(() => undefined);
75}
76
77messaging.onBackgroundMessage((payload) =>
78  readPushStrings().then((strings) => {
79    if (payload.data?.type === "MFA_APPROVAL") {
80      const approvalCode = payload.data.approvalCode;
81      const labels = approvalLabels(strings);
82      return self.registration.showNotification(titleOf(payload, strings), {
83        body: bodyOf(payload),
84        icon: "/icons/notification-icon.png",
85        badge: "/icons/badge-icon.png",
86        tag: `mfa-approval-${approvalCode}`,
87        requireInteraction: true,
88        data: {
89          type: "MFA_APPROVAL",
90          approvalCode,
91          requestIp: payload.data.requestIp,
92          expiresAt: payload.data.expiresAt,
93          location: payload.data.location,
94          userAgent: payload.data.userAgent,
95          url: "/",
96        },
97        vibrate: [200, 100, 200, 100, 200],
98        actions: [
99          { action: "approve", title: labels.approve },
100          { action: "deny", title: labels.deny },
101        ],
102      });
103    }
104
105    return self.registration.showNotification(titleOf(payload, strings), {
106      body: bodyOf(payload),
107      icon: "/icons/notification-icon.png",
108      badge: "/icons/badge-icon.png",
109      tag: payload.data?.tag || "easybook-notification",
110      data: {
111        url: payload.data?.url || payload.data?.click_action || "/",
112        ...payload.data,
113      },
114      vibrate: [100, 50, 100],
115      actions: payloadActions(payload),
116    });
117  })
118);
119
120// Handle notification click
121self.addEventListener("notificationclick", (event) => {
122  const notificationData = event.notification.data || {};
123
124  event.notification.close();
125
126  // Handle MFA approval action clicks
127  if (notificationData.type === "MFA_APPROVAL") {
128    const approvalCode = notificationData.approvalCode;
129
130    // Open the app and trigger the MFA approval modal
131    // The app will handle showing the approval UI
132    event.waitUntil(
133      clients
134        .matchAll({
135          type: "window",
136          includeUncontrolled: true,
137        })
138        .then((clientList) => {
139          // Try to find and focus an existing window
140          for (const client of clientList) {
141            if (client.url.includes(self.location.origin) && "focus" in client) {
142              client.focus();
143              // Post message to trigger approval modal in the app
144              client.postMessage({
145                type: "MFA_APPROVAL_NOTIFICATION",
146                action: event.action || "open", // 'approve', 'deny', or 'open'
147                approvalCode,
148                requestIp: notificationData.requestIp,
149                expiresAt: notificationData.expiresAt,
150                location: notificationData.location,
151                userAgent: notificationData.userAgent,
152              });
153              return;
154            }
155          }
156          if (clients.openWindow) {
157            return stashPendingApproval(notificationData).then(() => clients.openWindow("/"));
158          }
159        })
160    );
161    return;
162  }
163
164  // Get the URL to open for normal notifications
165  const urlToOpen = notificationData.url || "/";
166
167  // Handle action clicks for normal notifications
168  if (event.action) {
169    const actionUrl = notificationData[`action_${event.action}_url`];
170    if (actionUrl) {
171      event.waitUntil(clients.openWindow(actionUrl));
172      return;
173    }
174  }
175
176  // Focus existing window or open new one
177  event.waitUntil(
178    clients
179      .matchAll({
180        type: "window",
181        includeUncontrolled: true,
182      })
183      .then((clientList) => {
184        // Try to focus an existing window
185        for (const client of clientList) {
186          if (client.url.includes(self.location.origin) && "focus" in client) {
187            client.focus();
188            // Navigate to the URL
189            if (urlToOpen !== "/") {
190              client.navigate(urlToOpen);
191            }
192            return;
193          }
194        }
195        // Open new window if none found
196        if (clients.openWindow) {
197          return clients.openWindow(urlToOpen);
198        }
199      })
200  );
201});

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.