Send Safari Push Notifications to your Mac users using Sitecore – part 1

PushNotifications

Push notifications have been around for a while and we have all experienced them in our phones and tablets. Sending notifications to web users have also been possible but the browser and the website must be active.

But did you know that you can send push notifications to your web users even when the browser is closed? It’s called Safari Push Notifications and was presented when Apple released their latest OS – Maverick. Unfortunately it only works on Safari running on Maverick(basically every mac user). It’s still a huge thing and I would not be surprised if it will be supported in the next IOS version.

I was thinking of showing you guys how to prepare your website for Safari Push Notifications and in the next post I will show you how to send push notifications by using the best CMS platform ever – Sitecore 🙂

To make it work we need to do following steps:

  1. Generate Certificate(.p12)
  2. Webservice/restapi
    When a webpage requests permission to display push notifications, an HTTP request for your credentials is sent to your web server. Similarly, when a user changes their website push notification settings in Safari or System Preferences, an HTTP request is sent to your web server. You need to configure a RESTful web service on your server to respond to these requests accordingly. The web service does not need to be hosted on the same server(s) or domain(s) that serve your webpages.
  3. Build a Pushpackage
    When the website asks user for permission to send push notifications, Safari will ask your server for a push package.The package contains data that is used by the notification UI, such as your website name and icon, as well as a cryptographic signature
  4. Implement javascript
    There are two important JavaScript functions to keep in mind when dealing with push notifications. The first is a lightweight function that checks the user’s permission level without talking to the server. The second contacts with the server and displays the permission dialog to the user

I created a module, PushBroker, in Sitecore to hold data and configurations for the push notifications.

SitecoreApp

Certificate

Register the website in the Apple’s dev center and generate a certificate. You need to have a iOS developer license or Mac developer license.

Create a unique website push ID to distinguish your site from others. 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”.
RegisterAppId

After you’ve registered your Website Push ID, you’re ready to generate a certificate/private key pair used to encrypt your communication with the APN (Apple Push Notification) service.
RegisterAppId2

RegisterAppId3

