How to setup iOS Safari web-push notifications 🔔

Fasten your seatbelts, tech enthusiasts, because a major breakthrough has hit the field! iOS has recently unveiled its latest feature – web push notifications. This is a significant advancement, particularly for web developers working on Progressive Web Applications (PWAs).

Until recently, push notifications were exclusive to native apps downloaded from the App Store. But that’s all in the past now! We’ve just tried out these new iOS notifications on a project for one of our clients. And you’re in luck – we’re ready to share insider tips and tricks from our experience.

But hold on; it’s not just all talk and no play here. Whip out those code editors, and warm up those copy-pasta keyboard keys because we’ve got a demo for you!

Web-Push Notifications: The Internet’s Way of Saying ‘You’ve Got Mail!

While we don’t have a friendly postman like Tom Hanks to deliver these little envelopes, the web has its own way of saying: ‘You’ve got mail!’

Web-push notifications are exactly that—an alert that pops up on your device to inform you about updates, alerts, or messages from your subscribed websites, even when they’re not open on your screen.

However, web-push notifications aren’t just amazing for users—they’re also a powerful tool for marketers and website owners. Are you looking to increase your subscriber count? Notify your users about new content? Or sending targeted, personalized messages? Web-push notifications keep your audience engaged and prevent your updates from gathering dust in an unopened email inbox.

Mobile Web Push Requirements for iOS and iPadOS

What do we need to get some of that fine web push notifications implemented in our web apps?

  • The web app must be PWA. In other words, it must serve a valid web app manifest file.
  • Users need to install the app shortcut to their home screen using the share menu in Safari.
  • To trigger the permission prompt for notifications on a web app (installed on your home screen), the user must perform an action (for example, tap or scroll).

Let’s go over these steps in detail by creating a simple web app that sends push notifications to the users.

Demo

If you are not a developer and want to implement push notifications for iOS, contact us!

index.html

Alright, let’s kick things off with a straightforward HTML page. It’s going to load up our script and manifest file. But what’s a fun journey without a little button to light up our path? The button plays a crucial role here – it’s the trigger for our notification permission request.

Remember, Safari operates under specific guidelines. It won’t just invade the user’s space uninvited – it requires user action. In this case, that’s a click on a particular button. This interaction sends the invitation, or more accurately, the request for notification permission. Therefore, always remember that when dealing with Safari, obtaining permission is an interactive process.

<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <link rel="manifest" href="/webmanifest.json">
  <script src="script.js" defer></script>
  <title>iOS Notifications Demo App</title>
</head>
<body>
  <button id="subscribe">Subscribe</button>
</body>
</html>

webmanifest.json

Push notifications only work when your website gets installed/added to a user’s home screen. The key to this happening is something we call a web manifest file.

The web app manifest is a file that dictates how your web content appears as an app within the OS. It can include basic details like the app’s name, icon, and theme color; advanced preferences like desired orientation and app shortcuts; and catalog metadata like screenshots.

And oh, if you’ve got a swanky icon that you’re dying to showcase, you can use favicon generator to create the required icon sizes.

psst…if you are lazy to create your own icons, we got you! 512.png 192.png
psst2…if you are lazy to do any of this work, we got you!

{
    "name": "iOS Notifications Demo App",
    "short_name": "iOS Notifications Demo App",
    "icons": [
        {
            "src": "/192.png",
            "sizes": "192x192",
            "type": "image/png"
        },
        {
            "src": "/512.png",
            "sizes": "512x512",
            "type": "image/png"
        }
    ],
    "theme_color": "#ffffff",
    "background_color": "#ffffff",
    "display": "standalone",
    "start_url": "/"
}

script.js

So here’s how we get the party started – first things first, we register service worker. iOS Safari insists on this protocol before it sends over the notifications.

Next up, we give a job to our subscribe button and instruct it to eavesdrop (or rather, click-listen). Upon the click, we’ll sweep in, with the politeness of a top-hat-tipping English gent, to ask the user – “Would you pardon us for the interruption and grant our humble request to send you notifications?”

If we’re greeted with an accepting nod (or granted permission in technical jargon), it’s time to pop the champagne! Or, in this case, test the notification by calling registration.showNotification with appropriate parameters.

async function run() {
  // A service worker must be registered in order to send notifications on iOS
  const registration = await navigator.serviceWorker.register(
    "serviceworker.js",
    {
      scope: "./",
    }
  );

  const button = document.getElementById("subscribe");
  button.addEventListener("click", async () => {
    // Triggers popup to request access to send notifications
    const result = await window.Notification.requestPermission();

    // If the user rejects the permission result will be "denied"
    if (result === "granted") {
      await registration.showNotification("Hello there", {
        body: "Looking good on iOS!",
      });
    }
  });
}

run();

Create an empty serviceworker.js file

Service workers are reliable helping hands in the background of a web application, taking care of tasks like managing offline requests, enabling push notifications, and synchronizing data.

For now, it just needs to ‘exist’ to meet service workers’ requirements.

You look great, the code looks great, and it is time for the first test!

Enabling the Notifications Feature in iOS 16.4 Safari Beta

Safari has notifications off by default while in beta. To switch them on, hop over to Settings > Safari > Advanced > Experimental Features, find “Notifications,” and flick that toggle to on. This should be preset in the coming iOS 16.4, but for now, with the beta, this extra step may be required.

HTTPS Connection

For this to work, we need an HTTPS connection. You can deploy your website using free services like Netlify, or you can use ngrok to hook your local server to that sweet HTTPS URL. You can read here how to do it. We will use ngrok in this demo.

After deployment, open the URL on your iOS device and notice that pressing subscribe button doesn’t do anything. Don’t be mad; this is because we need to click the share button and then “Add to Home Screen.”

