Web Push Notification Using OneSignal Into Umbraco

During the Christmas of 2009 I had asked myself, is it worth it to hang with Umbraco where there is no users at all? Now it's Christmas of 2019 With Small Umbraco Community. Still Enjoying, Exploring and Coding With Umbraco

If you want to keep your consumer engaged with your website or app and never want to let them to miss new updates then first thing that hits your mind is sending notifications. Be it social media promotion, be it email newsletter, push notification or any other channels that fits your requirements, the goal is to keep updating your consumers about what's going around your website or app. Popularity of push notification as a communication channel is growing every day. This can be even defined as a micro marketing method also.

Today I am going to share how I have done web push notification using OneSignal with Umbraco.

Quick Dive into OneSignal

I found OneSignal is easy to use in terms of setup and development. It's available for websites and mobile applications which supports all major native and mobile platforms. Availability of REST API and well explained documentation also makes easier to implement. Another reason is it's free plan that includes 30K web subscribers and unlimited subscribes in mobile. Number of push notifications can be sent in free plan is unlimited. A/B testing and real-time reporting dashboard are another key features included in their free plan.

Dashboard.png
Report.png

Steps to Take in OneSignal

  1. Create a free account

    OneSignal free account can be create from their signup page. Just fill up the required detail or use any external methods then you are all set to go.
  2. Create an application

    After completing registration, first thing you need to consider is to add application from dashboard. Each app comes with unique app id and REST API key which is needed for subscription and send notifications. You can find more details about creating an application here.
    App Dashboard.png
  3. Configure an application

    You can give your site name, URL of your website or app details, configure the notification styling in configuration section. Details about how to configure an application can be found here.
    configuration.png

    Prompt.png

How It Works in Umbraco?

Before we start coding there are 2 things you need to consider which is required to enable OneSignal in your site.

  • Adding OneSignal SDK files

    SDK file can be download from here. Unzip the SDK files and upload the SDK files (OneSignalSDKWorker.js and OneSignalSDKUpdaterWorker.js) to the root directory of your site. Those SDK files should be accessible publicly.
  • Adding OneSignal code to site

    At the end of the configuration steps in OneSignal, you will get code snippets that you have to include in your website. That snippet has to place inside the <head> tag.
    OneSignalCode.png


    After completing above steps, when you browse your site you will asked for notification permission.
    Notification Access.png

Settings in Umbraco

Pushing data from Umbraco to OneSignal is done by subscribing Published event. I will be describing about publish event code a bit later but first let’s look into the important settings that has to add in web.config file's  appSettings section.

<add key="PushNotificationOnPublish" value="false"/>
<add key="PushNotificationAllowedTypes" value="blogpost,notificationPost"/>
<add key="PushNotificationTitleProperty" value="pageTitle"/>
<add key="PushNotificationContentProperty" value="excerpt"/>
<add key="OneSignal:appId" value="6f68b2ff-c68b-4551-bbbb-e0fc9432b0c0"/>
<add key="OneSignal:apiKey" value="NTI5ZWQzNDUtMzViYS00MjFjLTllODUtMjM2YzIxN2Q4MTcx"/>
<add key="OneSignal:restUrl" value="https://onesignal.com/api/v1/notifications"/>

PushNotificationOnPublish controls the behavior of pushing content from Umbraco to OneSignal. If value is set to true then notification sent each time when content is published. If this is set to false then notification is sent when property Send Push Notification (alias: sendPushNotification) is set to true. After notification sent, this property is set to false automatically to stop notification being sent again for same content node.

PushNotificationAllowedTypes controls which type of content nodes will be use to send push notification. You can add your document type alias with the separation of comma. 

PushNotificationTitleProperty property's value will be use as title of the push notification where as PushNotificationContentProperty property's value will be use as body content of the push notification.

Rest 3 settings OneSignal:appId, OneSignal:apiKey and OneSignal:restUrl holds APP Id, API key and REST API URL of OneSignal

So now let’s discuss about the code that push Umbarco content to OneSignal. Here I have created a class file that inherits IComponent to subscribe Published event inside a Component.

public class PushNotificationComponent: IComponent
{
	private readonly ILogger _logger;
	private readonly IContentService _contentService;
	private readonly IUmbracoContextFactory _umbracoContext;
	private UmbracoContext _context;

	public PushNotificationComponent(ILogger logger, IContentService contentService, IUmbracoContextFactory umbracoContext)
	{
		_logger = logger;
		_contentService = contentService;
		_umbracoContext = umbracoContext;
	}

	public void Initialize()
	{
		
	}		

	public void Terminate()
	{
		
	}
}

You can see I have used IContentService and IUmbracoContextFactory in constructor. IContentService is used to publish the content node after reset the value of Send Push Notification property to false. And IUmbracoContextFactory is used to get the URL of published node to send to Onesignal. 

