🎉 New! Web Push Notifications for Chatkit. Learn more in our latest blog post.
Hide
Products
chatkit_full-logo

Extensible API for in-app chat

channels_full-logo

Build scalable realtime features

beams_full-logo

Programmatic push notifications

Developers

Docs

Read the docs to learn how to use our products

Tutorials

Explore our tutorials to build apps with Pusher products

Support

Reach out to our support team for help and advice

Sign in
Sign up

Integrate Chatkit webhooks in a React Native chat app

  • Wern Ancheta
June 3rd, 2019
You need a good level of understanding of React Native, and Node 11+, Yarn 1+ and React Native 0.5+ installed on your machine.

In this tutorial, we’ll take a look at how you can use Chatkit webhooks to send push notifications when a specific event happens on the app. In a chat app, this can be when someone new joins the room, someone leaves a room, or another user mentions you in the room.

Prerequisites

You need a good level of understanding of React Native, and familiarity with building and running apps in your development environment to follow this tutorial.

You also need to have basic knowledge of Node.js.

The following package versions are used in this tutorial:

  • Node 11.2.0
  • Yarn 1.13.0
  • React Native 0.59.5

If you encounter any issues getting the app to work, try switching to the ones above.

We’re using Chatkit in this tutorial, so you need to have a Chatkit account to create a new app instance. We’re also using ngrok, for exposing the server to the internet, create an account for that as well.

App overview

We’re going to update a pre-made chat application that’s built with Chatkit and React Native Gifted Chat. The chat app already has all the basic features:

  • Public and private rooms.
  • Sending a message.
  • Attaching image files.
  • Loading older messages.
  • Typing indicators.
  • User presence indicator (shows whether the users in the room are offline or online).

We will update it to include push notifications for the following events:

  • A message is sent. We won’t be sending push notifications every time a message is sent because that would be a waste of resources. Instead, we will send them if any of the following conditions are met:
    • If there are offline users in the room where the message was sent.
    • If a user is mentioned or tagged in the message. In this case, we only send the notification to those users who are mentioned.
  • A user is added to the room.
  • A user leaves the room.

Note that this tutorial won’t support running the app on iOS. This is because push notifications isn’t possible on iOS without an Apple developer account, which requires a pricey annual license (for those who aren’t making any profit from the App Store at least). This is true even if you only want to send notifications to a test device.

You can view the code on this GitHub repo.

Setting up Firebase

We will use the Cloud Messaging service from Firebase for sending out Push Notifications. Go to console.firebase.google.com and add a new project:

Once the project is created, you will be redirected to its dashboard. Click on the gear icon next to the Project Overview link and select Project settings:

That will redirect you to the project’s settings page. Take note of the sender ID that you see on that page because you will be using it later to configure push notifications in the app.

While still on that page, click on the Service accounts tab. We’ll use it to authenticate the server later on to use Firebase’s services. In this case, we’re only using it to authenticate the Cloud Messaging service. Click on the Generate new private key button to download the JSON config file for authenticating the use of Firebase on your server. Keep it on your downloads folder for now. We will transfer it to the server later:

Bootstrapping the app

Let’s start things off by bootstrapping the chat app. The following commands will install the dependencies and set it up:

    git clone https://github.com/anchetaWern/RNChatkitWebhooks
    cd RNChatkitWebhooks
    git checkout starter
    yarn
    react-native eject
    react-native link react-native-gesture-handler
    react-native link react-native-document-picker
    react-native link react-native-fs
    react-native link react-native-config
    react-native link react-native-vector-icons
    react-native link rn-fetch-blob
    react-native link react-native-push-notification

As this is already a pre-made chat app, the only dependency that we’re concerning ourselves with in this tutorial is React Native Push Notification. Most of the config updates will be for that dependency.

First, update the android/build.gradle to include the Firebase and Google Play Services as a dependency:

    buildscript {
      ext {
        // ...
        supportLibVersion = "28.0.0"

        // add these:
        googlePlayServicesVersion = "+"
        firebaseVersion = "+"  
      }
      repositories { 
        //... 
      }
      dependencies {
        classpath 'com.android.tools.build:gradle:3.3.1'
        classpath 'com.google.gms:google-services:4.0.1' // add this
      }
    }

