Would you like 540% more daily app opens, 30% more social sharing, and 20% more mobile transactions? According to AdWeek, push notifications for your mobile apps can dramatically increase engagement and adoption. This post will show you how to send push notifications to iOS apps using Force.com and Urban Airship.

About Push Notifications

Push notifications on mobile and other platforms allow your cloud-based applications to send brief alerts and updates to a client application. Apple and Android each offer their own services for this; this article will focus on the Apple system for iOS applications, but the Android flow and implementation are very similar and detailed in this blog post.

Where We’re Going

We’re going to build part of an iPad app that allows Service Cloud users to view Cases, attach audio memos, and receive a notification when they close. Here’s what the notification will look like:


Push-notification2.png


We won't walk through building the entire app from scratch, but instead focus on the pieces that implement push notifications for a Force.com app. To build a copy of this demo app, you can sign up for a free Developer Edition account, as well as a free Urban Airship account. Then use the Force.com IDE to install the code that powers this article's example app, which is open source and available on GitHub. (You can just get the Urban Airship library to include in your own app.) Xcode includes the iOS simulator and is a free download in the Mac App Store, though you will need a physical iOS device and a paid Apple iOS Developer account to send the final notification. All these pieces fit together in the following architecture, with Salesforce as the Provider:

Push-flow.png


Use Case and Design

Our use case is that as a Service Cloud user, we want to view a Case, record an audio memo, and receive a push notification when the Case Status is set to Closed. Calling an external web service when a record is updated gives us the choice of a workflow-based outbound message or an Apex trigger. Since we need a specific format for the push notification API and Outbound Messaging sends standard SOAP messages, we’ll use an Apex trigger. We'll also separate our logic for push from our external communication, so our Salesforce architecture will look like this:


Salesforce-push-architecture2.png

The Trigger

We’ll keep our trigger small so our code is readable and self-documenting. Our goal here is to find which Cases, in a potential bulk trigger call, should pass to our notification logic:

trigger Case_PushNotification on Case (after insert, after update) {
    set<Id> closedCases = new set<Id>();
    
    for (Id id : Trigger.newMap.keySet()) {
   	 if (Trigger.newMap.get(id).IsClosed &&
   		 ((Trigger.isInsert && Trigger.newMap.get(id).Status == 'Closed') || !Trigger.oldMap.get(id).IsClosed)) {
   			 
   		 closedCases.add(id);
   	 }
    }

    if (closedCases.size() > 0) {
   	 CasePushNotification.sendClosedNotification(closedCases);
    }
}

Push Notification Logic