And here is the code that reads all the setting from web.config

private bool SendPushNotificationOnPublish { get; } = bool.Parse(ConfigurationManager.AppSettings["PushNotificationOnPublish"]);

private string AppId { get; } = ConfigurationManager.AppSettings["OneSignal:appId"];

private string ApiKey { get; } = ConfigurationManager.AppSettings["OneSignal:apiKey"];

private string ApiUrl { get; } = ConfigurationManager.AppSettings["OneSignal:restUrl"];

private string TitleProperty { get; } = ConfigurationManager.AppSettings["PushNotificationTitleProperty"];

private string ContentProperty { get; } = ConfigurationManager.AppSettings["PushNotificationContentProperty"];

private bool IsAllowedDocType(string documentTypeAlias)
{
	var allowedDocumentTypes = ConfigurationManager.AppSettings["PushNotificationAllowedTypes"];
	return allowedDocumentTypes.Contains(documentTypeAlias);
}

 Finally we are in the main code block that fires when content node is published.

public void Initialize()
{
	ContentService.Published += ContentService_Published;
}

private void ContentService_Published(IContentService sender, Umbraco.Core.Events.ContentPublishedEventArgs e)
{
	using (var contextReference = _umbracoContext.EnsureUmbracoContext())
	{
		_context = contextReference.UmbracoContext;
	}

	foreach (var item in e.PublishedEntities)
	{
		if (IsAllowedDocType(item.ContentType.Alias))
		{
			var title = item.GetValue(TitleProperty) != null ? item.GetValue<string>(TitleProperty) : item.Name;
			var content = item.GetValue(ContentProperty) != null ? item.GetValue<string>(ContentProperty) : item.Name;
			var url = "";

			if (_context != null)
			{
				var publishedEntity = _context.Content.GetById(item.Id);
				url = publishedEntity.Url(mode: UrlMode.Absolute);
			}


			var segments = new string[] { "All" };

			if (SendPushNotificationOnPublish)
			{
				if (PushNotification(title, content, segments, url))
				{
					_logger.Info<PushNotificationComponent>(
						$"Notification sent successfully. Page Details : '{title}' ('{item.Id}')");
				}
				else
				{
					_logger.Warn<PushNotificationComponent>(
						$"Notification send failed. Page Details : '{title}' ('{item.Id}')");
				}
			}
			else
			{
				if (item.GetValue<bool>("sendPushNotification"))
				{
					if (PushNotification(title, content, segments, url))
					{
						item.SetValue("sendPushNotification", false);
						_contentService.SaveAndPublish(item, raiseEvents: false);
						_logger.Info<PushNotificationComponent>(
							$"Notification sent successfully. Page Details : '{title}' ('{item.Id}')");
					}
					else
					{
						_logger.Warn<PushNotificationComponent>(
							$"Notification send failed. Page Details : '{title}' ('{item.Id}')");
					}
				}
			}
		}
	}
}

private bool PushNotification(string title, string content, string[] segments, string link)
{
	try
	{
		var request = WebRequest.Create($"{ApiUrl}") as HttpWebRequest;

		request.KeepAlive = true;
		request.Method = "POST";
		request.ContentType = "application/json; charset=utf-8";

		request.Headers.Add("authorization", $"Basic {ApiKey}");

		var serializer = new System.Web.Script.Serialization.JavaScriptSerializer();
		var obj = new
		{
			app_id = AppId,
			headings = new {en = title},
			contents = new {en = content},
			included_segments = segments,
			url = link
		};
		var param = serializer.Serialize(obj);
		var byteArray = Encoding.UTF8.GetBytes(param);

		using (var writer = request.GetRequestStream())
		{
			writer.Write(byteArray, 0, byteArray.Length);
		}

		using (var response = request.GetResponse() as HttpWebResponse)
		{
			if (response != null)
			{
				using (var reader = new StreamReader(response.GetResponseStream()))
				{
					var responseContent = reader.ReadToEnd();
					return !responseContent.Contains("error");
				}
			}
		}
	}
	catch (Exception ex)
	{
		return false;
	}

	return false;
}

You can see there is nothing fancy in the code. It's just a basic code that checks all the settings and call PushNotificaion function from ContentService_Published. PushNotification function is simply posting data to OneSignal REST API with WebRequest.

So we have completed coding component. Now we have to register our component with Umbraco using a composer.

public class NotificationComposer : ComponentComposer<PushNotificationComponent>
{
}

Here is the final output with how it looks in windows notification.

Notification.png

And here is our final code. 

using System;
using System.Configuration;
using System.IO;
using System.Net;
using System.Text;
using Umbraco.Core.Composing;
using Umbraco.Core.Logging;
using Umbraco.Core.Models.PublishedContent;
using Umbraco.Core.Services;
using Umbraco.Core.Services.Implement;
using Umbraco.Web;

