PhoneGap iOS

Preparing to Develop with Push Notifications

Before you can test push notifications with your iOS app you must generate an iOS Provisioning Profile which consists of:

  • An App ID and a Push certificate associated with this App ID
  • Developer Certificate(s)
  • Device(s)

This tutorial will discuss the process for obtaining each of these assets (App ID, certificates, etc.) and creating the Provisioning Profile within the context of a developer who is part of a developer team. This portion of the tutorial is very thorough and designed to break down the many steps that are required to start developing for iOS. If you are already familiar with these steps but just want to know how to get setup for Push with an existing App ID, skip to step 4.

To get started, an admin user for your team must invite you to the developer team via the Member Center. It may be worthwhile to request that your admin send you an invitation to be a developer with admin privileges as opposed to just a member so you can add your own devices, approve certificates and the like. Regardless, this tutorial will assume you are only a member and will discuss the steps an admin would need to perform on your behalf. That said, once you receive your invitation, follow the instructions to register and become a developer on the team.

[1] Create and request a developer certificate

Open Keychain Access via Spotlight and navigate to Keychain Access >> Certificate Assistant >> Request a Certificate from a Certificate Authority. Enter your email and name, select the radio button Save to disk and save the certificate request file (CSR).

To request a developer certificate for yourself, login to the iOS Dev Center with your developer account and navigate to the iOS Provisioning Portal. Once in the Provisioning Portal, click on the Certificates section and request a certificate. On the next screen, load up your certificate request then wait for your admin to approve you.

Once you are are approved, download your certificate and open it. It should automatically be loaded into your Keychain of certificates.

[2] Add a device to the development team

You will need to ask an admin to add a device for you. Alternatively, if you have access to a device that is already added to the team’s list of devices, you may use that instead. To add a device, an admin needs to give the device a name and enter the device’s 40 hex character Device ID from the Devices section in the Provisioning PoÍrtal.

[3] Create an App ID

You will need to ask an admin to create an App ID from the App IDs section of the Provisioning Portal.

The admin will give the app a description or name of some sort, a Bundle Seed ID and a unique bundle name aka Bundle Identifier. The Bundle Seed ID is automatically generated by Apple. In the words of Apple, “[it] can be utilized to share keychain access between multiple applications you build with a single App ID.” To do Push Notifications, the Bundle Identifier cannot be an asterisk/wildcard. It must be unique so that Push Notifications will arrive at the appropriate application.

[4] Generate a Push SSL Certificate

You need to make one more certificate so that you can send pushes to your application. The Push SSL Certificate will be used by the application server to interact with APNS. APNS uses this certificate to validate the application server that will send notifications through APNS to your app. To generate this certificat, click on the App ID that you previously created and select the option to “configure” for development or production. The image below shows our App ID (blurred) that was already configured for development but has not been configured for production. When you click on the configure button Apple will take you through a process similar to creating a developer certificate. It will ask for a CSR (Certificate Signing Request). You may use the CSR previously generated; however, if you wish to associate the certificate with a different name and email you may want to create a separate CSR. After submitting this to Apple, they will return a Push SSL Certificate that you can download and use to send pushes or give to a 3rd party service like Urban Airship or PushWoosh.

[5] Create an iOS Provisioning Profile

An iOS Provisioning Profile acts like a basket for all the developer certificates and devices that you want to associate with a specific App ID. As such, the Provisioning Profile requires that you specify a profile name, one App ID, all developer certificates you want to allow and all devices you will use for development of your app.

Once an admin has created this profile, download and open it on your computer to load it into the Xcode Organizer. When you connect your device for development, locate it in Xcode’s Organizer. Make sure the device is ready for development, set up the device for development (there should be a button once you select the device). To add the Provisioning Profile you just added, find the it in the Organizer under Library >> Provisioning Profiles and drag it to your device. Your device should now be ready for development.

Set up Application Server (or Push Notification Service)

For more information, please see App Server page.

Modify PhoneGap Code

Now that you have set up a PhoneGap project in Xcode, created an iOS Provisioning Profile and established either an application server or push notification service, it is time to dig into the actual PhoneGap code.