Download the certificate and import it(double-click the 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 file.

That’s it. Now we have a working certificate let’s upload it in the PushBroker manager

SitecoreApp

Web service/rest api

The web service/rest api must be under HTTPS protocol. We need to have following rest methods:

  • Download the Pushpackage
    When a user allows permission to receive push notifications, a POST request is sent to the following URL:

    webServiceURL/version/pushPackages/websitePushID
  • Registering or Updating Device Permission Policy
    When users first grant permission, or later change their permission levels for your website, a POST request is sent to the following URL:

    webServiceURL/version/devices/deviceToken/registrations/websitePushID
  • Forgetting Device Permission Policy
    If a user removes permission of a website in Safari preferences, a DELETE request is sent to the following URL:

    webServiceURL/version/devices/deviceToken/registrations/websitePushID
  • Logging Errors
    If an error occurs, a POST request is sent to the following URL:

    webServiceURL/version/log

    This one is crucial this is how Apple communicates with us. If something goes wrong in downloading your push package or delivery of your push notifications, the logging endpoint on your web service will be contacted with an error message describing the error.

I started to build a rest service but then I changed my mind. Why not use something that is already working in Sitecore – the Sitecore Item Web Api. I found some really good posts from Mike Reynolds(sitecorejunkie) about adding custom <itemWebApiRequest> pipeline processors.

I did a base class which inherits the Sitecore.ItemWebApi.Pipelines.Request.RequestProcessor.

namespace GH.Framework.PushBroker.Pipelines
{
    public abstract class CustomizedActions : RequestProcessor
    {
        protected bool HasMethodRequest(string method)
        {
            return IsACustomizedActionsRequest() && HttpContext.Current.Request.Url.Segments.Any(s => s.ToLower().StartsWith(method));
        }


        
        protected string ExtraxtJsonFromRequest()
        {
            string jsonContents;
            using (Stream receiveStream = HttpContext.Current.Request.InputStream)
            {
                using (StreamReader readStream = new StreamReader(receiveStream, Encoding.UTF8))
                {
                    jsonContents = readStream.ReadToEnd();
                }
            }
            return jsonContents;
        }

        protected string ExtractPushIdFromUrl(RequestArgs requestArgs, string action)
        {
            for (int i = 0; i < requestArgs.Context.HttpContext.Request.Url.Segments.Length - 1; i++)
            {
                if (!requestArgs.Context.HttpContext.Request.Url.Segments[i].ToLower().StartsWith(action))
                    continue;

                return requestArgs.Context.HttpContext.Request.Url.Segments[i + 1].TrimEnd('/');
            }

            return string.Empty;

        }

        protected void Error()
        {
            HttpContext.Current.Response.StatusCode = 500;
            HttpContext.Current.Response.End();
        }

        protected void ReportSuccess()
        {
            HttpContext.Current.Response.StatusCode = 200;
            HttpContext.Current.Response.End();
        }

        private static bool IsACustomizedActionsRequest()
        {

            string[] segments = HttpContext.Current.Request.Url.Segments;
            for (int index = segments.Length - 1; index > 0; --index)
            {
                if (string.Equals(segments[index], "customized/", StringComparison.InvariantCultureIgnoreCase) && string.Equals(segments[index - 1], "-/", StringComparison.InvariantCultureIgnoreCase))
                    return true;
            }
            return false;
        }
    }
}
http://mywebsite.net/en/-/item/v1/-/customized/log

A custom <itemWebApiRequest> pipeline processor for “Logging Errors”

namespace GH.Framework.PushBroker.Pipelines
{
    public class ResolveSafariLogAction : CustomizedActions
    {
        public override void Process(RequestArgs requestArgs)
        {
            Assert.ArgumentNotNull(requestArgs, "requestArgs");


            if (!HasMethodRequest(RestMethods.Log.ToString().ToLower()))
                return;

            AppleSays();


        }

        private void AppleSays()
        {
            string jsonContents = ExtraxtJsonFromRequest();
            Logger.Info("Apple says: " + jsonContents);
        }

    }
}

http://mywebsite.net/en/-/item/v1/-/customized/devices/deviceToken/registrations/websitePushID

A custom <itemWebApiRequest> pipeline processor for “Registering or Updating Device Permission Policy” and “Remove permissions”.

namespace GH.Framework.PushBroker.Pipelines
{
    public class ResolveSafariSubscribeDeviceAction : CustomizedActions
    {
        public override void Process(RequestArgs requestArgs)
        {
            Assert.ArgumentNotNull(requestArgs, "requestArgs");


           if (!HasMethodRequest(RestMethods.Devices.ToString().ToLower()))
                return;


            Subscribe(requestArgs);

        }

        private void Subscribe(RequestArgs requestArgs)
        {
            string jsonContents = ExtraxtJsonFromRequest();

            string language = Language.Current.ToString();

            string pushId = ExtractPushIdFromUrl(requestArgs, RestMethods.Devices.ToString().ToLower());

            if (string.IsNullOrWhiteSpace(pushId))
            {
                Error();
                return;
            }

            Model.Application appleApplication = ApplicationRepository.Get(pushId, Language.Parse(language));

            if (appleApplication == null)
            {
                Error();
                return;
            }


            string deviceTooken = ExtractDeviceTookenFromUrl();

          
            if (string.IsNullOrWhiteSpace(deviceTooken))
            {
                Error();
                return;
            }

            if (requestArgs.Context.HttpContext.Request.HttpMethod == "POST")
            {
                AppleReceiverRepository.Update(appleApplication.Id, deviceTooken, true, string.Empty, string.Empty,
                     Language.Current);
            }

            if (requestArgs.Context.HttpContext.Request.HttpMethod == "DELETE")
            {
                AppleReceiverRepository.Update(appleApplication.Id, deviceTooken, false, string.Empty, string.Empty,
                     Language.Current);
            }

        }
        
        private string ExtractDeviceTookenFromUrl()
        {
            for (int i = 0; i < HttpContext.Current.Request.Url.Segments.Length - 1; i++)
            {
                if (!HttpContext.Current.Request.Url.Segments[i].StartsWith("devices"))
                    continue;

                return HttpContext.Current.Request.Url.Segments[i + 1].TrimEnd('/');
            }

            return string.Empty;

        }
       
    }
}

If the request is a “post” method “AppleReceiverRepository.Update” will store the receiver id(deviceToken) in the PushBroker manager. In the next post I will show you how to send notifications using the receiver id’s.
StoreReceiver

http://mywebsite.net/en/-/item/v1/-/customized/pushpackages/websitePushID

Finally a <itemWebApiRequest> pipeline processor for “Download the Pushpackage”.

namespace GH.Framework.PushBroker.Pipelines
{
    public class ResolveSafariPushPackageAction : CustomizedActions
    {
        public override void Process(RequestArgs requestArgs)
        {
            Assert.ArgumentNotNull(requestArgs, "requestArgs");


            if (!HasMethodRequest(RestMethods.Pushpackages.ToString().ToLower()))
                return;

            GetPushPackage(requestArgs);


        }

        private void GetPushPackage(RequestArgs requestArgs)
        {
            string jsonContents = ExtraxtJsonFromRequest();

            string language = Language.Current.ToString();

            string pushId = ExtractPushIdFromUrl(requestArgs, RestMethods.Pushpackages.ToString().ToLower());

            if (string.IsNullOrWhiteSpace(pushId))
            {
                Error();
                return;
            }

            Model.Application appleApplication = ApplicationRepository.Get(pushId, Language.Parse(language));

            if (appleApplication == null)
            {
                Error();
                return;
            }

            PushPackageService pushPackageService = new PushPackageService();
            System.IO.FileInfo zipFileInfo = pushPackageService.GeneratePackage((AppleWebSettings)appleApplication.ApplicationSettings,
               requestArgs.Context.HttpContext.Response, HttpContext.Current.Server.MapPath("~/Components/Pushpackage"),
               HttpContext.Current.Server.MapPath("~/Components/"));

            requestArgs.Context.HttpContext.Response.Clear();
            requestArgs.Context.HttpContext.Response.ContentType = @"application/zip";
            requestArgs.Context.HttpContext.Response.AddHeader("Content-Disposition", "attachment; filename=" + zipFileInfo.Name);
            requestArgs.Context.HttpContext.Response.AddHeader("Content-Length", zipFileInfo.Length.ToString(CultureInfo.InvariantCulture));
            requestArgs.Context.HttpContext.Response.WriteFile(zipFileInfo.FullName);
      
        }

    }
}

Here is the configuration:

<?xml version="1.0" encoding="utf-8"?>
<configuration xmlns:patch="http://www.sitecore.net/xmlconfig/">
  <sitecore>
    <pipelines>
      <itemWebApiRequest>
        <processor patch:before="*[@type='Sitecore.ItemWebApi.Pipelines.Request.ResolveAction, Sitecore.ItemWebApi']"
                   type="GH.Framework.PushBroker.Pipelines.ResolveSafariPushPackageAction, GH.Framework.PushBroker" />
        <processor patch:before="*[@type='Sitecore.ItemWebApi.Pipelines.Request.ResolveAction, Sitecore.ItemWebApi']"
                   type="GH.Framework.PushBroker.Pipelines.ResolveSafariSubscribeDeviceAction, GH.Framework.PushBroker" />
        <processor patch:before="*[@type='Sitecore.ItemWebApi.Pipelines.Request.ResolveAction, Sitecore.ItemWebApi']"
                   type="GH.Framework.PushBroker.Pipelines.ResolveSafariLogAction, GH.Framework.PushBroker" /> 
      </itemWebApiRequest>
    </pipelines>
  </sitecore>
</configuration>

Pushpackage

When the 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
Website.json

The json file contains following information (example):

{  
    "websiteName": "Name of website",  
    "websitePushID": "web.net.testsite",  
    "allowedDomains": ["http://testsite.net"],  
    "urlFormatString": "http://testsite.net/%@/",  
    "authenticationToken": "19f8d7a6e9fb8a7f6d9330dabe",  
    "webServiceURL": "https://testsite.net/en/-/item/v1/-/customize"  
}

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.

Iconset

The iconset is a directory that contains PNG images in varying sizes. The images in your iconset populate the icons displayed to the user in the permission prompt, Notification Center, and the notification itself.

Manifest.json

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

{
"icon.iconset/icon_16x16.png":"E675CB16E81C66A17C0F4F4446727209B9A38CE0",
"icon.iconset/icon_16x16@2x.png":"920249E14C2DC15E7FA98E86D972339067305BD0",
"icon.iconset/icon_32x32.png":"920249E14C2DC15E7FA98E86D972339067305BD",
"icon.iconset/icon_32x32@2x.png":"067FED298843E3C84F036E15A0C171B7E5578C21",
"icon.iconset/icon_128x128.png":"F42604358D13A784B76DC0B30EF3D87EFEB5FDE9",
"icon.iconset/icon_128x128@2x.png":"5FE1CD64884D4B7ACBBCDA838C43EBC14654F627",
"website.json":"DA39A3EE5E6B4B0D3255BFEF95601890AFD80709"
}
Signature

The signature is a PKCS #7 detached signature of the manifest file.

Wrapping it up

How do we generate the pushpackage? First we need to put some data in the PushBroker manager.
appsettings

And the images:
Icons

Everything is set for generating the pushpackage. 🙂 This part was really hard, especially signing with a certificate.

namespace GH.Framework.PushBroker.Infrastructure
{
    public class PushPackageService
    {

        private static IEnumerable<string> PushPackageFiles
        {
            get
            {
                return new string[]{
                "icon.iconset/icon_16x16.png",
                "icon.iconset/icon_16x16@2x.png",
                "icon.iconset/icon_32x32.png",
                "icon.iconset/icon_32x32@2x.png",
                "icon.iconset/icon_128x128.png",
                "icon.iconset/icon_128x128@2x.png",
                "website.json" 
                };
            }
        }

        public FileInfo GeneratePackage(AppleWebSettings appleWebSettings,
            HttpResponse currentResponse, string packageRoot, string zipPath)
        {
            GenerateIconToPushPackageService generateIconToPushPackageService = new GenerateIconToPushPackageService();

            GenerateWebsiteJsonToPushPackageService generateWebsiteJsonToPushPackageService = new GenerateWebsiteJsonToPushPackageService();

            if (!Directory.Exists(Path.Combine(packageRoot, "icon.iconset")))
                Directory.CreateDirectory(Path.Combine(packageRoot, "icon.iconset"));

            foreach (string fileName in PushPackageFiles)
            {

                if (fileName.StartsWith("icon"))
                    generateIconToPushPackageService.GenerateIcon(appleWebSettings, currentResponse, Path.Combine(packageRoot,fileName));

                if (fileName.StartsWith("website"))
                    generateWebsiteJsonToPushPackageService.GenerateWebsiteJson(appleWebSettings, Path.Combine(packageRoot, fileName));
          
            }
            
            GenerateManifestToPushPackageService generateManifestToPushPackageService = new GenerateManifestToPushPackageService();
            generateManifestToPushPackageService.GenerateManifest(packageRoot, PushPackageFiles);

            GenerateSignatureToPushPackageService generateSignatureToPushPackageService = new GenerateSignatureRoPushPackageService();
            generateSignatureToPushPackageService.GenerateSignature(appleWebSettings, HttpContext.Current, packageRoot);
         
            if (File.Exists(Path.Combine(zipPath, string.Format("{0}.pushpackage.zip",appleWebSettings.WebsitePushName))))
                File.Delete(Path.Combine(zipPath, string.Format("{0}.pushpackage.zip", appleWebSettings.WebsitePushName)));

            ZipFile.CreateFromDirectory(packageRoot, Path.Combine(zipPath, string.Format("{0}.pushpackage.zip", appleWebSettings.WebsitePushName)));

            return new System.IO.FileInfo(Path.Combine(zipPath, string.Format("{0}.pushpackage.zip", appleWebSettings.WebsitePushName)));
        }

       
    }
}

GenerateIconToPushPackageService.GenerateIcon writes the images(icons) to disk.

namespace GH.Framework.PushBroker.Infrastructure
{
    public class GenerateIconToPushPackageService
    {
        public void GenerateIcon(AppleWebSettings appleWebSettings, HttpResponse currentResponse, string filePath)
        {
            WebClient webClient = new WebClient();

            IImage iconImage = GetIconImage(filePath, appleWebSettings);

            Assert.IsNotNull(iconImage, "Image {0} is missing", filePath);

            byte[] imageData =
                webClient.DownloadData(string.Concat("http://", HttpContext.Current.Request.Url.Host,
                   iconImage.Source));

            using (System.IO.FileStream fs = new FileStream(filePath, FileMode.OpenOrCreate))
            {
                fs.Write(imageData, 0, imageData.Length);
            }
        }

        private static IImage GetIconImage(string filePath, AppleWebSettings appleWebSettings)
        {
            if (filePath.EndsWith("16x16.png"))
                return appleWebSettings.Icon16;

            if (filePath.EndsWith("16x16@2x.png"))
                return appleWebSettings.Icon16x2;

            if (filePath.EndsWith("32x32.png"))
                return appleWebSettings.Icon32;

            if (filePath.EndsWith("32x32@2x.png"))
                return appleWebSettings.Icon32x2;

            if (filePath.EndsWith("128x128.png"))
                return appleWebSettings.Icon128;

            if (filePath.EndsWith("128x128@2x.png"))
                return appleWebSettings.Icon128x2;

            return null;
        }
    }
}

GenerateWebsiteJsonToPushPackageService.GenerateWebsiteJson writes the “website.json” to disk.

namespace GH.Framework.PushBroker.Infrastructure
{
    public class GenerateWebsiteJsonToPushPackageService
    {
        public void GenerateWebsiteJson(AppleWebSettings appleWebSettings, string filePath)
        {
            using (System.IO.FileStream fs = new FileStream(filePath, FileMode.Create))
            {
                using (StreamWriter streamWriter = new StreamWriter(fs))
                {
                    streamWriter.Write("{");
                    streamWriter.WriteLine();

                    streamWriter.Write(@"""websiteName"": ""{0}"", ", appleWebSettings.WebsitePushName);
                    streamWriter.WriteLine();

                    streamWriter.Write(@"""websitePushID"": ""{0}"", ", appleWebSettings.WebsitePushId);
                    streamWriter.WriteLine();

                    streamWriter.Write(@"""allowedDomains"": {0} ,", ConvertToJsonService.Convert(appleWebSettings.AllowedDomains.Where(s => !string.IsNullOrWhiteSpace(s))));
                    streamWriter.WriteLine();

                    streamWriter.Write(@"""urlFormatString"": ""{0}"", ", appleWebSettings.UrlFormatString);
                    streamWriter.WriteLine();

                    streamWriter.Write(@"""authenticationToken"": ""authenticationToken_{0}"", ", appleWebSettings.AuthenticationToken);
                    streamWriter.WriteLine();

                    streamWriter.Write(@"""webServiceURL"": ""{0}"" ", appleWebSettings.WebServiceUrl);
                    streamWriter.WriteLine();

                    streamWriter.Write("}");
                    streamWriter.WriteLine();
                   
                }
            }
        }
    }
}

GenerateManifestToPushPackageService.GenerateManifest writes the “manifest.jon” to disk. (Getting correct SHA1 checksum was indeed tricky)

namespace GH.Framework.PushBroker.Infrastructure
{
    public class GenerateManifestToPushPackageService
    {
        public void GenerateManifest(string folderPath, IEnumerable<string> pushPackageFiles)
        {
            Dictionary<string, string> contentDictionary = CreateContentDictionary(folderPath, pushPackageFiles);

            CreateManifestFile(folderPath, contentDictionary);
        }

        private static void CreateManifestFile(string folderPath, Dictionary<string, string> contentDictionary)
        {
            string jsonString = JsonConvert.SerializeObject(contentDictionary, Formatting.Indented);

            using (FileStream fs = File.Create(Path.Combine(folderPath, "manifest.json")))
            {
                Byte[] content = new UTF8Encoding(true).GetBytes(jsonString);
                fs.Write(content, 0, content.Length);
            }

        }

        private static Dictionary<string, string> CreateContentDictionary(string folderPath, IEnumerable<string> pushPackageFiles)
        {
            Dictionary<string, string> fileDictionary = new Dictionary<string, string>();

            foreach (string fileName in pushPackageFiles)
            {
                StringBuilder hashDataBuilder;
                try
                {
                    using (FileStream fs = new FileStream(Path.Combine(folderPath, fileName), FileMode.Open))
                    {
                        using (BufferedStream bs = new BufferedStream(fs))
                        {
                            using (SHA1Managed sha1 = new SHA1Managed())
                            {
                                byte[] hash = sha1.ComputeHash(bs);
                                hashDataBuilder = new StringBuilder(2 * hash.Length);
                                foreach (byte b in hash)
                                {
                                    hashDataBuilder.AppendFormat("{0:X2}", b);
                                }
                            }
                        }
                    }
                }
                catch (IOException ex)
                {
                    throw new Exception("Problem creating hash from files in push package" + " Error: " + ex.Message +
                                        " Stack: " + ex.StackTrace);
                }

                fileDictionary.Add(fileName, hashDataBuilder.ToString());
            }

            return fileDictionary;
        }
    }
}

GenerateSignatureRoPushPackageService.GenerateSignature writes the “signature” to disk. This one was not easy at all. Loading the certificate and get the signing to work properly.

namespace GH.Framework.PushBroker.Infrastructure
{
    public class GenerateSignatureToPushPackageService
    {
        public void GenerateSignature(AppleWebSettings appleWebSettings, HttpContext httpContext, string folderPath)
        {
           string manifest = File.ReadAllText(Path.Combine(folderPath, "manifest.json"));
           
            Assert.IsNotNullOrEmpty(manifest, "Cannot create signature of manifest.json. File does not exist");
            
            WebClient webClient = new WebClient();

            byte[] appleCert =
                webClient.DownloadData(string.Concat("http://", httpContext.Request.Url.Host,
                    appleWebSettings.CertificationFile.Source));

            Byte[] content = new UTF8Encoding(true).GetBytes(manifest);
            
            CreateSignatureFile(appleWebSettings, folderPath, content, appleCert);
        }

        private static void CreateSignatureFile(AppleWebSettings appleWebSettings, string folderPath, byte[] content,
            byte[] appleCert)
        {
            try
            {
                ContentInfo contentInfo = new ContentInfo(content);
                
                SignedCms signedCMS = new SignedCms(contentInfo, true);
                
                //Create detached pck#7 signature using cert obtained from Apple Developer Network
                System.Security.Cryptography.X509Certificates.X509Certificate2 cert =
                    new System.Security.Cryptography.X509Certificates.X509Certificate2(appleCert,
                        appleWebSettings.CertificationPassword,
                        System.Security.Cryptography.X509Certificates.X509KeyStorageFlags.MachineKeySet |
                        System.Security.Cryptography.X509Certificates.X509KeyStorageFlags.PersistKeySet |
                        System.Security.Cryptography.X509Certificates.X509KeyStorageFlags.Exportable);


                CmsSigner signer = new CmsSigner(cert);
                
                signer.IncludeOption = X509IncludeOption.EndCertOnly;
                
                signedCMS.ComputeSignature(signer);


                using (FileStream fs = File.Create(Path.Combine(folderPath, "signature")))
                {
                    Byte[] contentToWrite = signedCMS.Encode();
                    fs.Write(contentToWrite, 0, contentToWrite.Length);
                }

            }
            catch (CryptographicException ex)
            {
                throw new Exception("Problem creating signature" + " Error: " + ex.Message + " Stack: " +
                                    ex.StackTrace);
            }
            catch (IOException ex)
            {
                throw new Exception("Problem writing signature to file" + " Error: " + ex.Message + " Stack: " +
                                    ex.StackTrace);
            }
        }
    }
}

That’s the pushpackage what’s left is now the client script.

Implement javascript

There are two important JavaScript functions to keep in mind when dealing with push notifications. The first is a lightweight function that checks the user’s permission level without talking to the server. The second contacts with the server and displays the permission dialog to the user.

The datacontainer holding data for the javascript

<div class="notificationSettings" 
   data-restURL="<%# Sitecore.Context.Database.GetItem(Constants.Templates.AppleWebApplication).GetReferrersAsItems().FirstOrDefault().GetString(Constants.Fields.AppleWebApplication.AppleWebSettingsWebServiceURL) %>"
   data-pushId="<%# Sitecore.Context.Database.GetItem(Constants.Templates.AppleWebApplication).GetReferrersAsItems().FirstOrDefault().GetString(Constants.Fields.AppleWebApplication.AppleWebSettingsPushId) %>" >
</div>

The javascript…

var PushBroker = PushBroker || {};


jQuery(document).ready(function () {
    PushBroker.SafariNotifications.DomReady();
});


PushBroker.SafariNotifications = {
    DomReady: function() {

        var safariNotifications = new PushBroker.SafariNotifications.Init();

        safariNotifications.prepareForPushNotification();

    },
    Init: function() {

        var self = this;

        self.dataContainer = jQuery(".notificationSettings");

        self.prepareForPushNotification = function () {
            "use strict";

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

        self.checkRemotePermission = function (permissionData) {
            "use strict";

            if (permissionData.permission === 'default') {
                console.log("The user is making a decision");
                window.safari.pushNotification.requestPermission(
                    self.dataContainer.data("restURL"),
                    self.dataContainer.data("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);
            }
        };

    }
}

That’s it guys, now we have a website ready to send Safari Push Notifications to all Mac users out there 🙂
cnn-notification

That’s all for now folks 🙂


2 thoughts on “Send Safari Push Notifications to your Mac users using Sitecore – part 1

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s

This site uses Akismet to reduce spam. Learn how your comment data is processed.