How to Set Up Push Notifications in Safari on iOS
Tags: javascript, webdev, react, ios
Published: 2023-10-15 12:55:59

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.

{% embed https://www.youtube.com/watch?v=9Az_pxqULvw&ab_channel=u4aew %}

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.

![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/qifmmuga2b7gmdbst6rd.png)

### 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.

```javascript
// 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.

```javascript
// 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

```javascript
// 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:

```javascript
// 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](https://push.fancy-app.site/). For a more detailed code review, I'm leaving the link to the [GitHub repository](https://github.com/u4aew/react-pwa-push-notifications).