Hello! In this article, we will explore how to send push notifications to iOS users, even if your app is temporarily unavailable in the App Store. With the release of Safari 16.4, the ability to receive notifications in Progressive Web Apps (PWA) has become available.
Sure, let's tackle this task from a frontend developer's perspective.
What We'll Need
- Server-side: For the server logic, we'll choose Node.js.
- Client-side: We will be using React.js for creating the user interface.
- Push service: We'll use Google Cloud Messaging as the service for sending push notifications.
Creating a Server on Express.js
Let's start by generating VAPID keys. To understand why they are needed, let's briefly go over the theory. VAPID is essentially a unique ID for your application in the world of web push notifications. These keys help the browser understand where the notification is coming from and provide an additional layer of security. So, we'll have a pair of keys: public and private.
Now onto the practice! We will use a Node.js library called web-push
. This library works well with Google Cloud Messaging, Google's system for sending notifications.
Install the library using npm:
npm install web-push -g
Generate the keys
web-push generate-vapid-keys
We've generated two keys: a public key and a private key. The public key will be used on the client side when users are subscribing to notifications. This key will allow the browser to identify the source of notifications to make sure they are coming from a trusted server.
The private key will be stored on our server. It's required for signing the data we send and for verifying the authenticity of our application with notification sending systems like Google Cloud Messaging.
We'll create two URLs: one for saving notification subscriptions and another for sending them.
// Import the web-push library for working with push notifications
const webPush = require('web-push');
webPush.setVapidDetails(
publicKey,
privateKey
);
// Initialize an object to store subscriptions
let subscriptions = {}
// Route for subscribing to push notifications
app.post('/subscribe', (req, res) => {
// Extract subscription and ID from the request
const {subscription, id} = req.body;
// Store the subscription in the object under the key ID
subscriptions[id] = subscription;
// Return a successful status
return res.status(201).json({data: {success: true}});
});
// Route for sending push notifications
app.post('/send', (req, res) => {
// Extract message, title, and ID from the request
const {message, title, id} = req.body;
// Find the subscription by ID
const subscription = subscriptions[id];
// Create the payload for the push notification
const payload = JSON.stringify({ title, message });
// Send the push notification
webPush.sendNotification(subscription, payload)
.catch(error => {
// Return a 400 status in case of an error
return res.status(400).json({data: {success: false}});
})
.then((value) => {
// Return a 201 status in case of successful sending
return res.status(201).json({data: {success: true}});
});
});
Setting Up the Client Side
Here we will be working with a basic React application. To make everything as simple as possible, we'll create our own hook to help us simplify the work with push notifications. I'll leave comments to make it clear how everything is set up.
// Function to convert Base64URL to Uint8Array
const urlBase64ToUint8Array = (base64String) => {
const padding = '='.repeat((4 - base64String.length % 4) % 4);
const base64 = (base64String + padding)
.replace(/-/g, '+')
.replace(/_/g, '/');
const rawData = window.atob(base64);
const outputArray = new Uint8Array(rawData.length);
for (let i = 0; i < rawData.length; ++i) {
outputArray[i] = rawData.charCodeAt(i);
}
return outputArray;
};
// Hook for subscribing to push notifications
const useSubscribe = ({ publicKey }) => {
const getSubscription = async () => {
// Check for ServiceWorker and PushManager support
if (!('serviceWorker' in navigator) || !('PushManager' in window)) {
throw { errorCode: "ServiceWorkerAndPushManagerNotSupported" };
}
// Wait for Service Worker to be ready
const registration = await navigator.serviceWorker.ready;
// Check for pushManager in registration
if (!registration.pushManager) {
throw { errorCode: "PushManagerUnavailable" };
}
// Check for existing subscription
const existingSubscription = await registration.pushManager.getSubscription();
if (existingSubscription) {
throw { errorCode: "ExistingSubscription" };
}
// Convert VAPID key for use in subscription
const convertedVapidKey = urlBase64ToUint8Array(publicKey);
return await registration.pushManager.subscribe({
applicationServerKey: convertedVapidKey,
userVisibleOnly: true,
});
};
return {getSubscription};
};
Example of Using the React Hook to Get the Subscription Object and Send it to the Server
// Import the useSubscribe function and set the public key (PUBLIC_KEY)
const { getSubscription } = useSubscribe({publicKey: PUBLIC_KEY});
// Handler for subscribing to push notifications
const onSubmitSubscribe = async (e) => {
try {
// Get the subscription object using the getSubscription function
const subscription = await getSubscription();
// Send the subscription object and ID to the server for registration
await axios.post('/api/subscribe', {
subscription: subscription,
id: subscribeId
});
// Log a message in case of successful subscription
console.log('Subscribe success');
} catch (e) {
// Log a warning in case of an error
console.warn(e);
}
};
So, we've successfully completed most of the work, now we need to display the notification that will come from Google Cloud Messaging.
Push Notifications
To implement background tracking of new notifications in our application, we will use two key technologies: Service Worker and Push API.
A Service Worker is a background script that runs independently from the main thread of the web application. This script provides the ability to handle network requests, cache data, and in our case — listen for incoming push notifications.
The Push API is a web API that allows servers to send information directly to the user's browser.
Example of service-worker.js:
// Add a 'push' event listener to the service worker.
self.addEventListener('push', function(event) {
// Extract data from the push event
const data = event.data.json();
// Options for the notification
const options = {
// The message text in the notification
body: data.message,
// The icon displayed in the notification
icon: 'icons/icon-72x72.png'
};
// Use waitUntil to keep the service worker active
// until the notification is displayed
event.waitUntil(
self.registration.showNotification(data.title, options)
);
});
Demo
To test it on your own device, here's the link to the demo environment. For a more detailed code review, I'm leaving the link to the GitHub repository.
Top comments (0)