Push notifications can seem a little tricky to get started with, but with node-apn, the only real difficulty we'll have is attaining the right certificates from Apple. I personally referred to this Ray Wenderlich tutorial to figure this stuff out, but they used a PHP (eugh) script to send the notification, whereas here I'll use a Node backend. A lot of the Swift code here will be from that tutorial.
This guide will be broken down into three parts: getting the ceritficates from Apple, developing the Swift app, and developing the backend. We're going to start with a Swift app and Node backend from my course on mobile backend development with node, and we'll modify these in such a way that we'll be left with a specific API endpoint, that when queried, sends a push notification to an iOS device.
You're going to need an Apple Developer's license and an iPhone to get this all working.
Start by cloning the following two repositories onto your desktop:
- https://github.com/alex-paterson/mobile-backend-apis-with-node-client
- https://github.com/alex-paterson/mobile-backend-apis-with-node-server
Open up the Xcode workspace in the client project, and open the server directory in your favorite text editor so we can get started.
Certificates
The first thing you need to do is give your application a unique bundle identifier.
Then hit Capabilities and turn on push notifications.
This will create a new App ID for your application. Log in at the Apple Developer Portal, and navigate to your certificates dashboard, where you'll find the new App Identifier.
Select it, and hit edit. This should open up the App Services page, including the following:
Hit Create Certificate under Development SSL Certificate. You will be shown some instructions for generating a Certificate Signing Request (CSR). Do as it says, and upload the CSR. You will then download a file probably called aps_development.cer. Just keep this on the desktop for now. Rename it to cert.cer, and open it up, which imports it into your keychain.
Now, open up your keychain, find the certificate (select My Certificates on the left panel), right click on it, and select Export:
Save it on the desktop as key.p12. Don't give it a password. If you want to give it a password, you'll have to refer to here when we configure node-apn
.
We now have cert.cer and key.p12 on the desktop.
Next, boot up a terminal session and cd
onto the desktop. Run the following commands:
$ openssl x509 -in cert.cer -inform DER -outform PEM -out cert.pem
$ openssl pkcs12 -in key.p12 -out key.pem -nodes
You should now have the files: cert.pem and key.pem. These are the files we need to send notifications to the application with the bundle identifier we chose. Just hang on to these for now. Next step, setting up the app.
Our Swift App
Basically, every time our app launches, it's going to register with the Apple push notification service, triggering a function called application:didRegisterForRemoteNotificationsWithDeviceToken
. This device token is basically the device's push notifications address, and we're going to send it to our server.
We'll start in AppDelegate.swift, adding a new line to application:didFinishLaunchingWithOptions
:
func application(application: UIApplication, didFinishLaunchingWithOptions launchOptions: [NSObject: AnyObject]?) -> Bool {
registerForPushNotifications(application)
//...
}
Add this new registerForPushNotifications
function at the bottom of AppDelegate
:
func registerForPushNotifications(application: UIApplication) {
let notificationSettings = UIUserNotificationSettings(
forTypes: [.Badge, .Sound, .Alert], categories: nil)
application.registerUserNotificationSettings(notificationSettings)
}
This will re-register the user for notifications if they disable it. We just need three more AppDelegate functions, which have pretty self-explanatory names. Again, I recommend the Ray Wenderlich push notifications tutorial if you wish to understand this better; I don't want to bore you.
func application(application: UIApplication, didRegisterUserNotificationSettings notificationSettings: UIUserNotificationSettings) {
if notificationSettings.types != .None {
application.registerForRemoteNotifications()
}
}
func application(application: UIApplication, didFailToRegisterForRemoteNotificationsWithError error: NSError) {
print("Failed to register:", error)
}
func application(application: UIApplication, didRegisterForRemoteNotificationsWithDeviceToken deviceToken: NSData) {
let tokenChars = UnsafePointer<CChar>(deviceToken.bytes)
var tokenString = ""
for i in 0..<deviceToken.length {
tokenString += String(format: "%02.2hhx", arguments: [tokenChars[i]])
}
print("Device Token:", tokenString)
if User.isLoggedIn() {
User.setUserNotificationToken(tokenString) { (success) in
if (success) {
print("It worked!")
} else {
print("It didn't work!")
}
}
}
}
At the end of this last function, the one that is called when we have access to a device token, I call a function: User.setUserNotificationToken
, passing in the device token as a string. This is the next bit of code we're going to need, which posts the user's device token to the server. We'll later construct an API endpoint to receive this token, and set it as a property of the user in the database. Here's User.isLoggedIn
and User.setUserNotificationToken
(add these to the User class definition in User.swift):
// User.swift
//...
static func isLoggedIn() -> Bool {
if NSUserDefaults.standardUserDefaults().valueForKey("userId") != nil {
return true
}
return false
}
//...
static func setUserNotificationToken(token: String, completionHandler: (success:Bool) -> ()) {
let parameters = [
"token": token
]
print(User.id)
Alamofire.request(.POST, APIEndpoints.setTokenURL(User.id), parameters: parameters, encoding: .JSON, headers: ["authorization": User.token])
.validate()
.responseJSON { response in
switch response.result {
case.Success:
print(response)
if let value = response.result.value {
let json = JSON(value)
// We did it
completionHandler(success: true)
return
}
case.Failure(let error):
print(error)
}
completionHandler(success: false)
}
}
//...
We also need to add APIEndpoints.setTokenURL
. I'll change the baseURL
property of this as well, to port 3000 of my local IP address:
// APIEndpoints.swift
//...
private static let baseURL = "http://10.0.0.16:3000/v1"
//...
static func setTokenURL (userId: String) -> String {
return "\(baseURL)/users/\(userId)/set_token"
}
//...
option+click on the wi-fi symbol on your status bar to see your local IP address.
Alright, that's all we need to do in our Xcode project. You may have noticed that our user will actually need to restart the app after signing up, so that User.setUserNotificationToken
has an actual user to set the device token for. Push notifications will not work until this happens. It's pretty obvious how you could save the device token and send it along with the sign in/sign up request, but I won't do that here.
The Backend
Boot up a terminal session and cd
into the server directory. Run the following commands:
$ npm install
$ npm install --save apn
$ nodemon index.js
In index.js, set the default host to your machines local IP address with: var host = process.env.HOST || '10.0.0.16';
.
We can now add two actions and two routes to our backend; one for setting the user's device token, and one for triggering the push notification.
I'm just going to put these two actions in todos_controller.js
.
// todos_controller.js
var apn = require('apn');
exports.setToken = function(req, res, next) {
var user = req.user;
user.apn_token = req.body.token;
user.save(function(err) {
if (err) { return next(err) }
return res.json({success: "true"});
})
}
exports.sendNotification = function(req, res, next) {
var user_id = req.params.user_id;
User.findById(user_id, function(err, user) {
if (err) { return next(err) }
var token = user.apn_token;
var options = {
cert: __dirname + '/cert.pem',
key: __dirname + '/key.pem'
};
var apnConnection = new apn.Connection(options);
var myDevice = new apn.Device(token);
var note = new apn.Notification();
note.expiry = Math.floor(Date.now() / 1000) + 3600;
note.badge = 3;
note.sound = "ping.aiff";
note.alert = "\uD83D\uDCE7 \u2709 You have a new message";
note.payload = {'messageFrom': 'Caroline'};
apnConnection.pushNotification(note, myDevice);
return res.json({success: "true"});
});
}
The first action handles the User.setUserNotificationToken
function we wrote before, and the second will be the one we use to actually send push notifications.
You can see in the apnConnection
options object, I specify the path to cert.pem and key.pem. Copy and paste these files into the controller/
directory. You'll probably want to actually keep these somwhere else in your application, but here's fine for now.
Now let's add two corresponding routes to router.js:
// router.js
router.route('/users/:user_id/set_token')
.post([requireJwt, TodosController.checkValidUser, TodosController.setToken]);
router.route('/users/:user_id/send_notification')
.get([TodosController.sendNotification]);
We also need to add a token attribute to our User model:
// models/user.js
var userSchema = new Schema({
//...
apn_token: {
type: String
}
});
Finally, run the app on your phone, and confirm that the backend is running in a terminal session. Your phone will need to be on the same Wifi network as your machine to communicate with the server.
Testing Things Out
Sign up a new user on your iOS device. You should see a user ID printed to the logs. Copy this, and navigate to the following URL in your browser: http://10.0.0.16:3000/v1/users/57c987df4d8004f71a41fc3e/send_notification
, filling in the user id.
It's likely your server will crash, because it will attempt to send a notification to a user with no device token in the database. Start your server again. Now, run the app on your device for a second time, so it actually sets the user's apn_token
on the server. You should see the following.
Now, exit the app on your phone, and navigate back to http://10.0.0.16:3000/v1/users/57c987df4d8004f71a41fc3e/send_notification
in your browser again.
Magically, the notification will appear. Obviously this application doesn't make sense as it requires users to relaunch the app to receive push notifications, but the snippets are all there for you to build something useful with.