Push notifications in Magento

Push notifications in Magento

When running a web shop there is always need to somehow keep your customers posted with the new information. It can be done in different ways, but using push notifications seems like a pretty neat and convenient way to do it. This technology has been around for some time now but unlike mobile apps, there aren’t many websites that actually utilize it. There’s probably not enough buzz about it out there and to be honest, for certain channels, the technology is not quite ready yet in terms of development. Nevertheless, let’s check it out.

How it works?

Implementation of push notification feature differs depending on a mobile platform and a web browser we are targeting, but the core principle of the technology is the same in all cases.

In order to start receiving push notifications user first must opt-in to receive messages. In other words we must give a permission to an app or a website to send notifications to our device. This usually happens on applications first launch after installation or on first visit to a website, and looks similar to this:

push_notifications_opt_in_web push_notifications_opt_in_app

If we choose to receive push notifications then mobile app or a web browser makes a request to notification service provider which generates a unique token for our device and sends it back to the app/browser. Token is then sent to the web server of our choice which stores it in the database for later use.

push_notifications_token_gen_and_save

To send push notifications we use tokens which were previously stored in the database. Think of a token as an address of a device which should receive a message. Notifications are not sent directly to devices but instead to notification service providers which are responsible for its delivery to the end users.

push_notifications_send

Which notification service provider we are referencing depends on the platform we wish to target.

There are a few most “important” ones:

  • GCM (Google Cloud Messaging) for Chrome browser and Android apps,
  • APNS (Apple Push Notification Service) for OSX and iOS apps,
  • and from up until recently MWPS (Mozilla Web Push Service) for Firefox browser.

There is no universal standard which defines how notification providers should implement their service and how it should work. Therefore, every provider has its own way of implementation with different requirements and limitations. From developers point of view that kind of makes things more complicated.

Our experience

That’s all well and nice but you are probably wondering how do I implement this in my Magento store? Well, one of our clients requested of us to implement push notifications system on his Magento web shop and to cover two platforms – Apple (OSX and iOS mobile application) and Google (Chrome browser and Android mobile app). In the following text I’ll try to briefly cover the way we implemented it and the problems we faced along the way. I won’t go into details of our module or it’s structure. Instead, I’ll focus on parts which are specific for push notifications system and necessary in order to utilize it.

Before we started our work on the extension we knew it had to be robust. We needed to cover several things:

  • implement javascript opt-in logic
  • create an API through which different channels can import generated tokens to our database,
  • make all imported tokens easily visible in magento admin,
  • make an admin interface which enables creation of notification messages and their scheduling for sending,
  • make all created notification messages easily visible in magento admin along with their current status (new, scheduled, sent, failed),
  • implement the actual notifications sending process for different channels,
  • implement javascript for notification display,
  • make it easily expandable with additional new channels in the future (Firefox, Edge…).

Before we go over these steps I would like to give you links to the documentations since I’m gonna be referring to them later on. So, Google and Apple.

› Opt-in

Implementation of opt-in and subscription creation process is different depending on a platform. I won’t go into mobile app side of the story since I’m not competent to talk about it. Instead, I’ll give you an example of how it can be done for web platforms – Chrome and Safari.

• Chrome

Before displaying opt-in pop up we need to make a few checks to make sure that the given browser supports all that is needed for push notifications to work properly. First thing we need to check is whether service workers are supported in your browser. In short, service worker is a script that runs in the background in browser, and enables features that don’t require web page or user interaction. For now, lets just say that Chrome uses service worker to display a push notification. We’ll see what the actual file looks like later on. Here’s a code sample which does the check:

<?php   $workerFile = Mage::helper('inchoo_notification')->getChromeWorkerFile(); ?>
<script>
   window.addEventListener('load', function() {
       // Check that service workers are supported, if so, progressively
       // enhance and add push messaging support, otherwise continue without it.
       if ('serviceWorker' in navigator) {
           navigator.serviceWorker.register('<?php echo $workerFile; ?>')
               .then(initialiseState);
       } else {
           console.warn('Service workers aren\'t supported in this browser.');
       }
   });
</script>

Important thing to note is that service workers function on secure connections only. In other words, if your site is not on https Chrome push notifications simply can’t work since service worker can’t be registered on insecure connections.