We have provided PhoneGap push notification sample code that consists of five file sets critical to responding to push notifications with PhoneGap for iOS:

  • AppDelegate native code (.h & .m files)
  • PushNotification native code (.h & .m files)
  • PushNotification.js
  • app.js (select snippets)
  • index.html (select snippets)

The rest of the files are supplementary to the sample app FinSlap that we developed to demonstrate Push Notification functionality with PhoneGap. To get your PhoneGap running with Push, add the aforementioned files to your Xcode Project by following the steps below:

  • Delete unnecessary files
    • Delete your existing AppDelegate.h and AppDelegate.m in Xcode (move to trash or move elsewhere as desired)
    • Delete the contents of your existing www (there should only be index.html and the cordova-x.x.x.js)
  • Add library files
    • Add AppDelegate.h and AppDelegate.m from our library to your Classes group in Xcode (check the box to copy items)
    • Add PushNotification.h and PushNotification.m to the Plugins group in Xcode (check the box to copy the items)
    • Copy the contents of the www folder we have provided into your project’s www folder. This includes index.html and app.js in addition to the supplementary files (which can be removed or modified later).
    • Add the SBJson folder to your Xcode project (check the box to copy items and create groups for any added folders)
  • Modify the Cordova.plist under the Supporting Files group
    • Add the UrbanAirship host url to “<ADD URL HERE>” ExternalHosts (alternatively you can just add a wildcard [*])
    • Add key “pushnotification” and value “PushNotification” (case-sensitive) to the Plugins list
  • Update your project’s Bundle Identifier (under Summary or Info) with the Bundle Identifier associated with your App ID
  • Update your Code Signing Identity under your project’s Build Settings
At this point, you should be able to run your app on a development device without any bugs.

Register Device for Push – Get Token

The app you are developing must register with Apple’s APNS. If your app is sucessful in registering, it will recieve a token from APNS. This token represents your app on a specific device. Think of it as an address that you can send mail to and APNS will deliver a message to your app on the device with which the token is associated. The code that makes a request to APNS to register for Push Notifications is split into both objective-c (native) and javascript (cordova) code. This code is found in appDelegate.m in the application:didFinishLaunchingWithOptions: method.

app.js

// Runs when PhoneGap is ready
$(document).bind("deviceready", function () {

    // Every time the app is opened, we should re-register with APNS and our push application server (in this case UA)
    registerPNS();

    ...

});

PushNotification.js

// Register with APNS
function registerPNS() {
    console.log("Registering with APNS via the App Delegate");
    var options = [{ alert:true, badge:true, sound:true }];
    cordova.exec(successCallback, errorCallback, "PushNotification", "registerPNS", options);
}

PushNotification.m

- (void)registerPNS:(NSMutableArray *)arguments
           withDict:(NSMutableDictionary *)options {
    //NSLog(@"registerPNS:%@\n withDict:%@", [arguments description], [options description]);

    ...

    UIRemoteNotificationType notificationTypes = UIRemoteNotificationTypeNone;
    if ([options objectForKey:@"badge"]) {
        notificationTypes |= UIRemoteNotificationTypeBadge;
    }
    if ([options objectForKey:@"sound"]) {
        notificationTypes |= UIRemoteNotificationTypeSound;
    }
    if ([options objectForKey:@"alert"]) {
        notificationTypes |= UIRemoteNotificationTypeAlert;
    }

    if (notificationTypes == UIRemoteNotificationTypeNone)
        NSLog(@"PushNotification.registerPNS: Push notification type is set to none");

    [[UIApplication sharedApplication] registerForRemoteNotificationTypes:notificationTypes];

}

You may have noticed that we do not directly interface with the native method application:didFinishLaunchingWithOptions: to set the notification types as we do in our Native iOS app. We want to set these parameters in Javascript because this is our cross-platform language.

To change how the app registers for notifications simply change the true/false values in var options = [{ alert:true, badge:true, sound:true }] (highlighted on line 5 in PushNotification.js).

For more information, visit Apple’s documentation on Local and Push Notification Programming Guide.

Send the Token to the Application Server

After you send a request to register with APNS, one of two callback javascript functions will be called from native to corodova (lines 9 & 13).

PushNotification.m

