DeviceToken and turbolinks-ios

Ruby on Rails 5, Turbolinks and Turbolinks iOS are fantastic frameworks to build hybrid native apps. We've already built a few at Firmhouse ranging from prototypes to full blown apps.

One thing that's not quite obvious from the documentation of turbolinks-ios, is how to pass the user's device token to your Rails backend in the case that you want to send push notifications. Read on for two options to get the user's device token from the device to your Rails app.

One: Pass via URL parameters

First in your AppDelegate.swift, you need to tell your app to register for remote notifications. Implement your applicationDidFinishLaunchingWithOptions as follows:

func application(application: UIApplication, didFinishLaunchingWithOptions launchOptions: [NSObject: AnyObject]?) -> Bool {  
    application.registerForRemoteNotifications()

    return true
}

Then, also in your AppDelegate.swift add the callback method where you can receive the device token and do something with it. Implement the applicationDidRegisterForRemoteNotificationsWithDeviceToken like so:

func application(application: UIApplication, didRegisterForRemoteNotificationsWithDeviceToken deviceToken: NSData) {  
    let defaults = NSUserDefaults.standardUserDefaults()
    defaults.setObject(deviceToken, forKey: "deviceToken")
    defaults.synchronize()
}

This piece of code saves the device token in the user settings of your app.

Now we can write some code that reads the deviceToken from your user settings and passes it onto your Rails app via URL params.

First, lets change the standard presentVisitableForSession method to use a custom URL method to load the actual URL passed in. For example:

func presentVisitableForSession(session: Session, URL: NSURL, action: Action = .Advance) {  
        let visitable = VisitableViewController(URL: URLWithToken(URL))

        if action == .Advance {
            pushViewController(visitable, animated: true)
        } else if action == .Replace {
            popViewControllerAnimated(false)
            pushViewController(visitable, animated: false)
        }

        session.visit(visitable)
}

Then implement the URLWithToken method as follows:

private func URLWithToken(rawURL: NSURL)-> NSURL {  
        deviceToken = NSUserDefaults
            .standardUserDefaults()
            .dataForKey("deviceToken")?
            .description
            .stringByTrimmingCharactersInSet(NSCharacterSet.init(charactersInString: "<>"))
            .stringByReplacingOccurrencesOfString(" ", withString: "")

      if deviceToken == nil {
          return rawURL
      } else {
          var finalURL = "\(rawURL)"
          if finalURL.rangeOfString("?") == nil {
              finalURL = "\(finalURL)?deviceToken=\(deviceToken!)"
          } else {
              finalURL = "\(finalURL)&deviceToken=\(deviceToken!)"
          }
          return NSURL(string: finalURL)!
      }
}

In this method, we take the URLToken stored in the user defaults for the app. Then we remove the special characters and spaces so it can be sent correctly via URL param. The second half of the method makes sure that we:

  1. Do not send a token when it's not present
  2. Set the deviceToken as the primary URL parameter when the URL doesn't contain other parameters yet
  3. Add the deviceToken as a URL parameter when other parameters are already present in the URL.

At the Rails end, you can have a before_action in your ApplicationController that checks for this parameter and updates on the current user, like so:

class ApplicationController  
  before_action :save_device_token, if: :user_signed_in?

  private

  def save_device_token
      return if params[:deviceToken].blank?

      current_user.update(ios_device_token: params[:deviceToken])
  end
end  

Passing the device token via URL is the first option we tried. It's a good implementation that does the job. However, a downside is that it clutters every Turbolinks request that is made by your app, and another downside is that you need to sanitize the token before appending it to the URL.

Lately, I have been trying the next option - option two - in the new iOS app for Sporzer, a mobile fitness community that we're building.

Two: Pass via UserAgent string

This option passes the device token in the UserAgent string of your Turbolinks iOS webview client. I like this approach since it's similar to passing custom HTTP headers and you don't need to override the URLs throughout your app.

First - just like option one - update AppDelegate.swift so it registers for remote notifications and stores the device token in the user defaults for your app.

Then, in the UINavigationController that makes your Turbolinks requests happen, set up your custom WKWebViewConfiguration like so:

private lazy var webViewConfiguration: WKWebViewConfiguration = {  
    let configuration = WKWebViewConfiguration()

    let applicationName = "Sporzer iPhone"
    let deviceToken = NSUserDefaults
        .standardUserDefaults()
        .dataForKey("deviceToken")

    if (deviceToken != nil) {
        let applicationNameWithDeviceToken = "\(applicationName)  deviceToken: \(deviceToken!)"
        configuration.applicationNameForUserAgent = applicationNameWithDeviceToken
    } else {
        configuration.applicationNameForUserAgent =  applicationName
    }

    configuration.processPool = self.webViewProcessPool
    return configuration
}()

private lazy var session: Session = {  
    let session = Session(webViewConfiguration: self.webViewConfiguration)
    session.delegate = self
    return session
}()

This code grabs the device token from the user defaults and then appends it to the application name in the user agent string. This way the Turbolinks webview will pass this to your Ruby on Rails app.

At the Ruby on Rails end, you do the following in your ApplicationController's before_action:

class ApplicationController  
    before_action :store_ios_token, if: :user_signed_in?

    private

    def store_ios_token
        match = request.user_agent.match(/.*deviceToken: (.*)/)
        return if match.nil? || match[1].blank?
        token = match[1]
        current_user.update(ios_token: token)
    end
end  

Conclusion

Done! That's it. With either option, you can store the device token of your user's iOS device in your app. With the device token stored, you can use a gem like Houston, to start sending push notifications to your users. Awesome!

Did you enjoy this post? If you have any questions or comments, please let me know! You can reach me on Twitter via @michiels or send me an email at michiel@firmhouse.com.