Still on the same file, add the config for the React Native Config package. This allows us to read the app config from the .env file at the root directory of the project:

    // android/app/build.gradle
    apply from: "../../node_modules/react-native/react.gradle"
    apply from: project(':react-native-config').projectDir.getPath() + "/dotenv.gradle" // add this

Next, update the android/app/build.gradle file and add Firebase and Google Play services as a dependency:

    dependencies {
      // ...  
      implementation project(':react-native-gesture-handler')
      // add these
      implementation 'com.google.firebase:firebase-core:16.0.1' 
      implementation 'com.google.firebase:firebase-core:16.0.8' 
      implementation 'com.google.android.gms:play-services-base:16.1.0' 
      // ...
      implementation "com.facebook.react:react-native:+"
    }

Lastly, update your .env file with your Chatkit and Firebase credentials:

    CHATKIT_INSTANCE_LOCATOR_ID="YOUR CHATKIT APP INSTANCE (omit v1:us1:)"
    CHATKIT_SECRET_KEY="YOUR CHATKIT SECRET"
    CHATKIT_WEBHOOK_SECRET="iWontTell"
    FIREBASE_SENDER_ID="YOUR FIREBASE SENDER ID"

Updating the app

As mentioned earlier, the app is already pre-made. All we have to do is add the functionality for accepting push notifications. In this case, the only file we need to update is the App.js file. Here, we include the React Native Push Notification library and configure the app so it’s able to receive Push Notifications:

    // App.js
    // ...
    import { View } from "react-native";
    import PushNotification from "react-native-push-notification";
    import Config from "react-native-config";
    const FIREBASE_SENDER_ID = Config.FIREBASE_SENDER_ID;

    import Root from "./Root";

    PushNotification.configure({
        senderID: FIREBASE_SENDER_ID,
        onRegister: function(token) {
          console.log('device registration:', token);
        },
        onNotification: function(notification) {
          console.log('notification: ', notification);
        },
        popInitialNotification: true, 
        requestPermissions: true,
    });

    export default class App extends Component {
      // ...
    }
    // ...

Here’s a break down of the options we’ve used above:

  • senderId - the Sender ID you saw earlier on the Cloud Messaging tab of your project settings page.
  • onRegister - the function that gets called when the device registration token has been successfully generated. Take note of this token when you run the app because you will supply it later to the server so it knows which device to send the notifications to.
  • onNotification gets called when a notification is received while the app is on the foreground. It also gets called when the user clicks on a notification. In this case, we’re simply logging the notification. By default, simply clicking a notification will only open the app. However, in a production app, what you want is to automatically navigate the user to the desired page. A specific chat room for example. That’s out of the scope of this tutorial so here’s a link on how to implement Deep linking when using React Navigation.
  • popInitialNotification will pop the initial notification so you can see all of its contents without having to click on the dropdown arrow.
  • requestPermissions - specifies if the device registration token will be requested or not. By default this is true, but it’s worth mentioning it because we cannot get the token needed by the server to send a notification to the device if this is set to false.

Once that’s done, you can actually run the app to get the device token:

    react-native run-android

Don’t forget to enable logging so you can see it:

    react-native log-android

At this point, you can also run the server. But we won’t be doing that yet since we still have to update it in the next section.

Updating the server

Now we’re ready to work on the server. Start by installing the dependencies:

    cd server
    yarn

We will use the Firebase Admin package to send out notifications to the test device. It requires the Firebase service account config file that you downloaded earlier on the Setting up Firebase section. Rename the file to rnchatkitwebhooks.json to simplify it and copy it over to the server/config directory:

    // server/index.js
    const admin = require("firebase-admin");
    const serviceAccount = require("./config/rnchatkitwebhooks.json");

    admin.initializeApp({
      credential: admin.credential.cert(serviceAccount)
    });

Next, include the crypto package. This package is built into Node so you don’t have to install it separately. We’re going to use it later to verify if the requests are indeed coming from Chatkit’s servers:

    const crypto = require("crypto");

Next, add a new environment config. This is the secret key used for verifying that the request really came from Chatkit. Note that this is going to read from the .env file at the root of the project directory (not the server):

    const CHATKIT_SECRET = process.env.CHATKIT_SECRET_KEY;
    const CHATKIT_WEBHOOK_SECRET = process.env.CHATKIT_WEBHOOK_SECRET; // add this

