How to Implement Safari Push Notifications on Your Website26th of Dec 2013 - Samuli Hakoniemi

What if you could use APNs (Apple Push Notification Service) to send push notifications for your website users right on their desktop? Since OS X Mavericks it has been possible to dispatch push notifications from your web server directly to users.

In this article, I’ll provide step-by-step instructions of implementing Safari Push Notifications directly in your website.

Table of Contents

Prerequisites

In order to get everything up and running, you need at least:

In this example, I’m using Heroku and node.js (+ Express) for serving both the website content and push package.

Registering a Website Push ID

First step is to register a Website Push ID. This is done at “Certificates, Identifiers & Profiles” section of the Member Center.

Under “Identifiers”, you’ll find a sub-section titled Website Push IDs.

Insert description and identifier, which is recommended to be in reverse-domain name format, starting with “web”. In my case I’m using web.com.herokuapp.hakonieminotification as an identifier.

After you’ve registered your Website Push ID, you’re ready to generate a certificate.

Generating a Certificate

This can be considered as the hardest part of the tutorial. It requires multiple steps and all of these needs to be completed.

We start our journey by logging into Developer Overview. Under there you should see a folder link titled Certificates. Navigate there and you go into a same view where we’ve created Push ID. This time we select Certificates and create a new certificate.

Now you should see a list of Development and Production certificate types. Under Production there is a checkbox for Website Push ID Certificate. After selecting that you’ll get a prompt about which Website Push ID we are going to use. This should be obvious.

Now we’re going to create a CSR by using Keychain Access. Launch it and select Keychain Access » Certificate Assistant » Request a Certificate from Certificate Authority.

Fill in your details (leave empty if unsure). Request is Saved to disk. Now you should be able to save [filename].certSigningRequest file to the Desktop.

Now that we’re done with the CSR file we can continue our process on Development Portal and generate our certificate. After that we’re able to download our .cer file. After downloading it, double-click the .cer file.

You should end up in the Keychain Access, under login section, where you should see your certificate. Right-click it and select “Export Website Push ID [web.your.reversed.domain.name]”. This should open up a dialog where you can save [filename].p12. Then you’ll be prompted with the password which will be used to protect the exported item. In our case this can be left empty.

Now that we’ve created .p12 file, we can proceed on creating the actual package.

Contents of the Push Package

When website asks user for permission to send push notifications, Safari will ask your server for a push package. This package is a normal zip file containing following files (all files are required, and no other files can be included):

MyPushPackage.pushpackage
  icon.iconset
    icon_128x128@2x.png
    icon_128x128.png
    icon_32x32@2x.png
    icon_32x32.png
    icon_16x16@2x.png
    icon_16x16.png
  manifest.json
  signature
  website.json

Every icon file and website.json are created by you, while manifest.json and signature files are generated by a script.

Website.json

Website.json contains following information (mine as an example):

{
    "websiteName": "Heroku Push Notification Test",
    "websitePushID": "web.com.herokuapp.hakonieminotification",
    "allowedDomains": ["http://hakonieminotification.herokuapp.com"],
    "urlFormatString": "http://hakonieminotification.herokuapp.com/%@/",
    "authenticationToken": "19f8d7a6e9fb8a7f6d9330dabe",
    "webServiceURL": "https://hakonieminotification.herokuapp.com"
}

This is described in the Apple Documentation as:

  • websiteName – The website name. This is the heading used in Notification Center.
  • websitePushID – The Website Push ID, as specified in your registration with the Member Center.
  • allowedDomains – An array of websites that are allowed to request permission from the user.
  • urlFormatString – The URL to go to when the notification is clicked. Use %@ as a placeholder for arguments you fill in when delivering your notification. This URL must use the http or https scheme; otherwise, it is invalid.
  • authenticationToken – A string that helps you identify the user. It is included in later requests to your web service. This string must 16 characters or greater.
  • webServiceURL – The location used to make requests to your web service. The trailing slash should be omitted.

Creating the Push Package

