Thursday, 1 May 2025

Implementing Mobile Push Alerts for PWA Using Azure Notification Hub and FCM

 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

  1. Azure Notification Hub: Set up an Azure Notification Hub and configure it with FCM credentials through Azure Portal.
  2. 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
  3. 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!

Saturday, 26 April 2025

Power your Web/PWA app with Instant Messaging & Alerts

 Adding a real-time messaging & alert system to your app is the key and utmost requirement in today’s world.

This article outlines an end-to-end architecture. We’ll illustrate with Azure services, but you can easily adapt the approach using your preferred open-source or cloud alternatives.

Use Case: We need to implement a real-time alert delivery mechanism for our PWA web application (running on both browser and mobile platforms). This system must ensure that all alerts generated by various backend systems are reliably and instantly delivered to users without any data loss.

Solution Design

To ensure no alerts are missed, we can indeed break down the problem into these two key areas:

  1. Real-time Delivery to Online Users (Web/Mobile): The first challenge is to guarantee immediate delivery of alerts to users who are currently active and connected to the web or mobile application.
  2. Reliable Delivery to Offline Users (Web/Mobile): The second critical aspect is ensuring alert delivery to users who are not currently active or connected to the web or mobile application.

To address the requirement of real-time delivery to online users, we’ll establish active WebSocket connections. These connections will enable immediate pushing of messages to the application, which can then be displayed through notification modules like toast messages or bell alerts to effectively capture user attention upon arrival.
To achieve this real-time message pushing via WebSockets, we will utilize the Azure Web PubSub service. You can use your preferred open-source or cloud alternatives.

To handle message delivery for offline users, we’ll implement a persistence mechanism. Alerts will be stored in a database, allowing the application to retrieve and display them when the user comes back online (pull mechanism). For mobile users specifically, we will also leverage Azure Notification Hub in conjunction with Firebase Cloud Messaging (FCM) to directly push the alerts to their devices.

To guarantee no alert loss due to technical issues, we’ll implement a message queue service (like Azure Service Bus or an equivalent). An Azure Function will subscribe to this queue, ensuring reliable processing of each message. This separate function for message handling enhances the service’s scalability and avoids dependencies on our API services.

Here is the complete architecture diagram for the solution.

The left side of the diagram shows various alert producers (including potentially our .NET API). To guarantee delivery, all generated alerts are first routed to Azure Service Bus. From there, upon message receipt, a process is initiated to ensure:
1. Persistence in the database for offline user access upon their return online.
2. Real-time delivery to online users via Azure Web PubSub.
3. Delivery as mobile push notifications through Azure Notification Hub.

To deliver messages to users when they come online, the alerts saved in the database will be served via your .NET Core API. This pull mechanism is employed by the web application only upon its initial load. Subsequently, all real-time message delivery is guaranteed through the Azure Web PubSub service. Because the messages are stored persistently in the database, we can implement features like read/unread status and user-initiated message deletion.

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!

Friday, 18 April 2025

Go Mobile: Convert Your Vue App with PWA for Mobile Brilliance in no time.

 Considering today’s fast-paced environment where time equates to money, the same principle applies to your application. If you possess a well-developed web application built with technologies like Vue, Angular, or React and are looking to expand into the mobile space, you might encounter significant hurdles related to budget, effort, required skills, and time constraints. This is where Progressive Web Apps (PWAs) offer a compelling solution.

What is PWA?

A progressive web app (PWA) is an app that’s built using web platform technologies, but that provides a user experience like that of a platform-specific app.

Similar to a traditional website, a PWA boasts the advantage of running across various platforms and devices using a single codebase. However, akin to native apps, PWAs can be installed on devices such as Android, iOS, and Windows smartphones. Furthermore, they possess the capabilities to function offline, operate in the background, and seamlessly integrate with the device’s features and other installed applications.

Numerous leading websites and companies have already embraced PWA technology, including Microsoft, Twitter (with Twitter Lite), Pinterest, Instagram (their web app), Uber, Starbucks, AliExpress, Trivago, and many others.

Opting for a PWA not only provides a swift pathway to establishing a mobile presence but also accelerates the development process and yields significant cost savings by eliminating the need for a separate mobile development budget.

Alright, let’s explore the steps involved in transforming your Vue app bundles for mobile using Webpack.

Keep in mind that while the following example focuses on a Vue application, the underlying principles and configurations are generally applicable to React and Angular projects as well, often requiring only minor adjustments or even remaining the same.

Steps 1: We need to define a Service Worker file.