Next, hardcode the device’s registration token. This is the token that you’ll get later when you run the mobile app. It serves as a unique ID for a specific device so Firebase knows where to deliver a specific notification. We won’t be using this directly though. Later on, we will create the Chatkit users and assign this device token to each of them for testing purposes:

    const device_token = ""; // replace with the device token you got earlier

Next, we add the text middleware to the body parser. This will parse all request bodies as raw text. As you'll see later on, we need the raw text to come up with the signature for verifying the request. Specifying the type allows us to selectively apply this middleware based on the content type passed in the request. In this case, the content type used by Chatkit for webhooks is application/json. So we only apply this middleware if that’s the content type of the request:

    app.use(bodyParser.urlencoded({ extended: false }));
    // add these:
    app.use(
      bodyParser.text({
        type: (req) => {
          const contype = req.headers['content-type'];
          if (contype === 'application/json') {
            return true;
          }
          return false;
        },
      }),
    );

The chat app also makes requests to the server. We don’t really specify the content type for those requests, so it’s application/text by default. In those cases, we want to apply the JSON middleware so the request body is automatically parsed as a JSON string. That way, we don’t have to make a separate call for JSON.parse():

    // update this
    app.use(bodyParser.json({
      type: (req) => {
        const contype = req.headers['content-type'];
        if (contype !== 'application/json') {
          return true;
        }
        return false;
      }
    }));

Notify offline users in a private room

We can now implement the functions to execute when a specific event is triggered. First, we’ll implement the function to execute when a message is sent while there are room members who are offline. Here, we extract the sender’s name, message, and the IDs of the offline users from the request body. We then call the sendNotificationToUsers() function to send the push notifications to those users:

    const notifyOfflineUsers = async({ payload }) => {
      const sender = payload.sender.name;
      const message = payload.message.parts[0].content;
      const short_message = shortMessage(message);

      const offline_user_ids = payload.offline_user_ids;

      try {
        const users = await getUsersById(offline_user_ids);
        sendNotificationToUsers(users, sender, short_message);
      } catch (err) {
        console.log("error notifying offline users: ", err);
      }
    }

Note: Currently, Chatkit only triggers this event on private rooms. This makes sense because public rooms are assumed to have thousands of members in them. Resource-wise, it’s not very practical to be sending out push notifications in this case.

Here’s the shortMessage() function. Its main responsibility is to truncate the message so it fits nicely within the character limit:

    const shortMessage = (message) => {
      return message.substr(0, 37) + "...";
    }

Here’s the getUsersById() function. This returns the device token that we need for the push notifications:

    const getUsersById = async(user_ids) => {
      try {
        const users = await chatkit.getUsersById({
          userIds: user_ids
        });
        return users;
      } catch (err) {
        console.log("error getting users: ", err);
      }
    }

Here’s the sendNotificationToUsers() function. As the name suggests, it’s going to send the push notifications to the users supplied by its caller:

    const sendNotificationToUsers = (users, title, body) => {
      if (users.length) {
        users.forEach((user) => {
          sendNotification(title, body, user.custom_data.device_token);
        });
      }
    }

The sendNotification() function is the one that will actually send the push notifications. It uses the Firebase Admin package to do so. The sendToDevice() method from its messaging module accepts the registration token of the device, and the actual data to send as its arguments:

    const sendNotification = (title, body, device_token) => {
      const notification_payload = {
        notification: {
          title,
          body
        }
      };

      admin.messaging().sendToDevice(device_token, notification_payload)
        .then((response) => {
          console.log('sent notification!', response);
        })
        .catch((notify_err) => {
          console.log('notify err: ', notify_err);
        });

      console.log(title, body, device_token);
    }

Notify on user added to a room

Next is the event when a new user is added to the room. Note that this isn’t the same as joining a room. Because in the app, we’re making a request to the server to add a user into the room instead of joining the room directly from the client side. There are separate webhooks for users joining the room and adding users to a room. Another thing to note is that unlike the webhook for notifying offline users that there’s a new message, this isn’t limited to just private rooms. So we have to manually check for it if we want to limit the notifications to just private rooms.

