Push Notifications On Mobile

Preparation

Make sure that you have completed the Logging In and Sending and Receiving Messages tutorials.

Introduction

In a previous tutorial, we discussed how to retrieve events with the Sync operation. We also said that this can be done at regular intervals (polling). But we also stated that it would not be possible to do this on mobile platforms such as Android and iOS. There are many reasons for this. First, because apps on mobile do not always run. They can be sleeping or shut down if the operating system decides to do so. Even if you could run them constantly (e.g. as a background service), they tend to use up the battery and network bandwidth which annoys users. Fortunately, there is a way to receive events asynchronously in mobile apps. The IM service has a mechanism for this. It’s called the Push Gateway (AKA Sygnal).

The Push Gateway can send events as notifications via two underlying backend systems: Apple Push Notification Service (APNS) for all iOS devices, and Firebase Cloud Messaging (also known as Google Cloud Messaging or GCM) for Android devices. Using the Push Gateway is pretty straightforward. All you have to do is register the mobile device, and then listen to incoming notifications.

Setup

Before even writing a single line of code, you should follow a few setup steps. These steps vary depending on your platform:

For iOS

  • Get an Apple Developer account
  • Set up your project for Push Notifications, including the selection of a unique application ID (in our example, this is “com.namaka.buzzclient.custom”). This is the same application ID as the previous step.

For Android

  • Make sure you have a Google account. This will be used to log into the Firebase console.
  • Link your Android project to a new Firebase project.

Registering a Mobile Device’s Push Notification Token

Registering your mobile device is done with the /api/MobilePhoneNotificationToken Administration API. You have to pass in the type (iOS or Android), an application ID (AKA Package Name on Android), and a device token. You should send this token after each login.

//### AppDelegate.swift: Add this to your application delegate:
var token = ""

func application(_ application: UIApplication, didRegisterForRemoteNotificationsWithDeviceToken deviceToken: Data) {
    token = deviceToken.map {String(format: "%02.2hhx", $0)}.joined()
}

//### Add this to the existing loginSuccessful() function:

func loginSuccessful(response: [String: Any]) {
    //... everything else stays the same

    sendToken() //Register this device for push notifications
} //### Add the following function as is: let APP_ID = "com.namaka.buzzclient.custom" //Unique identifier of this app, as per next step func sendToken() { let appDelegate = UIApplication.shared.delegate as! AppDelegate //Type 0 == iOS let parameters = ["applicationId": APP_ID, "type": 0, "debugBuild": true, "token": appDelegate.token] as [String : Any] let url = URL(string: server + "/api/MobilePhoneNotificationToken")! let session = URLSession.shared var request = URLRequest(url: url) request.httpMethod = "PUT" request.setValue("Bearer " + token!, forHTTPHeaderField: "Authorization") do { request.httpBody = try JSONSerialization.data(withJSONObject: parameters, options: .prettyPrinted) } catch { print(error.localizedDescription) return } request.addValue("application/json", forHTTPHeaderField: "Content-Type") request.addValue("application/json", forHTTPHeaderField: "Accept") let task = session.dataTask(with: request as URLRequest, completionHandler: { data, response, error in guard error == nil else { return } print("token sent successfully") }) task.resume() }

The next step is to listen to incoming notifications.

Processing Push Notifications

The next step is to create a notification delegate or listener, which implements the methods to process notifications and will handle them according to your needs. Notifications can arrive while the app is running in the foreground, but they may also arrive while the app is in the background or is not running at all.

When the app is running in the foreground, usually you want to process the notification silently and just update your GUI with the new data (e.g. by invoking a sync).

In other cases, the notification will be displayed on the screen for the user to see. He will then have the option of opening the notification or dismissing it. If the user opens the notification, it will start and/or bring the app to the foreground and then pass along the notification as per the previous paragraph.

The payload of these notifications will contain the event with the following data:

  • integer notificationType (0 = new message, 1 = new invite)
  • String roomId
  • String userName
  • String content
//### Add UNUserNotificationCenterDelegate protocol to AppDelegate class declaration:
@UIApplicationMain
class AppDelegate: UIResponder, UIApplicationDelegate, UNUserNotificationCenterDelegate {

//Add this constructor:
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {

    let center = UNUserNotificationCenter.current()
    let options: UNAuthorizationOptions = [.alert, .badge, .sound]
    center.requestAuthorization(options: options) { (granted, error) in
        if granted {
            application.registerForRemoteNotifications()
        }
    }
    UNUserNotificationCenter.current().delegate = self

    return true
}

//This will be called when a notification arrives while the app is running in the foreground:
func userNotificationCenter(_ center: UNUserNotificationCenter, willPresent notification: UNNotification, withCompletionHandler completionHandler: @escaping (UNNotificationPresentationOptions) -> Void) {
    let info = notification.request.content.userInfo
    let notificationType = info["notificationType"]
    let roomId = info["roomId"]
    let userName = info["userName"]
    let content = info["content"]
    
    //Process notification
    let service = BuzzRestService.shared()

    //Process notification
    service.sync {
        //Successful!
        //Refresh screens
        NotificationCenter.default.post(name: Notification.Name("Sync"), object: nil)
    }
//If you want the notification to be displayed, un-comment this: //completionHandler([.alert, .sound]) } ///### Add this to each screen to refresh it: override func viewDidLoad() { super.viewDidLoad()
NotificationCenter.default.addObserver(self, selector: #selector(onReceiveData(_:)), name: NSNotification.Name(rawValue: "Sync"), object: nil)
}
@objc func onReceiveData(_ notification:Notification) { // Refresh your screen, e.g. tableView.reloadData() DispatchQueue.main.async { self.tableView?.reloadData() } }

Upon receiving this event, it is up to you to sync up the events to process them, as shown in the previous tutorial.