[React] Passing Environment Variables to Service Workers

yutakusuno

Yuta Kusuno

Posted on January 18, 2024

[React] Passing Environment Variables to Service Workers

When working with Firebase Cloud Messaging in a React application, creating a service worker (the following firebase-messaging-sw.js) in the public directory becomes essential. However, handling environment-specific configurations, such as API keys, poses a challenge due to the inability to access environment variables using process.env within the public directory. This blog post explores two approaches to dynamically pass environment variables to the service worker.

// public/firebase-messaging-sw.js

// Give the service worker access to Firebase Messaging.
// Note that you can only use Firebase Messaging here. Other Firebase libraries are not available in the service worker.
importScripts('https://www.gstatic.com/firebasejs/8.10.1/firebase-app.js');
importScripts('https://www.gstatic.com/firebasejs/8.10.1/firebase-messaging.js');

// Initialize the Firebase app in the service worker by passing in your app's Firebase config object.
// https://firebase.google.com/docs/web/setup#config-object
firebase.initializeApp({
  apiKey: 'api-key',
  authDomain: 'project-id.firebaseapp.com',
  databaseURL: 'https://project-id.firebaseio.com',
  projectId: 'project-id',
  storageBucket: 'project-id.appspot.com',
  messagingSenderId: 'sender-id',
  appId: 'app-id',
  measurementId: 'G-measurement-id',
});

// Retrieve an instance of Firebase Messaging so that it can handle background messages.
const messaging = firebase.messaging();
Enter fullscreen mode Exit fullscreen mode

Using a Separate Script to Build Environment Variables

Create a .env file with your Firebase configurations.

REACT_APP_FIREBASE_API_KEY = 'api-key'
REACT_APP_FIREBASE_AUTH_DOMAIN = 'project-id.firebaseapp.com',
REACT_APP_FIREBASE_PROJECT_ID = 'project-id'
REACT_APP_FIREBASE_STORAGE_BUCKET = 'project-id.appspot.com'
REACT_APP_FIREBASE_MESSAGING_SENDER_ID = 'sender-id'
REACT_APP_FIREBASE_APP_ID = 'app-id'
REACT_APP_FIREBASE_MEASUREMENT_ID = 'G-measurement-id'
Enter fullscreen mode Exit fullscreen mode

Create a script (swEnvBuild.js) to build a JavaScript file (swEnv.js) containing the environment variables.

// src/swEnvBuild.js

const fs = require("fs");

// This dotenv is the one used inside React.
// Load dotenv Intentionally because build process does not have access to .env file yet.
const dotenv = require("dotenv");
dotenv.config();

const {
  REACT_APP_FIREBASE_API_KEY,
  REACT_APP_FIREBASE_AUTH_DOMAIN,
  REACT_APP_FIREBASE_PROJECT_ID,
  REACT_APP_FIREBASE_STORAGE_BUCKET,
  REACT_APP_FIREBASE_MESSAGING_SENDER_ID,
  REACT_APP_FIREBASE_APP_ID,
  REACT_APP_FIREBASE_MEASUREMENT_ID,
} = process.env;

const content = `const swEnv = {
    REACT_APP_FIREBASE_API_KEY: '${REACT_APP_FIREBASE_API_KEY}',
    REACT_APP_FIREBASE_AUTH_DOMAIN: '${REACT_APP_FIREBASE_AUTH_DOMAIN}',
    REACT_APP_FIREBASE_PROJECT_ID: '${REACT_APP_FIREBASE_PROJECT_ID}',
    REACT_APP_FIREBASE_STORAGE_BUCKET: '${REACT_APP_FIREBASE_STORAGE_BUCKET}',
    REACT_APP_FIREBASE_MESSAGING_SENDER_ID: '${REACT_APP_FIREBASE_MESSAGING_SENDER_ID}',
    REACT_APP_FIREBASE_APP_ID: '${REACT_APP_FIREBASE_APP_ID}',
    REACT_APP_FIREBASE_MEASUREMENT_ID: '${REACT_APP_FIREBASE_MEASUREMENT_ID}'
 }`;

fs.writeFileSync("./public/swEnv.js", content);
Enter fullscreen mode Exit fullscreen mode

Modify the npm start command in package.json to run the build script before starting the development server. Only the npm start command is being modified for testing in the development environment. When deploying, I think it is necessary to modify the npm build command.

