How to Send Push Notifications to a React-native App Running on an iPhone

React Native

NodeJS

Push Notifications

APNS

Introduction

In my previous article An overview of a push notification system, we gained a good understanding of how a push notifications system works. If PNS or provider server says nothing to you, I encourage you to read it first as I am not going to go over that again.

In this article, we will configure a react native application capable of receiving a push notification on a real iPhone device. This notification will be created by our nodeJS server that will then send the notification payload to APNs, the Push Notification Service of Apple.

⚠️ You will need a mac, an iPhone/iPad, and a valid Apple Developer account to follow this guide ⚠️

Generate a new react-native app

The first thing to do is to generate a new react-native app from scratch:

npx react-native init PushNotificationsDemo --template react-native-template-typescript

You could run your newly generated app on an iPhone simulator, but unfortunately, you cannot receive push notifications on a simulator. You will have to create a development provisioning profile to sign and run your app on a real device. If you don't know how to sign your app, you can follow for example this guide.

Register your app to APNs

Once the app is generated and run on a real device, we want to register it to APNs to retrieve the token that will be used to identify the device. We have some configuration to do.

1. Enable the push notification capability for your App ID

First, go to your apple developer account and look for the App Id used to identify your app:

app-id-apple-developer-account

You have to enable the push notification capability for this App ID:push-notification-capability

2. Enable the push notification capability for your app

Open your project in Xcode and enable the Push Notifications capabilityxcode_new_capability

xcode_push_notif_capability

Xcode will add the APS environment entitlement to your project:

aps-entitlement

This entitlement has two values :

  • development
  • production

It will condition the APNs server on which the device will try to register. There are two APNs environments, a development one and a production one. Use the first one when you are developing a new push notification feature on an iPhone that runs an app signed with a development certificate. Use the second one when your app is deployed in production.apns-development-production-env

You will not have to manually update the value of the aps-environment key, it is automatically updated by Xcode during the build phase depending on the type of certificate you use to sign the app.

⚠️Your provider server and your app should always be in sync about the APNs server to use. Don't expect to receive a notification on your app signed with a development certificate if the server that sends the notification communicates with api.push.apple.com and noT api.development.push.apple.com ⚠️

3. Configure your app to retrieve the token

Install react-native-notifications and follow the instructions. Then in your App.tsx file, add the following content at the beginning of the file:

Notifications.registerRemoteNotifications();
Notifications.events().registerRemoteNotificationsRegistered(
    (event: Registered) => { 
        console.log('Notification token:', event.deviceToken);
    },
);
Notifications.events().registerRemoteNotificationsRegistrationFailed(
    (event: RegistrationError) => { 
        console.log('Notification error', event);
    },
);

What happens under the hood ? Notifications.registerRemoteNotifications() calls the registerForRemoteNotifications() method of UIApplication to request the device token to APNs. When our app receives the response, it dispatches on of the following event:

  • registerRemoteNotificationsRegistered
  • registerRemoteNotificationsFailed

We listen to these events with two callbacks responsible to log the result.

If everything went well, you should see a unique token associated with your app and your device in your logs.

That's all for the react-native part, your device is now registered to the APNs, you will use the retrieved token on the server-side to send a notification to your device. Don't forget to write somewhere this token.

Associate the token to a user on your provider server

We are not going to implement this step in this guide as it doesn't strictly relate to push notifications. The main idea is to trigger a POST request to your provider server once the token is retrieved in registerRemoteNotificationsRegistered .

You may want to send:

  • the PNS token
  • the device ID and its OS
  • the user ID

Once the request is received by your provider server, you will have to persist the body data to the database of your choice. For the sake of simplicity, we are going to hardcode the PNS token on the provider server-side.

Trigger a new notification from a nodeJS provider server

Now that our device is registered to APNS and that our provider server has all the required information to target the device, let's create a secured connection to APNS from our provider server and send a notification payload to our device.

1. Establish a trusted connection to APNs using HTTP/2 and TLS

We have two ways to establish a trusted connection to APNS:

  • Token-based connection (recommended)
  • Certificate-based connection

The token-based connection is recommended by Apple because it's stateless, it's faster (APNs does not have to look for the certificate), the same key can be used for all your environments and the key does not expire. This is the connection we are going to implement in this tutorial.

As a prerequisite, we first need to generate and download the key that will be used to sign our token directly on our Apple developers account.

a. Go to the keys section:

apple-developers-key-section

b. Create a new key with the Apple Push Notifications service enabled:

apple-developers-new-key

This will generate a public / private key pair. You can download the private key. You will use the private key to sign your authentication token. The public key will be used by Apple to validate your signed token.