For this specific event, all we need to extract from the request body is the room and users. Where users contain the details of the user that was added in the room, while room contains the details of the room in which the user was added. We’re only adding one user at a time, so we can simply extract the first item in the users array. From there, we get the room details via the getRoom() function. The only thing we need from it is the member_user_ids (the user ID’s of the room members). Since the new user has already been added, their ID is also in this array. We filter it out so they won’t get a notification:

    const notifyOnUserAddedToRoom = async({ payload }) => {
      const { id: room_id, name: room_name, private: is_private } = payload.room;
      const { id: user_id, name: user_name } = payload.users[0];

      if (is_private) { 
        try {
          const room_data = await getRoom(room_id);
          const room_member_ids = room_data.member_user_ids.filter(id => id != user_id); // exclude the user that was just added to the room
          const users = await getUsersById(room_member_ids);
          sendNotificationToUsers(users, 'system', `${user_name} joined ${room_name}`); 
        } catch (err) {
          console.log("error notifying user added to room: ", err);
        }
      }
    }

Here’s the getRoom() function:

    const getRoom = async(room_id) => {
      try {
        const room = await chatkit.getRoom({
          roomId: room_id
        });
        return room;
      } catch (err) {
        console.log("error getting room: ", err);
      }
    }

Notify on user left room

When a user leaves the room, we also want to notify the members of that room that someone has left. This has pretty much the same code as the previous webhook handler, only the notification message has changed:

    const notifyOnUserLeftRoom = async({ payload }) => {
      const { id: room_id, name: room_name, private: is_private } = payload.room;
      const { id: user_id, name: user_name } = payload.user;

      if (is_private) {  
        try {
          const room_data = await getRoom(room_id);
          const room_member_ids = room_data.member_user_ids.filter(id => id != user_id);
          const users = await getUsersById(room_member_ids);
          sendNotificationToUsers(users, 'system', `${user_name} left ${room_name}`); 
        } catch (err) {
          console.log("error notifying user left room: ", err);
        }
      }
    }

Notify mentioned users

The last webhook handler that we’re going to implement is for notifying users who are mentioned in a message. Chatkit doesn’t have a specific trigger for that, so this one actually gets triggered every time a message is sent in the room. From the message, we use a regular expression to extract all the text which has a “@” at the beginning. We then filter the room_members array to only include the users which were mentioned:

    const notifyMentionedUsers = async({ payload }) => {
      try {
        const sender_id = payload.messages[0].user_id;
        const sender = await getUser(sender_id);

        const message = payload.messages[0].parts[0].content;
        const short_message = shortMessage(message);
        const room_id = payload.messages[0].room_id;

        const room_data = await getRoom(room_id);
        const room_members = await getUsersById(room_data.member_user_ids);

        const mentions = message.match(/@[a-zA-Z0-9]+/g) || [];    
        const mentioned_users = room_members.filter((user) => {
          return mentions.indexOf(`@${user.name}`) !== -1;
        });

        sendNotificationToUsers(mentioned_users, sender.name, short_message); 
      } catch (err) {
        console.log("error notifying mentioned users: ", err);
      }
    }

Here’s the getUser() function:

    const getUser = async(user_id) => {
      try {
        const user = await chatkit.getUser({
          id: user_id,
        });
        return user;
      } catch (err) {
        console.log("error getting user: ", err);
      }
    }

Receiving the webhook requests

Now we’re ready to add the code for actually receiving the requests coming from the Chatkit server. Create an object which maps the type of notifications that we’re getting from Chatkit with the function to execute for each of them:

    const notification_types = {
      'v1.message_sent_user_offline': notifyOfflineUsers,
      'v1.users_added_to_room': notifyOnUserAddedToRoom,
      'v1.user_left_room': notifyOnUserLeftRoom,
      'v1.messages_created': notifyMentionedUsers
    }

Next, add the route that will accept the requests coming from Chatkit’s servers. Here, we use the verifyRequest() function to check whether the request really came from Chatkit. If it is, we know that the request body is treated as a plain text, so we use JSON.parse() to convert it to an object. From there, we extract the event_type and execute the corresponding function:

    app.post("/notify", (req, res) => {
      console.log("webhook triggered! ", req.body);
      if (verifyRequest(req)) {
        const data = JSON.parse(req.body);
        const type = data.metadata.event_type;
        notification_types\[type\](data);
        res.sendStatus(200);
      } else {
        console.log("Unverified request");
        res.sendStatus(401); // unauthorized
      }
    });