If service workers are supported and Chrome manages to internally register the provided file, we call initializeState() function which does the rest of the necessary checks:

// Once the service worker is registered set the initial state
   function initialiseState() {
       // Are Notifications supported in the service worker?
       if (!('showNotification' in ServiceWorkerRegistration.prototype)) {
           console.warn('Notifications aren\'t supported.');
           return;
       }
 
       // Check the current Notification permission.
       // If its denied, it's a permanent block until the user changes the permission
       if (Notification.permission === 'denied') {
           console.warn('The user has blocked notifications.');
           return;
       }
 
       // Check if push messaging is supported
       if (!('PushManager' in window)) {
           console.warn('Push messaging isn\'t supported.');
           return;
       }
 
       // We need the service worker registration to check for a subscription
       navigator.serviceWorker.ready.then(function(serviceWorkerRegistration) {
           // Do we already have a push message subscription?
           serviceWorkerRegistration.pushManager.getSubscription()
               .then(function(subscription) {
                   if (!subscription) {
                       return;
                   }
 
                   // Keep your server in sync with the latest subscriptionId
                   sendSubscriptionIdToServer(subscription);
               })
               .catch(function(err) {
                   console.warn('Error during getSubscription()', err);
               });
       });
   }

If the given browser version supports all push requirements Chrome will display opt-in pop. However, opt-in pop up will be displayed only if Chrome has no data about notification permissions for the given website. It serves as initial push notification setting for a given page which Chrome saves internally under its configuration settings. If we forbid Chrome to show push notifications the only way to enable them back is via settings menu. More precisely under: Settings – Content Settings(under Privacy section) – Notifications – Manage Exceptions.

If we allow to receive push notifications Chrome tries to get a token for our device from GCM. In order for subscription process to succeed we need to include web app manifest file to our website. Manifest is a simple json file and looks something like this:

{
  "name": "Push Demo",
  "short_name": "Push Demo",
  "icons": [{
        "src": "images/icon-192x192.png",
        "sizes": "192x192",
        "type": "image/png"
      }],
  "start_url": "/index.html?homescreen=1",
  "display": "standalone",
  "gcm_sender_id": "<Your Project ID Without the Hash>"
}

You’ll notice the parameter called gcm_sender_id or project id. In order to obtain it you need to create a new project for your website through google developers console. If you are not familiar with how to do it just follow the google documentation, section “Make a Project on the Google Developer Console”.

Once we have created the manifest file we need to include it in the head of our website:

<link rel="manifest" href="path_to_manifest.json">

If manifest file is valid and everything goes well we should have a subscription object holding our device’s token. We can take the token and send it to our server in order to be saved to database:

function sendSubscriptionIdToServer(subscription)
 {
      subscription.endpoint.substring('https://android.googleapis.com/gcm/send/'.length);
 
       new Ajax.Request('<?php echo $this->getUrl('your_api_url', array('_secure'=> (Mage::app()->getStore()->isCurrentlySecure()) ? true : false))?>', {
           parameters: {subscriptionId: subscriptionId},
           onSuccess: function(response) {
               var status = JSON.parse(response.responseText).status;
               if(status) {
                   console.log('Subscription ID imported successfully.');
               } else {
                   console.log('Error while subscribing.');
               }
           },
           onFailure: function(response)
           {
               console.log('Error while subscribing.');
           }
       });
 }

At this point we have a new Chrome browser associated token in our database.

• Safari

Similar to Chrome, before taking any other action, we need to check whether push notifications are supported in a given browser version:

window.onload = function(){
   if ('safari' in window && 'pushNotification' in window.safari) {
       var permissionData = window.safari.pushNotification.permission(WEB_SITE_PUSH_ID);
       checkRemotePermission(permissionData);
   }
};

You’ll notice the parameter web_site_push_id. It identifies your website with Apple push service. You need to create one from your apple developers account. If your are not familiar with how to do it you can follow these instructions.

If push is supported, we call checkRemotePermission() function which checks permission type for a given website and takes an appropriate action:

var WEB_SITE_PUSH_ID = '<?php echo $this->getWebSitePushId();?>';
var packageUrl = '<?php echo Mage::app()->getDefaultStoreView()->getBaseUrl(Mage_Core_Model_Store::URL_TYPE_LINK, true) . 'safari_package_action_url'?>';
 