namespace Umbraco24DaysIn.Components
{
	public class PushNotificationComponent: IComponent
	{
		private readonly ILogger _logger;
		private readonly IContentService _contentService;
		private readonly IUmbracoContextFactory _umbracoContext;
		private UmbracoContext _context;

		public PushNotificationComponent(ILogger logger, IContentService contentService, IUmbracoContextFactory umbracoContext)
		{
			_logger = logger;
			_contentService = contentService;
			_umbracoContext = umbracoContext;
		}

		private bool SendPushNotificationOnPublish { get; } =
			bool.Parse(ConfigurationManager.AppSettings["PushNotificationOnPublish"]);

		private string AppId { get; } =
			ConfigurationManager.AppSettings["OneSignal:appId"];

		private string ApiKey { get; } =
			ConfigurationManager.AppSettings["OneSignal:apiKey"];

		private string ApiUrl { get; } =
			ConfigurationManager.AppSettings["OneSignal:restUrl"];

		private string TitleProperty { get; } =
			ConfigurationManager.AppSettings["pushNotificationTitleProperty"];

		private string ContentProperty { get; } =
			ConfigurationManager.AppSettings["pushNotificationContentProperty"];

		private bool IsAllowedDocType(string documentTypeAlias)
		{
			var allowedDocumentTypes = ConfigurationManager.AppSettings["PushNotificationAllowedTypes"];
			return allowedDocumentTypes.Contains(documentTypeAlias);
		}

		public void Initialize()
		{
			ContentService.Published += ContentService_Published;
		}

		private void ContentService_Published(IContentService sender, Umbraco.Core.Events.ContentPublishedEventArgs e)
		{
			using (var contextReference = _umbracoContext.EnsureUmbracoContext())
			{
				_context = contextReference.UmbracoContext;
			}

			foreach (var item in e.PublishedEntities)
			{
				if (IsAllowedDocType(item.ContentType.Alias))
				{
					var title = item.GetValue(TitleProperty) != null ? item.GetValue<string>(TitleProperty) : item.Name;
					var content = item.GetValue(ContentProperty) != null ? item.GetValue<string>(ContentProperty) : item.Name;
					var url = "";

					if (_context != null)
					{
						var publishedEntity = _context.Content.GetById(item.Id);
						url = publishedEntity.Url(mode: UrlMode.Absolute);
					}
	

					var segments = new string[] { "All" };

					if (SendPushNotificationOnPublish)
					{
						if (PushNotification(title, content, segments, url))
						{
							_logger.Info<PushNotificationComponent>(
								$"Notification sent successfully. Page Details : '{title}' ('{item.Id}')");
						}
						else
						{
							_logger.Warn<PushNotificationComponent>(
								$"Notification send failed. Page Details : '{title}' ('{item.Id}')");
						}
					}
					else
					{
						if (item.GetValue<bool>("sendPushNotification"))
						{
							if (PushNotification(title, content, segments, url))
							{
								item.SetValue("sendPushNotification", false);
								_contentService.SaveAndPublish(item, raiseEvents: false);
								_logger.Info<PushNotificationComponent>(
									$"Notification sent successfully. Page Details : '{title}' ('{item.Id}')");
							}
							else
							{
								_logger.Warn<PushNotificationComponent>(
									$"Notification send failed. Page Details : '{title}' ('{item.Id}')");
							}
						}
					}
				}
			}
		}

		public void Terminate()
		{
			
		}

		private bool PushNotification(string title, string content, string[] segments, string link)
		{
			try
			{
				var request = WebRequest.Create($"{ApiUrl}") as HttpWebRequest;

				request.KeepAlive = true;
				request.Method = "POST";
				request.ContentType = "application/json; charset=utf-8";

				request.Headers.Add("authorization", $"Basic {ApiKey}");

				var serializer = new System.Web.Script.Serialization.JavaScriptSerializer();
				var obj = new
				{
					app_id = AppId,
					headings = new {en = title},
					contents = new {en = content},
					included_segments = segments,
					url = link
				};
				var param = serializer.Serialize(obj);
				var byteArray = Encoding.UTF8.GetBytes(param);

				using (var writer = request.GetRequestStream())
				{
					writer.Write(byteArray, 0, byteArray.Length);
				}

				using (var response = request.GetResponse() as HttpWebResponse)
				{
					if (response != null)
					{
						using (var reader = new StreamReader(response.GetResponseStream()))
						{
							var responseContent = reader.ReadToEnd();
							return !responseContent.Contains("error");
						}
					}
				}
			}
			catch (Exception ex)
			{
				return false;
			}

			return false;
		}
	}
}

All code used in above demonstration is available at https://github.com/usome/Umbraco24DaysIn. Database backup also included inside the database folder. CMS username/password is admin@admin.com/umbraco2019.

Thank you for getting your time to read this article. Merry Christmas to all of you. 

Pasang Tamang