Now that we have our private key to sign our token, we can generate it. The token should follow the JWT standard. It is composed of:

  • a header containing the algorithm type used to sign the token and the previously retrieved key identifier:
{
   "alg": "ES256",
   "kid": "YOUR_KEY_IDENTIFIER"
}
  • a payload containing your developer's team id and the timestamp of the token generation:
{
   "iss": "YOUR_DEVELOPERS_TEAM_ID",
   "iat": "CURRENT_TIMESTAMP"
}
  • the signature: the result in base64 of the header and the payload signed with the selected algorithm and the key.

Let's implement that in a NodeJS based environment. First, generate a new nodeJS server by following this tutorial. Then you can add the jwt library that will provide you all the necessary tools to generate and sign your token.

yarn add jsonwebtoken

To generate your token, you can copy/past the following code:

const authorizationToken = jwt.sign(
  {
    iss: APPLE_TEAM_ID,
    iat: Math.round(new Date().getTime() / 1000),
  },
  fs.readFileSync(KEY_PATH, "utf8"),
  {
    header: {
      alg: "ES256",
      kid: KEY_ID,
    },
  }
);

Don't forget to replace the constants with your values:

  • APPLE_TEAM_ID can be found in the membership section of the Apple developers' website.
  • KEY_PATH is the path to the p8 file you just downloaded.
  • KEY_ID is the id of the key you just generated.

2. Generate and send a new push notification payload

The simplest push notification payload you can create can look like this:

{
   "aps": {
      "alert": {
	 "title": "\u2709 You have a new message",
      },
   },
}

Feel free to customize it as much as you want. You can find the specification here.

To send this payload to APNs, we need to establish an HTTP/2 connection with it. To do so, we can use the http2 module provided by NodeJS. The final simple implementation of our server can look like this:

const app = express()
const port = 3000

const authorizationToken = jwt.sign(
  {
    iss: APPLE_TEAM_ID,
    iat: Math.round(new Date().getTime() / 1000),
  },
  fs.readFileSync(KEY_PATH, "utf8"),
  {
    header: {
      alg: "ES256",
      kid: KEY_ID,
    },
  }
);

const http2Client = http2.connect(
  IS_PRODUCTION ? 'https://api.push.apple.com' : 'https://api.sandbox.push.apple.com'
);

app.post('/', (req, res) => {
  const request = http2Client.request({
    ':method': 'POST',
    ':scheme': 'https',
    'apns-topic': BUNDLE_ID,
    ':path': '/3/device/' + DEVICE_TOKEN,
    authorization:  `bearer ${authorizationToken}`,
  });
  request.setEncoding('utf8');
  
  request.write(
    JSON.stringify({
      aps: {
        alert: {
          title: "\u2709 You have a new message",
        },
      },
    })
  );

  request.on('end', () => {
    res.send('Notification sent !');
  });
  request.end();
})

app.listen(port, () => {
  console.log(`PushNotificationsServerDemo app listening at http://localhost:${port}`)
})

You may have noticed that a lot of code will have to be written to handle the communication with APNs properly. the features we will want to develop can be the following

  • token refreshing
  • notifications batching
  • error handling

aps-node will help you simplify this complexity. To install it you can run:

yarn add aps

With apn, our server code can be simplified to this:

const app = express()
const port = 3000

let provider = new apn.Provider({
  token: {
    key: KEY_PATH,
    keyId: KEY_ID,
    teamId: APPLE_TEAM_ID
  },
  production: IS_PRODUCTION
});

app.post('/', (req, res) => {
  var note = new apn.Notification();
  note.alert = "\u2709 You have a new message";
  note.topic = BUNDLE_ID;
  provider.send(note, DEVICE_TOKEN).then( (result) => {
    res.send('Notification sent !');
  });
})

app.listen(port, () => {
  console.log(`PushNotificationsServerDemo app listening at http://localhost:${port}`)
})

 

That all for the backend part! If you run your server (yarn start command) and generate a post request to http://localhost:3000/ you should receive a push notification on your device!

Conclusion

You've got this far, congrats!!! In this tutorial, we went through the configuration of both a react-native app and a nodeJS server to implement a push notification feature using APNs. As a result, we gain a deeper understanding of the usefulness of the Ids and Keys required by Apple to use their services. However, I do not recommend you implement your backend the way we did. You may encounter performance and scalability issues, you should have a look at some push notification SaaS available out there that will solve these kinds of problems for you. If you have any questions or feedbacks about these two articles on push notifications feel free to contact me on Twitter 🙂