A service worker acts as middleware between your PWA and the servers it interacts with. When an app requests a resource covered by the service worker’s scope, the service worker intercepts the request and acts as a network proxy, even if the user is offline. It can then decide if it should serve the resource from the cache using the Cache Storage API, serve it from the network as if there were no active service worker, or create it from a local algorithm.

Service worker file has to be in the public folder (to make the content available to dist (output) folder without bundling it.

Create a file i.r public/service-worker.js and add below code:

//Update the version whenever the below file changes.
const CACHE_NAME = 'my_cache_v1.0.2';
const urlsToCache = ['/', '/index.html', '/icon192x192.png', '/icon512x512.png', '/manifest.json',];
const ignoredHosts = ['localhost'];

self.addEventListener('install', (event) => {
event.waitUntil(
caches.open(CACHE_NAME).then((cache) => {
return cache.addAll(urlsToCache);
})
);
console.log('[Service Worker] Installing new version...');
self.skipWaiting(); // Activate immediately
});


// Fetch event - Serve cached assets when offline
self.addEventListener('fetch', (event) => {
const { hostname } = new URL(event.request.url);
if (ignoredHosts.indexOf(hostname) >= 0) {
return;
}
event.respondWith(
caches.match(event.request).then((response) => {
return response || fetch(event.request);
})
);
});

// Activate event - Clean up old caches
self.addEventListener('activate', (event) => {
console.log('[Service Worker] Activating new version...');
event.waitUntil(
Promise.all([
clearOldCaches(),
self.clients.claim(), // Take control of open tabs immediately
])
);
});

//delete old caches
async function clearOldCaches() {
const cacheNames = await caches.keys();
const oldCaches = cacheNames.filter((name) => name !== CACHE_NAME);
return Promise.all(oldCaches.map((name) => caches.delete(name)));
}

self.addEventListener('message', (event) => {
if (event.data && event.data.type === 'CLEAR_CACHE') {
caches.keys().then((cacheNames) => {
cacheNames.forEach((cacheName) => caches.delete(cacheName));
});
self.skipWaiting(); // Ensure new service worker activates immediately
}
});

In the service worker file we add above needed events to manage all the network request within its scope. The above code is standard and can be used as it is.

In case if you need to customised the install experience with a popup or something, you can use the “beforeinstallprompt” event to do so but please note that, this event is not supported by some browsers like safari.

// This variable will save the event for later use.
let deferredPrompt;
window.addEventListener('beforeinstallprompt', (e) => {
// Prevents the default mini-infobar or install dialog from appearing on mobile
e.preventDefault();
// Save the event because you'll need to trigger it later.
deferredPrompt = e;
// Show your customized install prompt for your PWA
// Your own UI doesn't have to be a single element, you
// can have buttons in different locations, or wait to prompt
// as part of a critical journey.
showInAppInstallPromotion();
});

Step 2: Register the service-worker when your application starts and to register it add below code to your main.ts (main.js) file.

//register service worker only for mobile devices
if ('serviceWorker' in navigator) {
window.addEventListener("load", () => {
navigator.serviceWorker
.getRegistration()
.then((registration) => {
if (!registration) {
navigator.serviceWorker
.register('/service-worker.js', { updateViaCache: 'none' })
.then((registration) => {
registration.addEventListener('updatefound', () => {
const newWorker = registration.installing;
newWorker.onstatechange = () => {
if (
newWorker.state === 'installed' &&
navigator.serviceWorker.controller
) {
console.log(
'New version available. Reloading service worker...'
);
window.location.reload();
}
};
});
});
console.log(
'Service Worker registered successfully:',
registration
);
} else {
console.log('Service Worker already registered:', registration);
}
})
.catch((error) => {
console.error('Service Worker registration failed:', error);
});

navigator.serviceWorker.ready.then((registration) => {
registration.active.postMessage({ type: 'CLEAR_CACHE' });
});
});
}

With this, service worker installation will happen silently and the service worker APIs will be available for PWA. Once the installation done, the ‘activate’ event will be fired and triggered the event code in service-worker.js file to activate the service worker which also cleans the old cache based on cache key (if you change the cache key with next push of code).

Note: Avoid using window’s load event listener to register the service worker if you don’t want to delay the service work registration until your application load.

Step 3: We need to create a manifest file to tell the browser, how we want our web content to display as an app in the mobile devices.

The manifest file includes basic information such as the app’s name, icon, and theme color; advanced preferences, such as desired orientation and app shortcuts; and catalog metadata, such as screenshots.

Hence create a manifest.json file as in public/manifest.json and add in below code:

{
"name": "My PWA APP",
"short_name": "PWA",
"start_url":".",
"display": "standalone",
"description": "My first PWA APP",
"lang": "en",
"dir": "auto",
"theme_color": "#ffffff",
"background_color": "#ffffff",
"orientation": "any",
"icons": [
{
"src": "/icon512x512.png",
"sizes": "512x512",
"type": "image/png",
"purpose": "maskable"
},
{
"src": "/icon192x192.png",
"sizes": "192x192",
"type": "image/png",
"purpose": "maskable"
}
],
"prefer_related_applications": false,
"shortcuts": [
{
"name":"My PWA APP",
"short_name": "PWA",
"url": ".",
"description":"My PWA APP"
}
]
}

Step 4: Create web.config in the public folder (if your application is hosted by a server i.e. Azure App service or server to host your app) as in public/web.config

In the web.config we define the .json mimetype to handle it as static content which is very much required for manifest.json file along with no cache custom headers and all. You can use the below code as it is for your web.config.

<?xml version="1.0"?>

<configuration>
<system.webServer>
<httpProtocol>
<customHeaders>
<add name="X-Frame-Options" value="sameorigin" />
<add name="Strict-Transport-Security" value="max-age=63072000; includeSubDomains; preload" />
<add name="Cache-Control" value="no-cache, no-store, must-revalidate" />
<add name="Pragma" value="no-cache" />
<add name="Expires" value="-1" />
</customHeaders>
</httpProtocol>
<staticContent>
<!-- Disable caching for index.html -->
<clientCache cacheControlMode="DisableCache" />
<mimeMap fileExtension=".json" mimeType="application/json" />
</staticContent>
<rewrite>
<rules>
<rule name="Handle History Mode and custom 404/500" stopProcessing="true">
<match url="(.*)" />
<conditions logicalGrouping="MatchAll">
<add input="{REQUEST_FILENAME}" matchType="IsFile" negate="true" />
<add input="{REQUEST_FILENAME}" matchType="IsDirectory" negate="true" />
</conditions>
<action type="Rewrite" url="index.html" />
</rule>
</rules>
</rewrite>
</system.webServer>
</configuration>

Step 5: Add the below link tags to your template.html or index.html (whichever you have as start html page).

 <link rel="manifest" href="/manifest.json" />
<!-- Enable PWA Standalone Mode on iOS -->
<meta name="mobile-web-app-capable" content="yes" />
<!-- Customize the Status Bar Appearance -->
<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent" />

To customize the images or splash screen for devices specific like iphone, you can add all the images here as link items. i.e.

<link rel="apple-touch-startup-image" media="screen and (device-width: 440px) and (device-height: 956px) and (-webkit-device-pixel-ratio: 3) and (orientation: landscape)" href="splash_screens/iPhone_16_Pro_Max_landscape.png">

Step 6: Make sure all the content from public folder is included in the dist (output folder) with your javascript bundler, add copy plugin in the webpack.config.js. In my case, it is webpack and the code is:

new CopyWebpackPlugin({
patterns: [
{ from: path.resolve(__dirname, 'public'), to: path.resolve(__dirname, 'dist/') }, // copies everything from public/ to dist/
],
}),

to make above code work. you need to install the “copy-webpack-plugin” package using the npm command as “npm install copy-webpack-plugin”.

Step 7: In the final step, make sure all the images you defined in your manifest file, available in the public folder.

Next, build your application and deploy. When you browse the application in your mobile device, you will get a popup automatically to install the application.

Bonus Step: In iPhone, the install popup won’t show as it is not supported by safari browser hence to overcome this in you can show a popup message using below code to navigate user to the install button (square-arrow-up icon, which usually comes in center of down the screen).

        const isIos = () => {
const userAgent = window.navigator.userAgent.toLowerCase();
return /iphone|ipad|ipod/.test(userAgent);
};

// Detect if device is in standalone mode
const isInStandaloneMode = () => 'standalone' in window.navigator && window.navigator.standalone;

// Show install message if on iOS and not in standalone mode
if (isIos() && !isInStandaloneMode()) {
//show popup here.
}

That’s the end of the article and your app on mobile.

To verify locally if the service worker is getting registered, Open the debugger tool of the browser then go to “Application” section from the top menu and click on “Service workers” from left side Application menu, it will display your registered service worker.

To know more about PWA and setup, read here: https://web.dev/learn/pwa/progressive-web-apps

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!