In order to send the right notifications to the right users, we need to create a map that stores the newly closed Cases for each user:

	@future(callout=true)
    public static void sendClosedNotification(set<Id> closedCaseIds) {    
   	 System.debug('sendClosedNotification ' + closedCaseIds);
   	 
   	 map<Id, list<Id>> casesByUserId = new map<Id, list<Id>>();
   	 
   	 for (Attachment attachment : [select OwnerId, ParentId from Attachment where ParentId in :closedCaseIds]) {
   		 if (!casesByUserId.containsKey(attachment.OwnerId)) {
   			 casesByUserId.put(attachment.OwnerId, new list<Id>());
   		 }
   		 
   		 casesByUserId.get(attachment.OwnerId).add(attachment.ParentId);
   	 }
    
   	 map<Id, integer> newResponseCountByUserId = newResponseCountByUserId(casesByUserId.keySet());

We also have some additional logic in another function for calculating the total number of closed cases for that user so we can display a numerical “badge” counter on the app’s home screen icon and within the app.

Now if you’re like many digital disciples these days, you have multiple devices, so we’re going to send notifications to all of those opted-in using a custom object:

    	for (Mobile_Device__c d : [select Name, User__c from Mobile_Device__c where User__c in :casesByUserId.keySet()]) {
   		 system.debug('Future push notification to User ' + d.User__c + ' on device ' + d.Name);

The Mobile Device custom object is just a key stored in Name with a lookup to the user who registered it. But where does that data come from?

Device Registration

In order to receive push notifications from an app, you must opt-in, and can choose from any combination of message, badge, and sound alerts. When a user opts-in, we need to store their device ID to send future alerts. We'll do this using a custom object, Mobile_Device__c, which stores the ID as the Name and has a lookup to the user who owns the device. Here's the Objective-C code in the iOS app:

- (void) didLogin {
    // STEP 10 a - Prompt to register for push notifications
    // Do after login so we can save device token to Salesforce 
#if !TARGET_IPHONE_SIMULATOR
    [[UIApplication sharedApplication] registerForRemoteNotificationTypes: 
	 (UIRemoteNotificationTypeAlert | UIRemoteNotificationTypeBadge | UIRemoteNotificationTypeSound)];	
#endif

// STEP 10 b - On successful push notification registration, save device token for user
- (void)application:(UIApplication *)app didRegisterForRemoteNotificationsWithDeviceToken:(NSData *)deviceToken {
    // NSData contains token as <abc1 defd ...> - strip to just alphanumerics
    NSString *token = [NSString stringWithFormat:@"%@", deviceToken];
    token = [token stringByTrimmingCharactersInSet:[NSCharacterSet characterSetWithCharactersInString:@"<>"]];

    ZKSObject *mobileDevice = [ZKSObject withType:@"Mobile_Device__c"];
    [mobileDevice setFieldValue:token field:@"Name"];
    // User__c will be set automatically by Salesforce trigger
    
    [[FDCServerSwitchboard switchboard] create:[NSArray arrayWithObject:mobileDevice] target:self selector:@selector(createResult:error:context:) context:nil];
}

Building Push Notifications

Thanks to Urban Airship, our push notification will be a simple JSON string. This code uses the older JSONObject library, but a GitHub Pull Request using the new native JSON functionality will be most welcome. Once we build our notification, we'll add it to the queue for our notification service:

			JSONObject caseInfo = new JSONObject();
	
			list<Id> cases = casesByUserId.get(d.User__c);
			list<JsonObject.value> caseIds = new list<JsonObject.value>();	
				
			for (Id id : cases) {
				caseIds.add(new JsonObject.value(id));
			}
			
			caseInfo.putOpt('caseIds', new JsonObject.value(caseIds));
			caseInfo.putOpt('userId', new JsonObject.value(d.User__c));
	
			string message = cases.size() == 1 ? 'Your case has been closed' : 'You have ' + cases.size() + ' closed cases';
			System.debug(message);
			
			pushNotificationService.queuePushNotification(d.Name, message, newResponseCountByUserId.get(d.User__c), caseInfo);

We put a few Salesforce IDs in the caseInfo list, which becomes part of the push notifications user info. Apple provides this for developers to pass custom information with their alerts, but it comes with a few important warnings:

  • Delivery isn't guaranteed, so you can't rely on it to trigger other actions or updates in your app.
  • Transmission of notifications isn't secure, so you won't want to send anything sensitive or confidential.
  • Finally, the overall size of the notification is limited, so we can't send any significant amount of data.

Instead, we'll just use anonymous Salesforce IDs for our app to use once its securely connected to Salesforce.

Sending Push Notifications

In the UrbanAirship class, we implement the PushNotificationInterface. (The extra layer of an interface makes it easy for us to test using a mock since web callouts aren't allowed in tests, but that's a subject for another wiki page.)

The first function we called, createNotificationBody, finishes building the complete JSON string we added to the queue. When we're ready to send them, we'll use sendQueuedNotifications to ultimately call createRequest:

	public static HttpRequest createRequest(string path, string body, string method) {
	    HttpRequest req = new HttpRequest();
	    req.setEndpoint(baseUrl + path);
	    req.setMethod(method);
	    
	    Push_Notification_Setting__c setting = [select Key__c, Master_Secret__c from Push_Notification_Setting__c where Name = :applicationName];
	    if (setting == null) {
	    	System.debug(LoggingLevel.ERROR, 'No Push Notification Setting found for ' + applicationName);
	    } else {
	    	req.setHeader('Authorization', 'BASIC ' + EncodingUtil.base64Encode(Blob.valueOf(setting.Key__c + ':' + setting.Master_Secret__c)));
	    }
	    
	    if (body != null) {
		    req.setHeader('Content-Length', String.valueOf(body.length()));
		    req.setHeader('Content-Type', 'application/json');
		    req.setBody(body);
	    }
	    
	    return req;
	}

This is a mostly standard HttpRequest, and to authenticate to Urban Airship we'll use an HTTP header value containing a base64 encoding of our Urban Airship application key and secret. We can hardcode these into our class, but that makes it tough to connect to multiple apps, or to publish the library as open source. So instead, we store them as a Custom Setting since only the admin should have access to these security tokens, and query for them just like a custom object.

The last piece of our push system is the queue. We can just send each notification as we go through each case and mobile device, but we do have a web callout governor limit that we'll hit when we start processing larger batches. Instead, we take advantage of Urban Airship's batch API to send multiple notifications with a single callout - always a good idea to keep in mind when designing your integrations.

Setup

Our unit tests use a mock object for the push notification service, so we can check our logic without needing a real connection. To do a full end-to-end integration, we'll need to go ahead and setup our push certificates from Apple, then provide them to Urban Airship.

Once you've followed those linked instructions, creating your Urban Airship app is simple. Just remember that each Urban Airship app can have only one certificate, so you'll create one for your test app with the sandbox certificate, and then a second production app with the production certificate.

Finally, create a new Salesforce Push Notification Setting in Custom Settings with the Urban Airship Application Key and Application Master Secret, using the name "Case Memo" to match our default query in the code:

Custom-settings.png

The Demo

Grab your iPad - it's time for the final step. Build your app to the device, run it, and opt into push notifications. In Salesforce, you should see a new Mobile Device record.

Pick one of the cases from your Developer Edition org and record an audio memo; in Salesforce, you'll see it as an attachment on the case and you are now considered a "subscriber" to the case.

Close the case in Salesforce and you'll soon see a push notification in your iPad app - congratulations! You're now a push notifications developer on salesforce.com.

You can repeat this process with multiple devices and users, and even close the app before you close the case. Push notifications are processed by the device, so if you choose to view one while your app is closed, it'll be opened and passed the notification information.

Processing Notifications on the Device

While that's the core of push notifications, let's take it one more step for the ultimate user experience. Instead of just bringing users back into the app where they last were, let's take them straight to the case that just closed using that custom information we passed in the alert.

There are two iOS hooks for push notifications; one if we're launching the app fresh and one if we're already running:

// STEP 10 e - Show notification alert if running
- (void)application:(UIApplication *)application didReceiveRemoteNotification:(NSDictionary *)userInfo {
    self.notificationData = userInfo;
	
    NSString *message = [ ( (NSDictionary*)[userInfo objectForKey:@"aps"] ) valueForKey:@"alert"];
    UIAlertView *alert = [[UIAlertView alloc] initWithTitle:@"Case Closed" message:message delegate:self cancelButtonTitle:@"Dismiss" otherButtonTitles:@"View", nil];
    [alert show];    
    [alert release];
}

Here we store the notification data for later use and extract the message for display. (Remember that this message is sent unsecured and displays in visible alerts even when the device is locked, so keep it simple and generic.) If the user clicks the view button in that alert, then we'll extract the case IDs that have closed, and switch our view to the first one:

// STEP 10 f - Go to Case in notification
- (void)alertView:(UIAlertView *)alertView clickedButtonAtIndex:(NSInteger)buttonIndex {
	if (buttonIndex == 1) {
        [self showCaseInNotification];
	}
}

- (void) showCaseInNotification {
    NSArray *caseIds = [self.notificationData objectForKey:@"caseIds"];
    self.detailViewController.detailItem = [self.rootViewController findCaseById:[caseIds objectAtIndex:0]];
}

Summary

Push notifications are a great tool to keep users informed and engaged in your mobile applications. Urban Airship provides a cloud service that makes it easy to send them from salesforce.com using the Urban Airship for Force.com library.

We wrote a trigger to implement our business logic, created alerts with messages and numerical badge counts, and queued these for delivery in a single web callout to meet governor limits. We kept our connection secure by storing our credentials in a Custom Setting, using certificates from Apple to enable Urban Airship to send notifications on our behalf.

Finally, we optimized our user experience by passing a Salesforce ID in the push notification that our iPad app used to securely navigate to the specific case that triggered the notification.

Related Resources

About the Author

Matthew-botos.jpg
Matthew Botos is a Technical Architect at Model Metrics, a salesforce.com company, and former Mobile Development Manager at Mavens Consulting. He’s an expert in iOS mobile apps with Force.com and has presented previously at Dreamforce and iPhoneDevCon. Most of the time, though, you'll find him on Twitter as BotosCloud.