Now that we have our content (icons + website.json) set up, we can create both manifest and signature files. This is done with createPushPackage.php script (or with push_package gem).

Manifest

The manifest is a JSON dictionary of your each file in push package where filename is the key and SHA1 checksum is the value.

createPushPackage.php contains a function create_manifest($package_dir) for creating the manifest. Use this and it’ll generate a file manifest.json into your .pushpackage directory.

Signature

Remember the .p12 we created in the beginning? This file is passed to the function create_signature($package_dir, $cert_path, $cert_password). If you left the password empty, just pass empty string to the function.

Archive file

There is a function called package_raw_data($package_dir) for creating the ZIP file. This is the package itself we’re serving for the Safari browser. If you’ve successfully completed the previous steps, you should now have created a valid package.

Serving Content and the Push Package

I’ve split this into two sections: server-side and client-side configuration. First we’ll start with the server-side configuration.

Server-side Configuration

My Node / Express application looks like:

var express = require('express');
var app = express();
var port = process.env.PORT || 3000;

app.listen(port);

app.get('/', function(req, res) {
    res.sendfile('index.html');
});

app.post('/v1/pushPackages/web.com.herokuapp.hakonieminotification', function(req, res) {
    res.sendfile('SamuliHakoniemi.pushpackage.zip');
});

app.post('/v1/log', function(req, res) {
});

This should be quite self-explanatory, but let’s go it quickly through:

  • Line #7 – serving the index.html file that requests the permission from the user to use push notifications.
  • Line #11 – serving the push package which is requested by the browser as a POST request
  • Line #15 – for logging (errors), where HTTP body contains a JSON with key logs and as a value there’s an array of strings describing errors.

Server-side Endpoints

As you might have noticed, there’s a certain logic with the endpoints. Notice that “version” is always v1 and deviceToken is the token you’ll receiver from the client when user grants a permission:

  • webServiceURL/version/pushPackages/websitePushID – location of the push package, requested by POST request.
  • webServiceURL/version/devices/deviceToken/registrations/websitePushID – when an user grants a permission or later updates his permission level, a POST request is sent. When user removes the permission for push notifications, a DELETE request is sent.
  • webServiceURL/version/log – when an error occurs a POST request is made to this endpoint

I suggest reading articles in Resources section for more verbose explanation of the endpoints.

Client-side Configuration

There are different code examples of implementing the permission request. This simple piece of code is used on my site:

var pushId = "web.com.herokuapp.hakonieminotification";

var subscribe = document.querySelector("#subscribe");
subscribe.addEventListener("click", function(evt) {
    pushNotification(); 
}, false);

var pushNotification = function () {
    "use strict";
    
    if ('safari' in window && 'pushNotification' in window.safari) {
        var permissionData = window.safari.pushNotification.permission(pushId);
        checkRemotePermission(permissionData);
    } else {
        alert("Push notifications not supported.");
    }
};

var checkRemotePermission = function (permissionData) {
    "use strict";
    
    if (permissionData.permission === 'default') {
        console.log("The user is making a decision");
        window.safari.pushNotification.requestPermission(
            'https://hakonieminotification.herokuapp.com',
            pushId,
            {},
            checkRemotePermission
        );
    }
    else if (permissionData.permission === 'denied') {
        console.dir(arguments);
    }
    else if (permissionData.permission === 'granted') {
        console.log("The user said yes, with token: "+ permissionData.deviceToken);
    }
};
  • Lines #4 – #7 – since I’m using a separate button for subscribing, we need to add event listener for it.
  • pushNotification() – this function is called after the subscribe button is clicked and it will check whether push notifications are actually supported. And if so, it makes the initial call for the checkRemotePermission function.
  • checkRemotePermission() – this function makes the actual request for the permission and is executed again as the callback of function window.safari.pushNotification.requestPermission(url, websitePushID, userInfo, callback).

Above lines of code are from the actual implementation I’ve made. You can test it at: http://hakonieminotification.herokuapp.com.