Here’s the verifyRequest() function. This uses the same verification strategy from the official docs. This is the main reason why we used the text middleware earlier. So that the request body can be supplied in its raw form (plain text) to the function for computing the signature. Once the signature is computed, we compare it to the webhook-signature supplied in the request. If they're the same then we know for sure that the request really came from a Chatkit server:

    const verifyRequest = (req) => {
      const signature = crypto
        .createHmac("sha1", WEBHOOK_SECRET)
        .update(req.body) // plain text request body
        .digest("hex")

      return signature === req.get("webhook-signature");
    }

Creating rooms and users

Now that we’ve added all the functions for handling the webhooks, the final step for this section is to add the routes for creating the rooms and users that you can use for testing. Here’s the route for creating a new room:

    app.get("/create-room/:name/:private", async (req, res) => {
      try {
        const { name, private } = req.params;
        const is_private = (private === 'true') ? true : false;
        const room = await chatkit.createRoom({
          creatorId: 'root',
          name: name,
          isPrivate: is_private
        });
        console.log(room.id);
        res.send("ok");
      } catch (err) {
        console.log("error creating room:", err);
        res.send("err");
      }
    });

And here’s the route for creating a new user and assigning them to an existing room. The important bit here is that you’re setting the device token that you got earlier from the app. The same device token is used for all of the users, so expect to get multiple notifications for the same thing when you test it later on:

    app.get("/create-and-assign-user/:username/:room_id", async (req, res) => {
      try {
        const { username, room_id } = req.params;
        const user_id = randomId(15); 
        await chatkit.createUser({
          id: user_id,
          name: username,
          customData: {
            device_token: device_token
          }
        });

        await chatkit.addUsersToRoom({
          roomId: room_id,
          userIds: [user_id]
        });

        res.send("ok");
      } catch (err) {
        console.log("error creating and assigning user to room: ", err);
        res.send("err");
      }
    });

Setting up the rooms and users

So that we can easily test the app, we need to set up a couple of rooms and users. To do that, you first have to run the server:

    cd server
    yarn start

To set up a room, access http://localhost:5000/create-room/{room_name}/{true} on your browser. Replace {room_name} with the name of the room, and set true if the room is private and false if it’s public:

Room Is Private?
General No
2A Yoyo Yes

To create a user, access http://localhost:5000/create-and-assign-user/{username}/{room_id} on your browser. Replace {username} with the username values below, and {room_id} with the ID of the rooms you created earlier. You can view them on the Chatkit console or check the server logs:

Username Room
shu 2A Yoyo
shinji 2A Yoyo

Setting up webhooks

To set up webhooks for your Chatkit app, go to your app’s console and click on the Settings tab. On that tab, click on the ADD A WEBHOOK button and you’ll be greeted with the following UI:

Set the name to Notify, the target URL to your ngrok HTTPS URL, and Webhook Secret to whatever value you supplied earlier to the CHATKIT_WEBHOOK_SECRET on your .env file. After that, enable the following triggers:

  • Message created
  • Users added to the room
  • User left room
  • Messages sent to offline users

Once that’s done, click on ADD WEBHOOK to make the webhook go live. At this point, Chatkit will send a webhook to the URL you specified every time those events get triggered in the app. You can even use the Chatkit console to test most of the notifications.

Running the app

At this point, you can now run the app. Start by exposing the server to the internet using ngrok:

    ./ngrok http 5000

Update the src/screens/Login.js, src/screens/Rooms.js, and src/screens/Chat.js file with your ngrok HTTPS URL:

    const CHAT_SERVER = "YOUR NGROK HTTPS URL";

After that, you can now run the app:

    react-native run-android

As mentioned at the beginning of this tutorial, it won’t be supporting running the app on iOS because Push Notifications (even in test environments) requires an Apple developer account.

Conclusion

In this tutorial, you learned how to integrate push notifications in an existing React Native chat app with the help of Chatkit Webhooks, Firebase, and the React Native Push Notification library.

You can view the code on this GitHub repo.

Clone the project repository
  • Node.js
  • React Native
  • Android
  • Chat
  • Webhooks
  • Chatkit

Products

  • Channels
  • Chatkit
  • Beams

© 2019 Pusher Ltd. All rights reserved.

Pusher Limited is a company registered in England and Wales (No. 07489873) whose registered office is at 160 Old Street, London, EC1V 9BW.