I have built a React progressive web application that makes use of service workers.
The service worker gets registered and is activated:
I have been trying to detect the "activate" event using this:
service-worker.js
navigator.serviceWorker.addEventListener("activate", function (event) {
console.log("service worker activated");
});
I added that at the end of the service-worker file. But, this event never gets triggered and I have no idea why.
I also tried to implement push notifications and trigger the from the backend. For this, I needed a "push" event listener that would listen to these events from the server:
navigator.serviceWorker.addEventListener("push", async function (event) {
const message = await event.data.json();
let { title, description, image } = message;
await event.waitUntil(showPushNotification(title, description, image));
});
This is how showPushNotification is defined:
export function showPushNotification(title, description, image) {
if (!("serviceWorker" in navigator)) {
console.log("Service Worker is not supported in this browser");
return;
}
navigator.serviceWorker.ready.then(function (registration) {
registration.showNotification(title, {
body: description,
icon: image,
actions: [
{
title: "Say hi",
action: "Say hi",
},
],
});
});
}
I tested calling that function manually and it successfully triggerss a push notification.
This is the server code that triggers the push notification:
const sendPushNotification = async (user_id, title, description, image) => {
const search_option = { user: user_id };
const users_subscriptions = await PushNotificationSubscription.find(
search_option
);
const number_of_users_subscriptions = users_subscriptions.length;
const options = {
vapidDetails: {
subject: "mailto:xxxx#xxxx.com",
publicKey: VAPID_PUBLIC_KEY,
privateKey: VAPID_PRIVATE_KEY,
},
};
let push_notif_sending_results = {};
for (let i = 0; i < number_of_users_subscriptions; i++) {
const user_subscription = users_subscriptions[i];
await webPush
.sendNotification(
user_subscription,
JSON.stringify({
title,
description,
image,
}),
options
)
.then((notif_send_result) => {
push_notif_sending_results[i] = { success: notif_send_result };
})
.catch((error) => {
push_notif_sending_results[i] = { error: error };
});
}
return push_notif_sending_results;
};
This is the part responsible for sending the push notification:
webPush
.sendNotification(
user_subscription,
JSON.stringify({
title,
description,
image,
}),
options
)
And it's successfully executed as it returns a 201 HTTP response.
So the "push" event listener is supposed to detect it and trigger a push notification.
I think everything regarding the push notification has been successfully implementing and the problem is how the "push" event listener is added since the "activate" event listener also doesn't work.
So I tried moving the two event listeners here right after the registration of the service worker is successful:
function registerValidSW(swUrl, config) {
navigator.serviceWorker.register(swUrl).then((registration) => {
registration.addEventListener("activate", (event) => {
console.log(
"🚀 ~ file: serviceWorker.js:159 ~ navigator.serviceWorker.register ~ event",
event
);
});
registration.addEventListener("push", async function (event) {
const message = await event.data.json();
let { title, description, image } = message;
await event.waitUntil(
showPushNotification(title, description, image)
);
});
});
}
But, it's still the same result.
Neither the "push" nor the "activate" event listeners get triggered.
Any idea what's going on?
Here's the whole service-worker file:
service-worker.js
import axios from "axios";
const isLocalhost = Boolean(
window.location.hostname === "localhost" ||
// [::1] is the IPv6 localhost address.
window.location.hostname === "[::1]" ||
// 127.0.0.0/8 are considered localhost for IPv4.
window.location.hostname.match(
/^127(?:\.(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)){3}$/
)
);
export function register(config) {
if (process.env.NODE_ENV === "production" && "serviceWorker" in navigator) {
// The URL constructor is available in all browsers that support SW.
const publicUrl = new URL(process.env.PUBLIC_URL, window.location.href);
if (publicUrl.origin !== window.location.origin) {
// Our service worker won't work if PUBLIC_URL is on a different origin
// from what our page is served on. This might happen if a CDN is used to
// serve assets;
return;
}
window.addEventListener("load", () => {
const swUrl = `${process.env.PUBLIC_URL}/service-worker.js`;
if (isLocalhost) {
// This is running on localhost. Let's check if a service worker still exists or not.
checkValidServiceWorker(swUrl, config);
// Add some additional logging to localhost, pointing developers to the
// service worker/PWA documentation.
} else {
// Is not localhost. Just register service worker
console.log(
"Is not localhost. Just register a service worker, by calling registerValidSW"
);
registerValidSW(swUrl, config);
}
});
}
}
async function subscribeToPushNotifications(serviceWorkerReg) {
let subscription = await serviceWorkerReg.pushManager.getSubscription();
if (subscription === null) {
const dev_public_vapid_key = "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx";
const prod_public_vapid_key =
"xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx";
const public_vapid_key = isLocalhost
? dev_public_vapid_key
: prod_public_vapid_key;
subscription = await serviceWorkerReg.pushManager.subscribe({
userVisibleOnly: true,
applicationServerKey: public_vapid_key,
});
axios
.post("/api/push_notif_subscription/subscribe", subscription)
.then((response) => {})
.catch((error) => {});
}
}
export function showPushNotification(title, description, image) {
if (!("serviceWorker" in navigator)) {
console.log("Service Worker is not supported in this browser");
return;
}
navigator.serviceWorker.ready.then(function (registration) {
registration.showNotification(title, {
body: description,
icon: image,
actions: [
{
title: "Say hi",
action: "Say hi",
},
],
});
});
}
function registerValidSW(swUrl, config) {
navigator.serviceWorker.register(swUrl).then((registration) => {
subscribeToPushNotifications(registration);
registration.onupdatefound = () => {
const installingWorker = registration.installing;
if (!installingWorker) {
return;
}
installingWorker.onstatechange = () => {
if (installingWorker.state === "installed") {
if (navigator.serviceWorker.controller) {
// At this point, the updated precached content has been fetched,
// but the previous service worker will still serve the older
// content until all client tabs are closed.
// Execute callback
if (config && config.onUpdate) {
config.onUpdate(registration);
}
} else {
// At this point, everything has been preached.
// It's the perfect time to display a
// "Content is cached for offline use." message.
// Execute callback
if (config && config.onSuccess) {
config.onSuccess(registration);
}
}
}
};
};
});
}
function checkValidServiceWorker(swUrl, config) {
// Check if the service worker can be found. If it can't reload the page.
fetch(swUrl, {
headers: { "Service-Worker": "script" },
}).then((response) => {
// Ensure the service worker exists, and that we really are getting a JS file.
const contentType = response.headers.get("content-type");
if (
response.status === 404 ||
(!!contentType && contentType.indexOf("javascript") === -1)
) {
// No service worker found. Probably a different app. Reload the page.
navigator.serviceWorker.ready.then((registration) => {
registration.unregister().then(() => {
window.location.reload();
});
});
} else {
// Service worker found. Proceed as normal.
console.log("Service worker found, calling registerValidSW");
registerValidSW(swUrl, config);
}
});
}
export function unregister() {
if ("serviceWorker" in navigator) {
navigator.serviceWorker.ready.then((registration) => {
registration.unregister();
});
}
}
navigator.serviceWorker.addEventListener("activate", function (event) {
console.log("service worker activated");
});
navigator.serviceWorker.addEventListener("push", async function (event) {
const message = await event.data.json();
let { title, description, image } = message;
await event.waitUntil(showPushNotification(title, description, image));
});
The events "push" and "activate" are part of the ServiceWorkerGlobalScope as within the Service Worker API.
Push notifications must be handled within the service worker itself.
Therefore only the service worker can register an "activate" event listener.
The same applies for a "push" listener.
Specially in terms of the "push" listener this makes sense.
The idea of push events is to receive them, even if the main app (in this case the website) has been closed.
The service worker is an exception, as it even runs without the page being loaded.
Therefore move the "push" event into your service worker.
Your code (within the service worker) may look like this:
this.addEventListener("push", async function (event) {
const message = await event.data.json();
let { title, description, image } = message;
await event.waitUntil(showPushNotification(title, description, image));
});
function showPushNotification(title, description, image) {
registration.showNotification(title, {
body: description,
icon: image,
actions: [
{
title: "Say hi",
action: "Say hi",
},
],
});
}
The rest seems fine to me.
Update (Some more explanation)
I took a more careful look at your service-worker.js and it seems it contains general methods for registering the service worker.
As mentioned above the main app and the service worker are two completely separate chunks of code, running in different spaces. So this means everything which is not supposed to run in the service worker itself must be put outside of the service-worker.js. The service worker (in your case) should only contain the code for handling push notifications. It's important that you do not include the "service-worker.js" within your application.
In your case, you may seperate these functions into service-worker-register.js which contain all functions which are for managing the service worker registration but should not be executed within the service worker itself (isLocalhost, register, subscribeToPushNotifications, registerValidSW, checkValidServiceWorker, and unregister). Please note the code snippet from above and make changes accordingly to the code left within the service worker.
MDN has a pretty in depth tutorial on service workers (and there are a lot more) I recommend having a look at:
developer.mozilla.org/en-US/docs/Web/API/Service_Worker_API/Using_Service_Workers
I'm trying to send notifications based on business logic that runs (on nodejs) on my server via a cron.
Issue
Notifications aren't appearing on the device.
Description
I'm using the firebase admin node package.
My code looks something like this
import admin from "firebase-admin";
import serviceAccount from "../../firebase-admin.json" assert { type: 'json' };
import { getMessaging } from 'firebase-admin/messaging';
admin.initializeApp({
credential: admin.credential.cert(serviceAccount)
});
...
console.log(message);
await getMessaging().send(message)
.then((response) => {
// Response is a message ID string.
console.log('Successfully sent message:', response);
})
.catch((error) => {
console.log('Error sending message:', error);
});
My log output is something like this
{
notification: {
title: 'This is a string',
body: 'This is another string'
},
token: 'aLphaNumeric:reallyLongAlphaNumericWithDashesAndUnderscores'
}
Successfully sent message: projects/<project-name>/messages/<id>
Everything I'm seeing suggests this should be sent!
sendMulticast and the Admin FCM APIs allow you to multicast a message to a list of device registration tokens. You can specify up to 500 device registration tokens per invocation.
sendMulticast take 2 arguments as input, 1st one is notification which contains the title and body of the message.
The other argument is fcmTokens with type array, so you must pass that argument as array even though there is only one fcmToken
//Import the file where you have imported the service file.
const adminApp = require("../firebase/firebaseConfig");
const notificationToAll = (title, body, tokens) => {
var notibody = {
notification: {
title: title,
body: body,
},
tokens: tokens,
};
return new Promise((resolve, reject) => {
adminApp
.messaging()
.sendMulticast(notibody)
.then((response) => {
console.log(response.responses);
if (response.responses[0].error != undefined) {
console.log(JSON.stringify(response.responses[0].error));
}
resolve(response);
})
.catch((error) => {
console.log(JSON.stringify(error));
reject(error);
});
});
};
module.exports = notificationToAll;
app.js
const notificationToAll = require("./helper/notification");
notificationToAll(
"This is a string",
`This is another string`,
["aLphaNumeric:reallyLongAlphaNumericWithDashesAndUnderscores"]
)
This is tested code and working in a live environment.
I was wondering if it was possible to use FCM in chrome extension Manifest version 3? And by calling the getToken function in the background.js script.
I tried to use it but I keep getting the following error:
FirebaseError: Messaging: We are unable to register the default service worker.
Even though the firebase-messaging-sw.js is in the extension root. And I see on the Chrome Extension API it has a gcm entry but not a fcm entry.
I did find a github repo where fcm worked with the V3 but in that repo they opened a new window and used the service worker registration function in there and the getToken function. That won't work for my case because I need the getToken function to be called in the background.js without user interaction.
Any suggestions?
Attached the modified background.js code:
import { initializeApp } from "./firebase-app.js";
import { getMessaging, onBackgroundMessage } from './firebase-messaging-sw.js';
import { getToken, onMessage } from "./firebase-messaging.js";
import { firebaseConfig, vapidKey } from './firebaseConfig.js';
// Initialize Firebase
const firebaseApp = initializeApp(firebaseConfig);
const messaging = getMessaging(firebaseApp);
onBackgroundMessage(messaging, (payload) => {
console.log('[background.js] Received background message ', payload);
self.registration.showNotification(payload.notification.title, {
body: payload.notification.body,
});
});
chrome.runtime.onInstalled.addListener(() => {
console.log('installed');
});
getToken(messaging, {
vapidKey,
}).then((currentToken) => {
if (currentToken) {
// Send the token to your server to send push notifications.
registrationTokenElement.textContent = currentToken;
console.log(currentToken);
} else {
// Show permission request UI
console.log('No registration token available. Request permission to generate one.');
// ...
}
}).catch((err) => {
console.log('An error occurred while retrieving token. ', err);
// ...
});
async function openPopupWindow() {
console.log('open popup');
const { popupWindowId } = await chrome.storage.local.get('popupWindowId');
if (popupWindowId) {
try {
await chrome.windows.update(popupWindowId, { focused: true });
return;
} catch (e) {
// ignore
}
}
const popup = await chrome.windows.create({
url: 'popup.html',
type: 'popup',
width: 300,
height: 500,
});
await chrome.storage.local.set({
popupWindowId: popup.id,
});
}
chrome.action.onClicked.addListener(() => {
console.log('click icon');
openPopupWindow();
});
self.onnotificationclick = function(event) {
console.log('On notification click: ', event.notification.tag);
event.notification.close();
event.waitUntil(openPopupWindow());
};
chrome.windows.onRemoved.addListener(async (windowId) => {
const { popupWindowId } = await chrome.storage.local.get('popupWindowId');
if (popupWindowId === windowId) {
console.log('close popup');
await chrome.storage.local.remove('popupWindowId');
}
});
I'm sending push messages using FCM through Firebase Functions. The messages are being sent properly, but I'm getting the 408 time-out error after the message is sent. I'm suspecting it might have to do with the unregistered tokens not being cleaned up because:
if I were to send another message to the same device, the same timeout occurs and
the only error message I get from the Firebase log is Function execution took 60002 ms, finished with status: 'timeout'.
exports.sendMessage = functions.https.onRequest(async (request, response) => {
const {
sender,
recipient,
content,
docID
} = request.body
functions.logger.log(
"docID:",
docID,
);
// Get the list of device notification tokens.
let deviceTokens; let ref;
try {
ref = admin.firestore().collection("deviceToken").doc(recipient);
const doc = await ref.get();
if (!doc.exists) {
console.log("No such document!");
response.status(500).send(e)
} else {
console.log("doc.data():", doc.data());
deviceTokens = doc.data().token;
}
} catch (e) {
response.status(500).send(e)
}
let senderProfile;
try {
senderProfile = await admin.auth().getUser(sender);
console.log("senderProfile", senderProfile);
} catch (e) {
console.log(e);
response.status(500).send(e)
}
// Notification details.
let payload = {
notification: {
title: senderProfile.displayName,
body: content,
sound: "default",
},
data: {
uid: senderProfile.uid,
displayName: senderProfile.displayName,
docID,
messageType: "status"
}
};
functions.logger.log(
"deviceTokens", deviceTokens,
"payload", payload,
);
// Send notifications to all tokens.
const messageResponse = await admin.messaging().sendToDevice(deviceTokens, payload);
// For each message check if there was an error.
messageResponse.results.forEach((result, index) => {
const error = result.error;
if (error) {
functions.logger.error(
"Failure sending notification to",
deviceTokens[index],
error,
);
// Cleanup the tokens who are not registered anymore.
if (error.code === "messaging/invalid-registration-token" ||
error.code === "messaging/registration-token-not-registered") {
const updatedTokens = deviceTokens.filter((token) => token !== deviceTokens[index]);
console.log("updatedTokens", updatedTokens);
ref.update({
token: updatedTokens,
})
.catch(function(e) {
console.error("Error removing tokens", e);
response.status(500).send(e)
});
}
}
});
response.status(200)
});
I'm unsure why the following isn't cleaning up the unregistered tokens:
const updatedTokens = deviceTokens.filter((token) => token !== deviceTokens[index]);
ref.update({
token: updatedTokens,
})
You always have to end HTTP functions with response.status(200).send() or response.status(200).end(). In the above function, you have response.status(200) so you have to end it either with response.status(200).send() or response.status(200).end(). Please check the documentation if it helps.
I am setting up FCM for my React web push notification and it is doing everything properly, except I don't know how to refresh token when it's expired
I have an FCMListener function which its code is as follow
navigator.serviceWorker
.register("/static-files/firebase-messaging-sw.js")
.then((registration) => {
firebase.initializeApp(settings.getConfig().FIREBASE_CONFIG);
const messaging = firebase.messaging();
messaging.useServiceWorker(registration);
try {
messaging
.requestPermission()
.then(() => {
return messaging.getToken();
})
.then((token) => {
let topic = `${userInfo.is_host ? "host" : "guest"}`;
if (token) {
this.subscribeToTopic(topic, token);
this.sendTokenToServer({
os: "web",
push_token: token,
});
} else {
messaging.onTokenRefresh(() => {
messaging
.getToken()
.then((refreshedToken) => {
this.subscribeToTopic(topic, token);
this.sendTokenToServer({
os: "web",
push_token: refreshedToken,
});
})
.catch((err) => {
console.log("Unable to retrieve refreshed token ", err);
});
});
}
});
} catch (error) {
if (error.code === "messaging/permission-blocked") {
console.log("Please Unblock Notification Request Manually");
} else {
console.log("Error Occurred", error);
}
}
messaging.onMessage((payload) => {
console.log("Notification Received", payload);
alert(payload.notification.body);
});
});
};
}
as I don't know how to expire a firebase token and I can't test what happens when the token is expired I don't know if the part where I am trying to get refreshedToken is right or not or even if this is the proper way to get refreshed token or not. I would really appreciate any hint and advise
For testing purposes you can delete the FCM token through the API by calling the deleteToken API. After doing that, reload the page, and your onTokenRefresh should fire.