var checkRemotePermission = function (permissionData) {
    if (permissionData.permission === 'default') {
        window.safari.pushNotification.requestPermission(
            packageUrl,
            WEB_SITE_PUSH_ID,
            {},
            checkRemotePermission
        );
    } else if (permissionData.permission === 'denied') {
        console.log('denied');
    } else if (permissionData.permission === 'granted') {
             sendSubscriptionIdToServer(permissionData)
 	}
};

If there is no notification permission data for the given website (permission equals ‘default’), Safari will ask for a permission i.e. try to display an opt-in pop up. But things are not that simple as with Chrome. In order to display opt-in pop up Safari first makes an ajax request to an url of our choice and expects a valid zip package to be returned. In order for a zip package to be valid it needs to be signed with a valid .p12 certificate and have very specific content and structure. For more information on the package itself and how to generate it check out the documentation under section “Building the Push Package” section.

If returned zip package is valid then opt-in pop up will get displayed, otherwise we’ll get an error in our console log.

If we allow to receive push notifications then unique token for our device is generated. We can grab token from permissionData object and send it to our server in order to be saved to database:

function sendSubscriptionIdToServer(permissionData)
{
 new Ajax.Request('<?php echo $this->getUrl('your_api_url', array('_secure'=> (Mage::app()->getStore()->isCurrentlySecure()) ? true : false))?>', {
                parameters: {
                     token_id: permissionData.deviceToken,
                     website_id: '<?php echo Mage::app()->getStore()->getWebsiteId()?>',
                     store_id: '<?php echo Mage::app()->getStore()->getId()?>'},
                onSuccess: function(response) {
                     console.log('Subscription ID imported successfully.');
                }
            });
        }
 
 }

At this point we have a new Safari browser associated token in our database.

› Token management

All tokens that were imported to Magento can easily be viewed through Magento admin.

push_notifications_token_grid

Every token has certain information associated with it like:

  • customer email and id – if customer was logged while subscribing for push,
  • website and store id customer subscribed from,
  • channel type (chrome, safari, ios, android or some other in the future) – which is necessary to differentiate tokens from different channels.

All this information is useful if we want to do some kind of segmentation of our customers when it comes to push notifications or to create different messages for different websites and languages (store views in Magento).

› Message creation and management

To create a new notification message admin has an appropriate interface at his disposal. It looks like this:

push_notifications_message_create

Two message parameters are obligatory:

  • message text – which is a text that user sees when he receives a message and
  • channel type – which defines a platform for which certain message is intended for.

In addition to those parameters, admin can define a resource which notification will display once user clicks/taps on it. This can really be anything, some product page, category page, cms page, search link, some external link or something else.

Once message is created it needs to be scheduled for sending in order to get picked up by a sending script.

Similar to tokens, all created messages and their current status can easily be viewed via admin grid:

push_notifications_messages_grid

› Sending messages

Messages are being sent periodically by cron. Since channels differ in a way messages are sent, every channel has a php class associated with it that handles the sending process and potential errors. This way we can add a new channels in the future without much trouble.

• Google

To send Android and Chrome push notifications we need to communicate with the GCM. In order for the GCM to accept our request we need a valid API key which authenticates our server with the GCM. API key is obtained through google developers console for the particular project. Just follow the google documentation if you are not familiar on how to do it.

Once we have an API key sending process is pretty straightforward. We need to make a curl request to:

https://android.googleapis.com/gcm/send

and include device tokens and the message data as a request parameters in the following format:

curl --header "Authorization: key=<YOUR_API_KEY>" --header
"Content-Type: application/json" https://android.googleapis.com/gcm/send -d
"{\"registration_ids\":[\"<YOUR_REGISTRATION_ID>\"], \"data\":[\"<MESSAGE_PAYLOAD>\"]}"

Just a small digression, in the documentation, message data is also referred to as a payload. I may use this word in the following text so you know what I mean.

Here’s the php code example which can be used for sending:

$curlParams = array(
   'registration_ids' => array('token_1', 'token_2', 'token_3'),
   'data' => array(
       'title'    => 'your_website.com',
       'text'   => 'message_text',
       'icon'  => Mage::getBaseUrl('skin') . 'path_to_notification_image',
       'url'      => 'your_website.com/product.html',
);
);
 