"scripts": {
- "start": "react-scripts start",
+ "start": "node src/swEnvBuild.js && react-scripts start",
Enter fullscreen mode Exit fullscreen mode

Now, running npm run start will create public/swEnv.js.

// public/swEnv.js

const swEnv = {
  REACT_APP_FIREBASE_API_KEY: 'api-key',
  REACT_APP_FIREBASE_AUTH_DOMAIN: 'project-id.firebaseapp.com',
  REACT_APP_FIREBASE_PROJECT_ID: 'project-id',
  REACT_APP_FIREBASE_STORAGE_BUCKET: 'project-id.appspot.com',
  REACT_APP_FIREBASE_MESSAGING_SENDER_ID: 'sender-id',
  REACT_APP_FIREBASE_APP_ID: 'app-id',
  REACT_APP_FIREBASE_MEASUREMENT_ID: 'G-measurement-id'
}
Enter fullscreen mode Exit fullscreen mode

Update public/firebase-messaging-sw.js to import and use the dynamically generated environment variables.

// public/firebase-messaging-sw.js

importScripts("https://www.gstatic.com/firebasejs/8.3.1/firebase-app.js");
importScripts("https://www.gstatic.com/firebasejs/8.3.1/firebase-messaging.js");
importScripts("swEnv.js"); // Added

firebase.initializeApp({
  apiKey: swEnv.REACT_APP_FIREBASE_API_KEY, // Changed... repeat for other variables
  authDomain: swEnv.REACT_APP_FIREBASE_AUTH_DOMAIN,
  projectId: swEnv.REACT_APP_FIREBASE_PROJECT_ID,
  storageBucket: swEnv.REACT_APP_FIREBASE_STORAGE_BUCKET,
  messagingSenderId: swEnv.REACT_APP_FIREBASE_MESSAGING_SENDER_ID,
  appId: swEnv.REACT_APP_FIREBASE_APP_ID,
  measurementId: swEnv.REACT_APP_FIREBASE_MEASUREMENT_ID,
});

const messaging = firebase.messaging();
Enter fullscreen mode Exit fullscreen mode

Using Query Parameters

Call registerServiceWorker in src/App.jsx to dynamically construct the service worker script URL with query parameters.

export const registerServiceWorker = async () => {
  let scriptURL = "firebase-messaging-sw.js";
  scriptURL += `?apiKey=${process.env.REACT_APP_FIREBASE_API_KEY}`;
  scriptURL += `&authDomain=${process.env.REACT_APP_FIREBASE_AUTH_DOMAIN}`;
  scriptURL += `&projectId=${process.env.REACT_APP_FIREBASE_PROJECT_ID}`;
  scriptURL += `&storageBucket=${process.env.REACT_APP_FIREBASE_STORAGE_BUCKET}`;
  scriptURL += `&messagingSenderId=${process.env.REACT_APP_FIREBASE_MESSAGING_SENDER_ID}`;
  scriptURL += `&appId=${process.env.REACT_APP_FIREBASE_APP_ID}`;
  scriptURL += `&measurementId=${process.env.REACT_APP_FIREBASE_MEASUREMENT_ID}`;

  if ("serviceWorker" in navigator) {
    try {
      const registration = await navigator.serviceWorker.register(scriptURL);
      if (registration.installing) {
        console.log("Service worker installing");
      } else if (registration.waiting) {
        console.log("Service worker installed");
      } else if (registration.active) {
        console.log("Service worker active");
      }
    } catch (error) {
      console.error(`Registration failed with ${error}`);
    }
  }
};
Enter fullscreen mode Exit fullscreen mode

Update public/firebase-messaging-sw.js to extract query parameters and initialize Firebase.

// public/firebase-messaging-sw.js

importScripts("https://www.gstatic.com/firebasejs/8.3.1/firebase-app.js");
importScripts("https://www.gstatic.com/firebasejs/8.3.1/firebase-messaging.js");

const params = new URL(location).searchParams;
const apiKey = params.get("apiKey");
const authDomain = params.get("authDomain");
const projectId = params.get("projectId");
const storageBucket = params.get("storageBucket");
const messagingSenderId = params.get("messagingSenderId");
const appId = params.get("appId");
const measurementId = params.get("measurementId");

firebase.initializeApp({
  apiKey: apiKey,
  authDomain: authDomain,
  projectId: projectId,
  storageBucket: storageBucket,
  messagingSenderId: messagingSenderId,
  appId: appId,
  measurementId: measurementId,
});

const messaging = firebase.messaging();
Enter fullscreen mode Exit fullscreen mode

Both approaches provide solutions for passing environment variables to service workers in a React application. Choose the approach that best fits your project requirements and development workflow. Additionally, alternative methods, such as using Webpack and Babel or workbox-webpack-plugin with InjectManifest are available. I will try these as well when I get a chance.

That is about it. Happy coding!

💖 💪 🙅 🚩
yutakusuno
Yuta Kusuno

Posted on January 18, 2024

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related