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.

The final version of iOS 5 has been finally released and there’s lots of buzz going around it’s new features. Most of the discussion focuses on the operating system itself which is totally understandable. There are lots of improvements and nifty little features to play with.

But one thing that seems not to get such attention is what iOS 5 brings to us, web developers, and how it improves the experience with web applications.

In this article I’ll go through most of the major features that are included in iOS 5 for web developer point-of-view.

Table of Contents

-webkit-overflow-scrolling

This is probably most anticipated feature for web applications. Until today it hasn’t been easily possible to add scrollable content in web document.

Briefly, all you need to define is:
elem {
 overflow:scroll;
 -webkit-overflow-scrolling:touch;
}

To achieve proper scrolling support for iOS 4 and/or other devices, I strongly suggest using iScroll 4.

And if you want to display scrollbars all the time, read this post: Force Lion’s scrollbar back. It will help you on displaying the scrollbar while user is not accessing the scrollable area, which is a very good visual guidance for user that content can be scrolled. But be warned: “custom” scrollbar won’t update it’s location while user is scrolling and meantime there are two scrollbars displayed.

position:fixed

Position:fixed is well-known CSS property that hasn’t earlier been included in iOS. But now it’s there, ready to use.

I noticed that setting a fixed element it has partial transparency by default. You even can’t turn it off by setting opacity to 1.0. If you happen to know how to solve this, please comment on my blog.

New Input Types

iOS5 provides several new input types that didn’t exist earlier on iOS4. These input types are: date, time, datetime, month and range.

I have to mention that the user experience with range is awful – with your (fat) finger you end up selecting the whole control instead of value slider all the time.

Note: input type=”file” isn’t still working. “Choose File” button is displayed, but at the same time it’s disabled.

WOFF Font Support

iOS 5 supports WOFF (Web Open Font Format) fonts. This is good news in a way. I haven’t personally tested whether there’s any benefit compared to SVG or TTF from a rendering or performance point-of-view.

Web Workers

Web Workers API is a bit less familiar for many developers. They allow to run long-running scripts without halting the user interface and they’re not interrupted by other actions.

The problem with Web Workers on iOS 5 is – as you may guess – the perfomance. You can try Web Workers with Javascript Web Workers Test. But I have to mention that while it took only about five seconds with my workstation, the same execution time with my iPhone 4 was 106 seconds. So as you can see, there’s a huge difference on performance.

contentEditable

iOS 5 supports contentEditable attribute, which allows rich text editing (RTE) of content. This is very welcomed feature offering the possibility of building WYSIWYG editors that can be used eg. with iPad.

Read more about this feature at: WYSIWYG Editing (contentEditable support) in iOS 5.

classList API

ClassList API is very useful while writing native JavaScript. It has few simple functions (like add(), remove(), toggle()) that are meant for handling classNames in an element.

If you want to implement classList API and ensure backwards compatibility, use classList.js polyfill, written by Eli Grey.

matchMedia()

Function matchMedia() is relatively new function for detecting media queries with JavaScript. The implementation is very simple:

if (matchMedia("(min-width: 1024px)").matches) {
    alert('your screen is at least 1024px wide');
}
else {
    alert('your screen is less than 1024px wide');
}

Can’t say how useful that is yet, since I’ve personally never used it before. But we’re living the times of Responsive Web Design and there may be conditions where this may be needed.

For browsers that doesn’t support matchMedia(), there is a matchMedia.js polyfill available, written by Paul Irish.

And if you’re more interested in similar logic, I suggest reading about yepnope.js.

Changes in Gestures Events

Gestures events (gesturestart, gesturechange, gestureend) now returns pageX and pageY values for events – in addition to scale and rotate values. These values didn’t exist in iOS4, forcing developer to retrieve X/Y-coordinates with corresponding touch events.

Compass

iOS 5 comes also with two neat properties: webkitCompassHeading and webkitCompassAccuracy. You can read more about them and test them at: Taking a new device API for a spin.

WebGL

Well… WebGL is kind of implemented in iOS5. But only for iAd.

However there are rumors promising good, and already it’s said that “things are in place” but they’re just not fully working (or have been disabled). So, let’s keep our fingers crossed that next (minor) update will include support for WebGL.

Anything Else?

Mark Hammonds has written a comprehensive article in mobiletuts+, titled iOS 5 for Web Devs: Safari Mobile Updates. That’s really worth of reading!

And if you’re interested in browser performance in general, then you should read iOS 5 Top 10 Browser Performance Changes.

If there are other things to mention, feel free to comment and bring your ideas up. I’ll keep on updating this post right after new information arises about iOS 5.