When the app has made itself comfy on the home screen, open it and tap that subscribe button. Like a courteous host, the app will ask if it can send notifications your way.

Once you approve it, it’ll shoot out a test notification! Let’s take a moment and shower you with well-deserved applause. You nailed it! Indeed, you’ve got a talent for this copy-pasta thing called programming.

Server Side Push Notifications

We got it to show notifications, and now we want more! We want to save subscription data and send notifications from our server!

Create Express Application

We’re in for a quick server-side ride! We’ll keep things simple. Let’s start by addressing our required dependencies.

npm i express cors dotenv web-push

express is for the web server, cors is for accepting requests from another domain, dotenv steps in to handle environment variables, and web-push? Well, it quite simply lives up to its name, taking charge of the web push!

Generate Vapid Keys

These keys are required to authenticate your application server with the push service, support secure communication, and provide reliable contact between the two entities—serious stuff.

./node_modules/.bin/web-push generate-vapid-keys

This command will generate public and private keys.

Add keys to .env file

Create .env file and paste public and private keys. We also need to add an email address (push service use this email if they need to contact you, for example, to say hello and that they think you are fantastic).

VAPID_PUBLIC_KEY="Huge string generated in previous step"
VAPID_PRIVATE_KEY="Not so huge string generated in previous step"
VAPID_MAILTO=genius_in_disguise@gmail.com

p.s. that email is available (you’re welcome)!

server.js

This is the simplified version, but remember you might have a database where you store user-specific subscriptions and other elements. This nifty API lets the client sign up for notifications by making a POST to /subscribe. To trigger the notification just send a GET request at /notify. And there you have it! Notification service is at the bare bones.

const express = require("express");
const webpush = require("web-push");
const dotenv = require("dotenv");
const cors = require("cors");

dotenv.config();

const app = express();

app.use(express.json());
app.use(cors());

let subscriptionData = null;

webpush.setVapidDetails(
  `mailto:${process.env.VAPID_MAILTO}`,
  process.env.VAPID_PUBLIC_KEY,
  process.env.VAPID_PRIVATE_KEY
);

app.get("/notify", (req, res) => {
  webpush.sendNotification(
    subscriptionData,
    JSON.stringify({ title: "Great job", body: "You got this!" })
  );
  res.sendStatus(200);
});

app.post("/subscribe", async (req, res) => {
  subscriptionData = req.body;
  console.log({ subscriptionData });
  res.sendStatus(200);
});

app.use(express.static("./public"));

app.listen(3000, () => console.log("Server started on port 3000"));

We have a problem; this server must also be available through HTTPS. Use ngrok or deploy this server on a cloud. If you are using ngrok, here is a config file (ngrok.yml) that connects your local client server (port 5500) and backend server (port 3000) to the world. You can use this configuration with this command: ngrok start --config ./ngrok.yml --all. Change configuration to reflect your use case.

authtoken: YOUR_FREE_NGROK_AUTH_TOKEN
tunnels:
    first:
        addr: 5500
        proto: http
    second:
        addr: 3000
        proto: http
version: "2"
region: us

Update script.js

const BASE_SERVER_URL = "https://" // Server url starting with https, if you used nginx URL is displayed after nginx start command 
const VAPID_PUBLIC_KEY = "VAPID_PUBLIC_KEY" // generated in one of previous steps

function urlBase64ToUint8Array(base64String) {
  const padding = "=".repeat((4 - (base64String.length % 4)) % 4);
  const base64 = (base64String + padding)
    .replace(/\-/g, "+")
    .replace(/_/g, "/");
  const rawData = atob(base64);
  const outputArray = new Uint8Array(rawData.length);
  for (let i = 0; i < rawData.length; ++i) {
    outputArray[i] = rawData.charCodeAt(i);
  }
  return outputArray;
}

async function run() {
  // A service worker must be registered in order to send notifications on iOS
  const registration = await navigator.serviceWorker.register(
    "serviceworker.js",
    {
      scope: "./",
    }
  );

  const button = document.getElementById("subscribe");
  button.addEventListener("click", async () => {
    // Triggers popup to request access to send notifications
    const result = await window.Notification.requestPermission();

    // If the user rejects the permission result will be "denied"
    if (result === "granted") {
      const subscription = await registration.pushManager.subscribe({
        applicationServerKey: urlBase64ToUint8Array(
          VAPID_PUBLIC_KEY // generated in one of previous steps
        ),
        userVisibleOnly: true,
      });

      if(subscription){
        await fetch(BASE_SERVER_URL + "/subscribe", {
          method: "post",
          headers: {
            "Content-Type": "application/json",
          },
          body: JSON.stringify(subscription),
        });
        button.innerText = "Subscribed!"
      }
    }
  });
}

run();

Update serviceworker.js

The last step is to update our service workers. We added an event listener to detect when a message has been pushed. We do that by extracting the title and body and calling the showNotification function.

self.addEventListener("push", async (event) => {
  const { title, body } = await event.data.json();
  self.registration.showNotification(title, {
    body,
  });
});

Reopen/refresh your PWA (redeploy if needed) and click on subscribe button, then open /notify route in your browser, and a new notification sent from the server should appear.

Final words

And there we have it! Together, we’ve navigated the world of web notifications, played with service workers, and made our way through subscription data and server connections. It’s been a thrilling journey filled with enlightenment and innovation. Pat yourself on the back, champ. You’ve done a splendid job! Until next time, keep coding and keep delighting your users!

Resources

https://web.dev/push-notifications-subscribing-a-user
https://devtails.xyz/@adam/how-to-setup-web-push-notifications-in-ios-safari
https://pushalert.co/documentation/ios-web-push