Progressive Web Apps (PWAs) are revolutionizing the way we deliver app-like experiences on the web. One of the key features of PWAs is the ability to send push notifications, keeping users engaged even when the app is not actively open. In this blog, I’ll walk you through implementing mobile push alerts for a PWA using Azure Notification Hub and Firebase Cloud Messaging (FCM).
Why Azure Notification Hub and FCM?
Azure Notification Hub provides a scalable and reliable way to send push notifications to multiple platforms, including web, iOS, and Android. FCM acts as the intermediary for delivering notifications to web clients, making it an ideal choice for PWAs.
Prerequisites
- Azure Notification Hub: Set up an Azure Notification Hub and configure it with FCM credentials through Azure Portal.
- Firebase Project: Create a Firebase project and obtain the configuration details (API key, project ID, etc.) from the url https://console.firebase.google.com/
Read more about FCM: https://firebase.google.com/docs/cloud-messaging - PWA Setup: Ensure your PWA is configured with a service worker.
Step 1: Setting Up Firebase in the PWA
In the main.ts file, initialize Firebase with your project configuration:
import firebase from 'firebase/compat/app';
import 'firebase/compat/messaging';
const firebaseConfig = {
apiKey: '<YOUR_FCM_API_KEY>',
authDomain: '<YOUR_PROJECT_ID>.firebaseapp.com',
projectId: '<YOUR_PROJECT_ID>',
messagingSenderId: '<YOUR_SENDER_ID>',
appId: '<YOUR_APP_ID>',
};firebase.initializeApp(firebaseConfig);
Register the Firebase service worker to handle background notifications:
if ('serviceWorker' in navigator) {
navigator.serviceWorker
.register('/firebase-messaging-sw.js', { scope: '/firebase-cloud-messaging-push-scope' })
.then((registration) => {
console.log('Firebase Service Worker registered:', registration);
registration.active?.postMessage({ type: 'INIT_FIREBASE', config: firebaseConfig });
})
.catch((error) => console.error('Service Worker registration failed:', error));
}
Step 2: Creating the Service Worker
Create a file name as “firebase-messaging-sw.js” in the public folder. The firebase-messaging-sw.js file handles incoming push notifications and displays them to the user:
importScripts('https://www.gstatic.com/firebasejs/9.6.1/firebase-app-compat.js');
importScripts('https://www.gstatic.com/firebasejs/9.6.1/firebase-messaging-compat.js');
firebase.initializeApp({
apiKey: '<YOUR_FCM_API_KEY>',
projectId: '<YOUR_PROJECT_ID>',
messagingSenderId: '<YOUR_SENDER_ID>',
appId: '<YOUR_APP_ID>',
});const messaging = firebase.messaging();messaging.onBackgroundMessage((payload) => {
console.log('Received background message:', payload);
const notificationTitle = payload.notification.title;
const notificationOptions = {
body: payload.notification.body,
icon: '/dc_icon192x192.png',
}; self.registration.showNotification(notificationTitle, notificationOptions);
});
Step 3: Requesting Notification Permissions
Add a NotificationService.ts in the src folder or you can add the code directly in app.vue. In the NotificationService.ts file, request notification permissions and retrieve the FCM token:
import { messaging, getToken } from './firebase';
export const requestNotificationPermission = async () => {
const permission = await Notification.requestPermission();
if (permission === 'granted') {
const token = await getToken(messaging, { vapidKey: '<YOUR_VAPID_KEY>' });
console.log('Notification Token:', token);
return token;
} else {
console.warn('Notification permission denied');
return null;
}
};
Step 4: Listening for Messages
Also in NotificationService.ts, listen for incoming messages while the app is in the foreground:
import { onMessage } from './firebase';
export const listenForMessages = () => {
onMessage(messaging, (payload) => {
console.log('Message received:', payload);
alert(`New notification: ${payload.notification.title}`);
});
};
Step 4: Initiate the notification service call from app.vue
Call requestNotificationPermission and listenForMessages from the app.vue created event. It has to be called once app is started.
import { requestNotificationPermission, listenForMessages } from './NotificationService';
export default defineComponent({
async created() {
this.enableNotifications();
listenForMessages();
},
methods:{
async enableNotifications() {
let token;
try {
token = await requestNotificationPermission();
const payload = {
"RegistrationId": null,
"PushChannel": this.token,
"Platform": "FcmV1",
"PushVariables": {}
};
if (token) {
const result = await api.user.deviceRegistration(payload);
this.registration = {
payload: JSON.stringify(payload),
token: this.token,
result: JSON.stringify(result)
};
} else {
console.error('Notification permission denied or token not available.');
}
}
catch (error) {
console.error('Error enabling notifications:', error);
}
},
}
})
Step 5: Integrating with Azure Notification Hub
Once the FCM token is retrieved, register it with Azure Notification Hub. This step involves sending the token to your backend, which then registers it with Azure Notification Hub. In the above code with Step 4, the line “const result = await api.user.deviceRegistration(payload);” does the same (you need to replace this code based on your backend implementation). In my case the backend server is .Net core API and the code is:
using Microsoft.Azure.NotificationHubs;
using Microsoft.Azure.NotificationHubs.Messaging;
using Newtonsoft.Json.Linq;
namespace Gda.Wdp.Awarehouse.NotificationService
{
public class NotificationHubsService : INotificationHubsService
{
const StringComparison OICase = StringComparison.OrdinalIgnoreCase;
public INotificationHubClient NotificationHubClient { get; set; }
public NotificationHubsService()
{
NotificationHubClient = new NotificationHubClient("Your NotificationHubConnectionString", "your NotificationHubName");
} public async Task<NotificationOutcome> SendNotification(NotificationPlatform platform,string title, string message, List<string> tag, Dictionary<string, string> additionalData = null)
{
string MpnsNotificationContent = $@"";
string WindowsNotificationContent = $@"";
string AppleNotificationContent = $@"";
string FcmNotificationContent = $@"";
string FcmV1NotificationContent = $@"{{ ""message"": {{ ""notification"": {{ ""body"" : ""{message}"", ""title"" : ""{title}""}} }} }}";
string AdmNotificationContent = $@"";
string BaiduNotificationContent = $@"";
var msgJObj = JObject.Parse(FcmV1NotificationContent);
if (additionalData != null)
{
var data = JObject.FromObject(additionalData);
msgJObj["message"]["data"] = data;
}
Notification notification = default;
switch (platform)
{
case NotificationPlatform.FcmV1:
notification = new FcmV1Notification(msgJObj.ToString());
break;
default:
throw new Exception($"{platform.ToString()} not supported with DC Notification.");
}
var outcome = await NotificationHubClient.SendNotificationAsync(notification, tag);
return outcome;
} public async Task<string> CreateOrUpdateRegistrationAsync(DeviceRegistration device,string userEmailId)
{
RegistrationDescription registration = null;
string registrationId = default;
if (!string.IsNullOrWhiteSpace(device.RegistrationId))
{
registration = await NotificationHubClient.GetRegistrationAsync<RegistrationDescription>(device.RegistrationId);
registrationId = registration?.RegistrationId;
}
if (registration == null)
{
var registrationList = await NotificationHubClient.GetRegistrationsByChannelAsync(device.PushChannel, 1);
registrationId = registrationList.FirstOrDefault()?.RegistrationId;
}
switch (device.Platform)
{
case NotificationPlatform.Mpns:
registration = new MpnsRegistrationDescription(device.PushChannel);
break;
case NotificationPlatform.Wns:
registration = new WindowsRegistrationDescription(device.PushChannel);
break;
case NotificationPlatform.Apns:
registration = new AppleRegistrationDescription(device.PushChannel);
break;
case NotificationPlatform.FcmV1:
registration = new FcmV1RegistrationDescription(device.PushChannel);
break;
case NotificationPlatform.Fcm:
registration = new FcmRegistrationDescription(device.PushChannel);
break;
case NotificationPlatform.Adm:
registration = new AdmRegistrationDescription(device.PushChannel);
break;
case NotificationPlatform.Baidu:
registration = new BaiduRegistrationDescription(device.PushChannel);
break;
default:
throw new Exception($"{device.Platform.ToString()} not supported with DC Notification.");
} if (registrationId == null)
registrationId = await NotificationHubClient.CreateRegistrationIdAsync(); if (device.Tags.Count == 0 || !device.Tags.Any(t => t.Equals(userEmailId, OICase)))
device.Tags.Add(userEmailId);
device.Tags.Add($"{device.Platform.ToString()}"); registration.RegistrationId = registrationId;
registration.PushVariables = device.PushVariables;
registration.Tags = new HashSet<string>(device.Tags); var registrationDescription=await NotificationHubClient.CreateOrUpdateRegistrationAsync(registration);
return registrationDescription.RegistrationId;
}
public async Task DeleteRegistrationsAsync(string pushChannel,string userEmailId)
{
if(!string.IsNullOrWhiteSpace(pushChannel))
await NotificationHubClient.DeleteRegistrationsByChannelAsync(pushChannel);
else if(!string.IsNullOrWhiteSpace(userEmailId))
{
var regList = await GetAllRegistrationsAsync(userEmailId, null);
foreach (var reg in regList)
{
await NotificationHubClient.DeleteRegistrationsByChannelAsync(reg.PnsHandle);
}
}
}
public async Task<List<RegistrationDescription>> GetAllRegistrationsAsync(string userEmailId, string pushChannel = null)
{
ICollectionQueryResult<RegistrationDescription> allRegistrations = null;
string continuationToken = null;
if (userEmailId == null && pushChannel == null)
allRegistrations = await NotificationHubClient.GetAllRegistrationsAsync(0);
else
allRegistrations = string.IsNullOrWhiteSpace(pushChannel) ? await NotificationHubClient.GetRegistrationsByTagAsync(userEmailId, 100) : await NotificationHubClient.GetRegistrationsByChannelAsync(pushChannel, 100); continuationToken = allRegistrations.ContinuationToken;
var registrationDescriptionsList = new List<RegistrationDescription>(allRegistrations);
while (!string.IsNullOrWhiteSpace(continuationToken))
{
var otherRegistrations = await NotificationHubClient.GetAllRegistrationsAsync(continuationToken, 0);
registrationDescriptionsList.AddRange(otherRegistrations);
continuationToken = otherRegistrations.ContinuationToken;
}
return registrationDescriptionsList;
} private async Task<NotificationDetails> WaitForThePushStatusAsync(string pnsType, INotificationHubClient nhClient, NotificationOutcome notificationOutcome)
{
var notificationId = notificationOutcome.NotificationId;
var state = NotificationOutcomeState.Enqueued;
var count = 0;
NotificationDetails outcomeDetails = null;
while ((state == NotificationOutcomeState.Enqueued || state == NotificationOutcomeState.Processing) && ++count < 10)
{
try
{
Console.WriteLine($"{pnsType} status: {state}");
outcomeDetails = await nhClient.GetNotificationOutcomeDetailsAsync(notificationId);
state = outcomeDetails.State;
}
catch (MessagingEntityNotFoundException)
{
// It's possible for the notification to not yet be enqueued, so we may have to swallow an exception
// until it's ready to give us a new state.
}
Thread.Sleep(1000);
}
return outcomeDetails;
} private NotificationHubSettings GetSettings()
{
var settings = new NotificationHubSettings();
settings.RetryOptions = new NotificationHubRetryOptions() { MaxRetries = 2 };
return settings;
}
}
public class DeviceRegistration
{
public DeviceRegistration()
{
Tags = new List<string>();
PushVariables = new Dictionary<string, string>();
}
public string? RegistrationId { get; set; }
public string PushChannel { get; set; }
public NotificationPlatform Platform { get; set; }
public List<string> Tags { get; set; }
public IDictionary<string, string> PushVariables { get; set; }
}
public class SendNotificationRequest
{
public SendNotificationRequest()
{
Platform = NotificationPlatform.FcmV1;
SendToTag = new List<string>();
}
public List<string> SendToTag { get; set; }
public string Message { get; set; }
public string Title { get; set; }
public NotificationPlatform? Platform { get; } //set; }
public Dictionary<string,string>? AdditionalData { get; set; }
}
}
The backend code above includes methods for device registration and sending notifications, if needed. You can implement this feature in your own backend as well.
Conclusion
With these steps, you can implement mobile push alerts for your PWA using Azure Notification Hub and FCM. This setup ensures that your users stay informed and engaged, even when they’re not actively using your app.
That wraps up this article! I hope you found the information valuable. If you’re interested in staying updated with similar content, don’t forget to follow. Thanks for reading, and happy coding!