- (void)registerPNS:(NSMutableArray *)arguments
           withDict:(NSMutableDictionary *)options {
    //NSLog(@"registerPNS:%@\n withDict:%@", [arguments description], [options description]);

    NSUInteger argc = [arguments count];
    if (argc > 0 && [[arguments objectAtIndex:0] length] > 0) {
        NSLog(@"Register success callback set");
        //self.registerSuccessCallback = [arguments objectAtIndex:0];
        self.callbackId = [arguments objectAtIndex:0];
    }

    if (argc > 1 && [[arguments objectAtIndex:1] length] > 0) {
        self.registerErrorCallback = [arguments objectAtIndex:1];
    }

    ...

}

PushNotification.js

// when APN register succeeded
function successCallback(e) {
    registerUAPush();

    // Hold on to this information locally
    localStorage.pushToken = e.deviceToken;
}

// when APN register failed
function errorCallback(e) {
    alert('Error during registration: ' + e.error);
}

Thus, if the request is successful, the javascript initiates the process for registering with your push application server, in this case Urban Airship (line 3).

How to Send a Push

See more about Push Servers.

How to Handle a Push

Your app needs to respond to push notifications when they come to the user’s device. When a push comes you may want the device to play a sound. You may want the push to trigger your app to go fetch some data from your server and display it on the screen. What you do with a push when it is received will depend a lot on the state state of your app or device. For example, we made a chat application. When the user is chatting with their friend Sarah and gets a message from Sarah, we want the new message from Sarah to be displayed on the screen. But if the user is chatting with Sarah and Tom sends the user a message, we want the app to let the user know they have a message from someone else but not to change the screen to the new message automatically. There are many different scenerios that your app will have for handling push notifications. We have simplified the scenrios into the two most basic levels and describe how to handle them below.

Please note that when a user is actively using your application, pushes sent to the phone will not be displayed in the status bar notification area.

The App is Closed

This situation occurs anytime your application is not on the screen in front of the user. Or as Apple describes it, your app is not in the foreground. When a push is received and your app is not active, the push notification will be stored in the Notification center. When a user taps on the notification then iOS will call one of two functions in your app. If your app was previously opened but was exited out of and now is “in the background” then iOS will call…

AppDelegate.m

- (void)application:(UIApplication *)application didReceiveRemoteNotification:(NSDictionary *)userInfo {
    NSLog(@"didReceiveNotification");

    // Get application state for iOS4.x+ devices, otherwise assume active
    UIApplicationState appState = UIApplicationStateActive;
    if ([application respondsToSelector:@selector(applicationState)]) {
        appState = application.applicationState;
    }

    // NOTE this is a 4.x only block -- TODO: add 3.x compatibility
    PushNotification *pushHandler = [self getCommandInstance:@"PushNotification"];
    if (appState == UIApplicationStateActive) {
	...
    } else {
        //save it for later
        self.launchNotification = userInfo;

        NSLog(@"inactive");

        [pushHandler performSelectorOnMainThread:@selector(notificationReceived:) withObject:OUT_OF_APP_PUSH waitUntilDone:NO];
    }

    ...
}

This function is also called when the app is in the foreground thus we use an conditional statements to see if the application was in an active state or in the background when the notification was received. After we have determined the app state we execute notificationReceived: with UIApplicationState parameter on the pushHandler, which is simply an instance of the PushNotification class.

PushNotification.m

- (void)notificationReceived:(int)appStateValue {
    NSLog(@"Notification received");
    NSLog(@"Ready: %d",ready);
    NSLog(@"Msg: %@", [notificationMessage descriptionWithLocale:[NSLocale currentLocale] indent:4]);

    if (ready && notificationMessage) {

        // Use SBJson framework to convert notificationMessage to JSON string
        NSString *jsStatement = [NSString stringWithFormat:@"window.plugins.pushNotification.notificationCallback(%@, %d);", [notificationMessage JSONRepresentation], appStateValue];

        [self writeJavascript:jsStatement];
        NSLog(@"run JS: %@", jsStatement);

        ...

        self.notificationMessage = nil;
    }
}

This method converts the payload to JSON and executes the notificationCallback method with the JSON payload. The notificationCallback method determines the app was in the background when notificaton was received and activates the outOfAppPushReceived trigger.