$apiKey = 'YOUR_API_KEY';
 
$this->_curlAdapter->write(
     Zend_Http_Client::POST,
     'https://android.googleapis.com/gcm/send',
     CURL_HTTP_VERSION_1_1,
     array(
        'Content-Type: application/json',
        'Authorization: key=' . $apiKey
     ),
     json_encode($curlParams)
);

Once we successfully deliver message data to GCM our work is done. It is up to GCM itself to deliver messages to the end users.

• Apple

With Apple things are a bit more complicated than with Google. According to the documentation all communication with the APNS has to go over the secure TCP socket connection and data needs to be sent in a specific binary format.

Address of the socket that we need to connect to is:

ssl://gateway.push.apple.com:2195

To create a socket connection in php we can use something like this:

$certDir =  'PATH_TO_PEM_CERTIFICATE';
$address =  'ssl://gateway.push.apple.com:2195';
 
$socketClient = stream_socket_client(
   $address,
   null,
   null,
   5,
   STREAM_CLIENT_CONNECT,
   stream_context_create(array('ssl'=>array('local_cert'=> $certDir)))
);

As you’ve probably noticed, to encrypt a communication with the APNS we need to pass a local certificate to the socket client as an option of stream context. Funny thing is, obtaining of the certificate actually gave us lots of trouble since apple documentation really lacks any useful information about the certificate or on how to obtain it. After lots of searching, trials and errors we found this tutorial, which turned out to be very useful. As you’ll see for yourself, process is quite extensive and somewhat complicated, especially if you are not an Apple user and not familiar with the system.
Once we have a valid certificate we can focus on sending our messages to APNS. As I mentioned before, all data needs to be written to the socket in a binary format. According to the documentation APNS supports three binary notification formats: legacy format, enhanced format and new binary interface. We ended up formating our messages in legacy notification format which has the following structure:

apple_push_notifications_legacy_format

If you are just starting your work on this matter you should definitely go with the new binary interface and format your messages that way. You can find all details about it in the documentation. We’ll eventually have to refactor our extension to adopt this new standard, but the legacy one works just fine for now.

Following php code can be used to do the formating:

$payload = array(
     'aps'    => array(
     'alert'    => array(
          'title'    => 'your_website.com',
          'body'   => 'message_text',
     ),
     'url-args'   => array(
          'some_url'
     )
  )
);
 
$encodedPayload = json_encode($payload);
 
$binaryMessage = chr(0).
      chr(0).
      chr(32).
      pack('H*', $deviceToken).
      chr(0).chr(strlen($encodedPayload)).
      $encodedPayload;

Once message is converted to binary format we can send it to APNS by writing to the socket:

fwrite($socketClient,  $binaryMessage);

At this point it is up to APNS to deliver message to the end user.

› Message delivery

Once push notification reaches a device it somehow needs to be displayed to the user.

On Apple’s OS X devices this process is handled automatically since the whole push notification system is integrated in the operating system itself. That basically means that if you format your notification message correctly you don’t have to do anything to display it to user, it all happens automatically. For mobile applications on iOS and Android there are certain things that need to be implemented on the app side but I won’t go into it since I’m not competent to talk about it. But what I will show you is how to display a notification in Chrome.

As mentioned before, Chrome uses service workers to display a notification message.

When service worker is registered with Chrome, it is registered for a specific domain. This way, when GCM pings our browser it checks whether there is a service worker associated with the domain from which the notification is coming. If so, Chrome dispatches a push event which gets picked up by a service worker which does the rest of the work.

In order for a service worker to actually show notification we need to fill it in with the necessary javascript code. Here’s a code sample from our worker file:

