Search code examples
reactjsfirebasefirebase-cloud-messagingprogressive-web-appsservice-worker

How to register two different service-workers in the same scope?


I have a service-worker.js file to make my reactjs app PWA. I now also want to add push notification using FCM, which requires me to have firebase-messaging-sw.js in the public folder. So now for both to work both are going to require to be in the same scope.

But as far as I have read from various answers on this site, we can't have two different service workers in the same scope, so how do we combine both service-worker.js and firebase-messaging-sw.js so both can function properly. One of the answers suggested that I rename the service-worker.js to firebase-messaging-sw.js which doesn't work. I did find one successful implementation on GitHub which I didn't understand much https://github.com/ERS-HCL/reactjs-pwa-firebase .

How can I have both the service-worker.js and firebase-messaging-sw.js work together?

firebase-messaging-sw.js

// Scripts for firebase and firebase messaging
importScripts("https://www.gstatic.com/firebasejs/8.2.0/firebase-app.js");
importScripts("https://www.gstatic.com/firebasejs/8.2.0/firebase-messaging.js");

 // Initialize the Firebase app in the service worker by passing the generated config
 const firebaseConfig = {
    apiKey: "xxxx",
    authDomain: "xxxx.firebaseapp.com",
    projectId: "xxxx",
    storageBucket: "xxxx",
    messagingSenderId: "xxxx",
    appId: "xxxx",
    measurementId: "xxxx"
}

 firebase.initializeApp(firebaseConfig);

 // Retrieve firebase messaging

const messaging = firebase.messaging();


self.addEventListener("notificationclick", function (event) {
  console.debug('SW notification click event', event)

  const url = event.notification.data.link
  event.waitUntil(
    clients.matchAll({type: 'window'}).then( windowClients => {
        // Check if there is already a window/tab open with the target URL
        for (var i = 0; i < windowClients.length; i++) {
            var client = windowClients[i];
            // If so, just focus it.
            if (client.url === url && 'focus' in client) {
                return client.focus();
            }
        }
        // If not, then open the target URL in a new window/tab.
        if (clients.openWindow) {
            return clients.openWindow(url);
        }
    })
);
})


messaging.onBackgroundMessage(async function(payload) {
  console.log("Received background message ", payload)

  const notificationTitle = payload.notification.title
  const notificationOptions = {
    body: payload.notification.body,
    icon: './logo192.png',
    badge: './notification-badgex24.png',
    data: {
        link: payload.data?.link
    }
  }

  self.registration.showNotification(notificationTitle, notificationOptions)

})

service-worker.js

import { clientsClaim } from 'workbox-core';
import { ExpirationPlugin } from 'workbox-expiration';
import { precacheAndRoute, createHandlerBoundToURL } from 'workbox-precaching';
import { registerRoute } from 'workbox-routing';
import { StaleWhileRevalidate } from 'workbox-strategies';

clientsClaim();

const fileExtensionRegexp = new RegExp('/[^/?]+\\.[^/]+$');
registerRoute(
  // Return false to exempt requests from being fulfilled by index.html.
  ({ request, url }) => {
    // If this isn't a navigation, skip.
    if (request.mode !== 'navigate') {
      return false;
    } // If this is a URL that starts with /_, skip.

    if (url.pathname.startsWith('/_')) {
      return false;
    } // If this looks like a URL for a resource, because it contains // a file extension, skip.

    if (url.pathname.match(fileExtensionRegexp)) {
      return false;
    } // Return true to signal that we want to use the handler.

    return true;
  },
  createHandlerBoundToURL(process.env.PUBLIC_URL + '/index.html')
);

registerRoute(
  // Add in any other file extensions or routing criteria as needed.
  ({ url }) => url.origin === self.location.origin && url.pathname.endsWith('.png'), // Customize this strategy as needed, e.g., by changing to CacheFirst.
  new StaleWhileRevalidate({
    cacheName: 'images',
    plugins: [
      // Ensure that once this runtime cache reaches a maximum size the
      // least-recently used images are removed.
      new ExpirationPlugin({ maxEntries: 50 }),
    ],
  })
);

self.addEventListener('message', (event) => {
  if (event.data && event.data.type === 'SKIP_WAITING') {
    self.skipWaiting();
  }
});


Solution

  • Ok, after spending weeks, I figured it out. So for anyone else, using the service worker created using the default create react app, follow the steps below.

    first create firebase-messaging-sw.js in public folder and put the same code as in the question, but this time also add importScripts("/service-worker.js").

    // Scripts for firebase and firebase messaging
    importScripts("https://www.gstatic.com/firebasejs/8.2.0/firebase-app.js")
    importScripts("https://www.gstatic.com/firebasejs/8.2.0/firebase-messaging.js")
    importScripts(`/service-worker.js`) // <- here notice this
    .
    .
    .
    

    The import will import the service-worker.js after the build step. The service worker you have in the src folder is only a template.

    You cannot use importScript in the service-worker.js file that's in the src folder as that will throw importScripts is not defined error.

    Your build folder after your build step:

    └── build/
        ├── static/
        │   ├── .
        │   ├── .
        │   └── .
        ├── index.html
        ├── firebase-messaging-sw.js
        ├── service-worker.js
        ├── .
        └── .
    

    Now in index.html add

     <script>
        if ('serviceWorker' in navigator){
            navigator.serviceWorker.register('/firebase-messaging-sw.js')
            .then(reg => console.debug("Service worker registered sucessfully"))
        }
      
      </script>
    

    And that's it. Both firebase-messaging and your PWA service worker will work


    or you can also

    You can create a new file called sw.js in your public folder and use imporScript() to import both firebase-messaging-sw.js and default service-worker.js

    make sure to pass in service worker registration in getToken of the firebase

     const registration = await navigator.serviceWorker.register('/sw.js')
            
     const currentToken = await getToken(messaging, { 
                                    serviceWorkerRegistration: registration,
                                    vapidKey: '<VAPID_KEY>' 
                                })
    

    now simply register your new service worker in index file as shown