PushNotification.js

// Customized callback for receiving notification
PushNotification.prototype.notificationCallback = function (notification, inAppPushBoolean) {
    console.log("Received a notification to PhoneGap");
    /* Manipulate notification here before passing to the event trigger (as appropriate) */
    if (inAppPushBoolean) {
        // User interacts with notification while app is active and running in foreground OR app launches for first time
        $(document).trigger("inAppPushReceived", notification);
    } else {
        // User interacts with notification while app is not open but running in background
        $(document).trigger("outOfAppPushReceived", notification);
    }
};

Our application then handles the “out-of-app” notification by taking the user to the appropriate chat conversation.

app.js

// Global event listener for incoming out-of-app pushes (app is not open, but running in background)
$(document).bind("outOfAppPushReceived", function (event, notification) {
    // console.log(JSON.stringify(notification));
    var currentPage = $('.ui-page-active').attr('id');
    drawConversation(notification.fromAccount);

});

If the device has been reset or for some other reason the app is not in the background, iOS will call application:didFinishLaunchingWithOptions: and you can access the payload using the NSDictionary launchOptions from the push and perform the desired logic.

AppDelegate.m

- (BOOL) application:(UIApplication*)application didFinishLaunchingWithOptions:(NSDictionary*)launchOptions
{
    ...

    // cache notification, if any, until webview finished loading, then process it if needed
    // assume will not receive another message before webview loaded
    self.launchNotification = [launchOptions objectForKey:UIApplicationLaunchOptionsRemoteNotificationKey];
    application.applicationIconBadgeNumber = 0;
}

The App is Open

This situation occurs when your app is in the foreground. Because the app is open the notification will not go into the notification center or display across the top of the screen. The method didReceiveRemoteNotification: will be called and we now will execute the code for when the application is open.

AppDelegate.m

- (void)application:(UIApplication *)application didReceiveRemoteNotification:(NSDictionary *)userInfo {
    NSLog(@"didReceiveNotification");

    // Get application state for iOS4.x+ devices, otherwise assume active
    UIApplicationState appState = UIApplicationStateActive;
    if ([application respondsToSelector:@selector(applicationState)]) {
        appState = application.applicationState;
    }

    // NOTE this is a 4.x only block -- TODO: add 3.x compatibility
    PushNotification *pushHandler = [self getCommandInstance:@"PushNotification"];
    if (appState == UIApplicationStateActive) {
		pushHandler.notificationMessage = userInfo; // if you want all push notification data

        // Send push
        [pushHandler notificationReceived:IN_APP_PUSH];
    } else {
        ...
    }

    ...
}

The rest of the logic is pretty similar to what happens when the app is in the background. However, when the notificationReceived method converts the payload to JSON and executes the notificationCallback method with the JSON payload, the notificationCallback method determines the app was in the foreground when notificaton was received and activates the inAppPushReceived trigger. Then the app binds to this event and executes the following code:

app.js

// Global event listener for incoming in-app pushes (app is open and being used)
$(document).bind("inAppPushReceived", function (event, notification) {
    // console.log(JSON.stringify(notification));
    var currentPage = $('.ui-page-active').attr('id');

    if (currentPage == "login" || currentPage == "register") {
        alert(notification.aps.alert);
    } else if (currentPage == "conversationList") {
        inAppNotification(notification.aps.alert, notification.fromAccount);
    } else if (currentPage == "accounts") {
        inAppNotification(notification.aps.alert, notification.fromAccount);
    } else if (currentPage == "conversation") {
        // we are in the conversation with the person we just got a push from
        if (notification.fromAccount == localStorage.current_conversation) {
$("</pre>
<div>").addClass("bubbledRight").append($("").html(notification.aps.alert)).appendTo("#chat");
 $.mobile.silentScroll(1000000);
 }
 // we are in a different conversation
 else {
 inAppNotification(notification.aps.alert, notification.fromAccount);
 }
 } else {
 inAppNotification(notification.aps.alert, notification.fromAccount);
 }

});
Advertisements

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 )

Google photo

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

Twitter picture

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

Facebook photo

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

Connecting to %s