self.addEventListener('push', function(event) {
     event.waitUntil(
 
     self.registration.pushManager.getSubscription().then(function(subscription) {
          var fetchUrl = "https://magento.store.com/get_chrome_push_data_from_file";
          fetch(fetchUrl).then(function (response) {
 
               if (response.status !== 200) {
                   console.log('Looks like there was a problem. Status Code: ' + response.status);
                   throw new Error();
               }
 
               // Examine the text in the response
               return response.json().then(function (data) {
                    if (data.error || !data.notification) {
                         console.error('The API returned an error.', data.error);
                         throw new Error();
                    }
 
                    if(data.status == 1) {
                         var title = data.notification.title;
                         var text = data.notification.text;
                         var icon = data.notification.icon;
                         var notificationTag = data.notification.tag;
                         var url = data.notification.url;
 
                         return self.registration.showNotification(title, {
                              body: text,
                              icon: icon,
                              tag: notificationTag,
                              data: {
                                   url : url
                              }
                         });
                    }
               });
          }).catch(function (err) {
               console.error('Unable to retrieve data', err);
          })
     })
);
});

If you take a closer look at the code you’ll notice something strange. We are grabbing notification data from remote url instead of getting it from the event object. Why are we doing that? Well, as I said in the first few lines of this article, for some channels push technology if not quite ready it terms of development. Chrome is exactly what I had in mind by saying that. At the moment GCM is not delivering message payload to Chrome. The reason for this is that in a future implementation, payload data will have to be encrypted before it is sent to a GCM. So basically, you can send payload in your curl request but it won’t be delivered to the browser. This kind of makes Chrome push notifications useless, or at least very limited in terms of functionality.

To overcome this problem and ensure at least some kind of Chrome push functionality for our client, we decided to grab notification data from a file on our server. File is populated with the data of the last Chrome message that gets scheduled from Magento admin.

The most important piece of code is obviously the one that shows the notification:

return self.registration.showNotification(title, {
     body: text,
     icon: icon,
     tag: notificationTag,
     data: {
          url : url
     }
});

If we want to open an url when user clicks on the notification we need to specify it with a bit of code, also in the service worker file:

self.addEventListener('notificationclick', function(event) {
     var url = event.notification.data.url;
     clients.openWindow(url);
});

Unsubscribe?

Yes, it’s possible.

I didn’t cover it in this post since I wanted to focus on initial subscription process and show you how to get up and running with this whole push notification thing.

Obviously, you can unsubscribe by going into your browser settings and block push notifications for a particular site. That’s one way of doing it. Aggressive, if you wish.

If you want more elegant way then you can add an interface to your site which customer can use to subscribe or unsubscribe from notifications. In that case you would need some more javascript code which would manipulate with the subscription (remove token from server or change its status) and update user interface (a button of some sort) depending on whether user is subscribed or not.

Documentations have some nice examples on how it can be done.

 
Phew…that’s it guys.

This one was a beast to write. I know, it’s a long one, but there are just so many things to cover. Hope you managed to follow along. I tried my best to be as concise as possible. If you find any mistakes or think that something is missing please let me know.

Also, here are some useful links for better understanding of some things that were mentioned in this post:

Happy coding 🙂 !

You made it all the way down here so you must have enjoyed this post! You may also like:

Enter HTTPS Sasa Brankovic
Sasa Brankovic, | 4

Enter HTTPS

Enabling Multi-part MIME Emails in Magento Tomislav Nikcevski
Tomislav Nikcevski, | 3

Enabling Multi-part MIME Emails in Magento

Pimcore Portlets Zoran Salamun
Zoran Salamun, | 0

Pimcore Portlets

15 comments

  1. magento 2 web push notifications using google firebase.
    any free download link Web Push Notification for Magento 2

  2. Thanks for the info Kresimir, I recently just got into the world of eCommerce using Magento platform. I would like to know if there is a way for the one handling orders (at the backend) to receive push notifications when an order has been made on the eCommerce site.

  3. Wow Kresimir, you have summed up push notifications in great detail. We have built a Magento extension which does this automatically even for non HTTPS Magento stores. I hope when the plugin is live you will like it.

    Just read your great blog post a day before our extension going live is great. It validates the idea and yes, it has limitless opportunities.

    On a side note on CentOS 6.5 the default Magento API caller doesn’t work, did you come across that issue? We ended up using FSOCK to solve it.

Leave a Reply

Your email address will not be published. Required fields are marked *

You may use these HTML tags and attributes: <a href="" title=""> <blockquote cite=""> <code> <del datetime=""> <em> <s> <strike> <strong>. You may use following syntax for source code: <pre><code>$current = "Inchoo";</code></pre>.

Tell us about your project

Drop us a line. We'd love to know more about your project.