Possible Problems and Solutions

You may encounter problems while you’re first trying to implement push notifications.

The most common one seems to be that user denies a permission without client even asking for it. This is because the request never reaches the push package (the endpoint isn’t correct). This use case is not described in the Apple’s documentation, which only claims that “denied” state occurs only when user denies the permission.

Other problem seems to be that once you’ve granted or denied the permission, you’re never seeing the permission prompt again. In order to fix that, you can configure permissions from Safari » Preferences » Notifications.

For other troubleshooting and interpreting the log messages, I suggest reading the Troubleshooting section from Apple’s documentation.

Resources

I hope that everything went well after reading my article. In any case, I suggest reading also these articles which contain very valuable information for implementing push notifications.

10 thoughts on “How to Implement Safari Push Notifications on Your Website

  1. zakaria on said:

    thanks for your tuto, please how i can have the authenticationToken ,i must to generate this authenticationToken , if yes how ?

  2. Samuli Hakoniemi on said:

    Hi zakaria,

    I’m under assumption that authenticationToken can be any string with 16 characters or more. But I have to dig deeper and ensure this.

  3. zakaria on said:

    I understand how I can have the authenticationToken ^^, but please I did not understand how i can use the Server-side Endpoints, if i make myWebService/v1/pushPackages/websitePushID return “Not Found”

  4. Samuli Hakoniemi on said:

    Are you making a POST request into your endpoint? If so, then I suggest you using logging and checking what’s going wrong.

    Also I suggest reading articles in the “Resources” section, especially “Configuring Safari Push Notifications”.

  5. zakaria on said:

    I tried bu it dosent work, if you can share with me your full code I’ll apreciated I’ll be very thankfull, and for the https protocol, is it obligatory ?

  6. Samuli Hakoniemi on said:

    The full code is included in the tutorial.

    And https is obligatory for serving the push package.

  7. Alberto Mota on said:

    I did all the stuff you and apple said. But i receive the next log message: “”logs”:[“Signature verification of push package failed”]”

    Apple said that this error is because: The manifest was not signed correctly or was signed using an invalid certificate

    The manifest is signed correctly because i have did it with createPushPackage.php and push_package gem. In both cases i receive the same message.

    So it seems to be that i am using and invalid certificate.

    But i know why it is invalid. I have exported as .p12. the Key chain says that it is valid. I generated my web push ID. My webpage is reachable only in the LAN do you think this is the problem?

    Please help me.

  8. Samuli Hakoniemi on said:

    It sounds like the request for push package is made and it’s reached, but there’s something wrong with the signature (or certificate). It’s said that “If the contents of your push package ever change, you’ll need to recompute your signature”.

    However, I’m not convinced that all of the logged errors are consistent. Therefore the error may not have anything to do with the signature.

    What’s your local server configuration? Are you using a SSL certificate that is not self-signed?

    You could try setting up an environment to Heroku like I’ve done, and try to serve the push package through there via HTTPS. If the same error occurs, then I can’t help you any further. Section “Generating Certificate” is all that I can offer help with.

  9. Hey, nice tutorial! Looked months for it.
    I tried to implement this push-thing since it was announced. But I never succeeded.

    At the moment, I don’t get the Permission-Ask-Window from Safari. I already copied your code 1:1, nothing gets shown up.
    By using my end-point and push-id, I can notice that one api-method gets called:
    @url POST {version}/pushPackages/{websitePushID}
    The push-package gets generated and that seems to work fine. But I don’t see the permissions-sheet and I already removed any permissions in Safari, cleared cache, … nothing helps.
    I remember that I already have seen this sheet months ago, during a try. But now, I don’t get it…

    What could I do? I don’t get log-messages from Apple…

  10. Ok: Got it working…

    At first: I did not get any logs cause I missed the ‘version’ parameter in log-url. Then I received error-logs claiming about not allowed urls. But they were ok… After all I found out: DO THE HECK NOT USE / AT THE END OF YOUR URLS.