In the second article of this series, you learned how to use the Gifted Chat React Native library with ChatKitty's JavaScript SDK to build a full featured chat screen with real-time messaging functionality into your app. You also added screens for users to create public channels, discover new channels, and view their channels.

In this tutorial, you'll be using Expo push notifications and ChatKitty Chat Functions to set up in-app and push notifications to inform users when new messages are received or relevant actions happen inside a channel and across your app.

After reading this article, you will be able to:

  1. Implement in-app notifications for users to see what's happening from another screen

  2. Use ChatKitty user properties to store arbitrary data related to your users like expo push tokens

  3. Use Expo push notifications and ChatKitty Chat Functions to implement push notifications

If you followed along the last article, you should already have the ChatKitty JavaScript SDK NPM package added to your Expo React Native project. To make sure you have the latest version of ChatKitty, run the yarn upgrade command:

# upgrade ChatKitty SDK to the latest version
yarn upgrade chatkitty

Before we begin, let's go over some terms we'll be using a lot in this article.

What are in-app notifications?

In-app notifications are messages that pop up while your app is in-use to inform a user of relevant actions related to another screen in your application from their current screen. ChatKitty sends notifications to your app through the ChatKitty JavaScript SDK. You can listen for these notifications and use them to build in-app notification views.

What are push notifications?

Push notifications are short messages sent to mobile devices to alert a user when something of interests happen, and provide information related to that event even when your app isn't currently in-use. Push notifications are a great way to engage your users and improve your customer experience. Push notifications are a critical part of most chat apps and have traditionally been difficult to implement. However, the Expo framework provides seamless support for push notifications, simplifying the process of send push notifications to your users.

Installing notification libraries

For this project, you'll be using the react-native-in-app-notification library to provide a customizable in-app React Native component. Add the ChatKitty fork of react-native-in-app-notification that fixes this issue to your project:

# install react-native-in-app-notification
yarn add @chatkitty/react-native-in-app-notification

You'll also be using Expo push notifications, so install the Expo notifications, permissions, and other dependency modules you'll need to get expo push tokens needed to register user devices for push notifications:

# install the Expo push notifications modules
yarn add expo-constants expo-notifications expo-permissions

Handling user in-app notifications

To use react-native-in-app-notification within your app, you'll need to wrap your routes component with its provider component.

Edit the index.js file you created earlier in src/navigation/ with a InAppNotificationProvider component wrapping the existing Routes component.

The index.js file should contain:

import { InAppNotificationProvider } from '@chatkitty/react-native-in-app-notification';
import React from 'react';
import { DefaultTheme, Provider as PaperProvider } from 'react-native-paper';

import { AuthProvider } from './AuthProvider';
import Routes from './Routes';

export default function Providers() {
  return (
      <PaperProvider theme={theme}>
        <AuthProvider>
          <InAppNotificationProvider>
            <Routes />
          </InAppNotificationProvider>
        </AuthProvider>
      </PaperProvider>
  );
}

const theme = {
  ...DefaultTheme,
  roundness: 2,
  colors: {
    ...DefaultTheme.colors,
    primary: '#5b3a70',
    accent: '#50c878',
    background: '#f7f9fb',
  },
};

Next, you'll need to update the HomeStack.js file in the src/navigation/ directory to:

  • Wrap the chat component, which will be displaying the in-app notification, with the withInAppNotification higher-order component.

  • Register a ChatKitty onNotificationReceived event listener using a useEffect React hook to show received notifications.

The HomeStack.js file should contain:

import { withInAppNotification } from '@chatkitty/react-native-in-app-notification';
import { createStackNavigator } from '@react-navigation/stack';
import React, { useEffect } from 'react';
import { IconButton } from 'react-native-paper';

import { kitty } from '../chatkitty';
import BrowseChannelsScreen from '../screens/BrowseChannelsScreen';
import ChatScreen from '../screens/ChatScreen';
import CreateChannelScreen from '../screens/CreateChannelScreen';
import HomeScreen from '../screens/HomeScreen';

const ChatStack = createStackNavigator();
const ModalStack = createStackNavigator();

export default function HomeStack() {
  return (
      <ModalStack.Navigator mode="modal" headerMode="none">
        <ModalStack.Screen
            name="ChatApp"
            component={withInAppNotification(ChatComponent)}
        />
        <ModalStack.Screen name="CreateChannel" component={CreateChannelScreen} />
      </ModalStack.Navigator>
  );
}

function ChatComponent({ navigation, showNotification }) {
  useEffect(() => {
    return kitty.onNotificationReceived((notification) => {
      showNotification({
        title: notification.title,
        message: notification.body,
        onPress: () => {
          switch (notification.data.type) {
            case 'USER:SENT:MESSAGE':
            case 'SYSTEM:SENT:MESSAGE':
              kitty.getChannel(notification.data.channelId).then((result) => {
                navigation.navigate('Chat', { channel: result.channel });
              });
              break;
          }
        },
      });
    });
  }, [navigation, showNotification]);

  return (
      <ChatStack.Navigator
          screenOptions={{
            headerStyle: {
              backgroundColor: '#5b3a70',
            },
            headerTintColor: '#ffffff',
            headerTitleStyle: {
              fontSize: 22,
            },
          }}
      >
        <ChatStack.Screen
            name="Home"
            component={HomeScreen}
            options={(options) => ({
              headerRight: () => (
                  <IconButton
                      icon="plus"
                      size={28}
                      color="#ffffff"
                      onPress={() => options.navigation.navigate('BrowseChannels')}
                  />
              ),
            })}
        />
        <ChatStack.Screen
            name="BrowseChannels"
            component={BrowseChannelsScreen}
            options={(options) => ({
              headerRight: () => (
                  <IconButton
                      icon="plus"
                      size={28}
                      color="#ffffff"
                      onPress={() => options.navigation.navigate('CreateChannel')}
                  />
              ),
            })}
        />
        <ChatStack.Screen
            name="Chat"
            component={ChatScreen}
            options={({ route }) => ({
              title: route.params.channel.name,
            })}
        />
      </ChatStack.Navigator>
  );
}

If you run the app now, go to the home screen, and send a message from another device as another user, you should see an in-app notification:

Screenshot: In-app notification

Nice. Now your users can stay informed on the latest events happening across your app screens.

Let's now handle what happens when your users have left your app.

Getting a user's expo push token

To send a push notification to a user using Expo, we'll need their expo push token. Once we get the expo push token, we can then store it as a ChatKitty user property, so we can access it later in a chat function or on a back-end.

Update the HomeStack.js file to register the user's device for push notifications and then store the user's expo push token as a user property.

The HomeStack.js file should contain:

import { withInAppNotification } from '@chatkitty/react-native-in-app-notification';
import { createStackNavigator } from '@react-navigation/stack';
import Constants from 'expo-constants';
import * as Notifications from 'expo-notifications';
import * as Permissions from 'expo-permissions';
import React, { useEffect } from 'react';
import { IconButton } from 'react-native-paper';

import { kitty } from '../chatkitty';
import BrowseChannelsScreen from '../screens/BrowseChannelsScreen';
import ChatScreen from '../screens/ChatScreen';
import CreateChannelScreen from '../screens/CreateChannelScreen';
import HomeScreen from '../screens/HomeScreen';

const ChatStack = createStackNavigator();
const ModalStack = createStackNavigator();

export default function HomeStack() {
  useEffect(() => {
    registerForPushNotificationsAsync().then((token) => {
      kitty.updateCurrentUser((user) => {
        user.properties = {
          ...user.properties,
          'expo-push-token': token,
        };

        return user;
      });
    });
  }, []);

  return (
      <ModalStack.Navigator mode="modal" headerMode="none">
        <ModalStack.Screen
            name="ChatApp"
            component={withInAppNotification(ChatComponent)}
        />
        <ModalStack.Screen name="CreateChannel" component={CreateChannelScreen} />
      </ModalStack.Navigator>
  );
}

function ChatComponent({ navigation, showNotification }) {
  useEffect(() => {
    return kitty.onNotificationReceived((notification) => {
      showNotification({
        title: notification.title,
        message: notification.body,
        onPress: () => {
          switch (notification.data.type) {
            case 'USER:SENT:MESSAGE':
            case 'SYSTEM:SENT:MESSAGE':
              kitty.getChannel(notification.data.channelId).then((result) => {
                navigation.navigate('Chat', { channel: result.channel });
              });
              break;
          }
        },
      });
    });
  }, [navigation, showNotification]);

  return (
      <ChatStack.Navigator
          screenOptions={{
            headerStyle: {
              backgroundColor: '#5b3a70',
            },
            headerTintColor: '#ffffff',
            headerTitleStyle: {
              fontSize: 22,
            },
          }}
      >
        <ChatStack.Screen
            name="Home"
            component={HomeScreen}
            options={(options) => ({
              headerRight: () => (
                  <IconButton
                      icon="plus"
                      size={28}
                      color="#ffffff"
                      onPress={() => options.navigation.navigate('BrowseChannels')}
                  />
              ),
            })}
        />
        <ChatStack.Screen
            name="BrowseChannels"
            component={BrowseChannelsScreen}
            options={(options) => ({
              headerRight: () => (
                  <IconButton
                      icon="plus"
                      size={28}
                      color="#ffffff"
                      onPress={() => options.navigation.navigate('CreateChannel')}
                  />
              ),
            })}
        />
        <ChatStack.Screen
            name="Chat"
            component={ChatScreen}
            options={({ route }) => ({
              title: route.params.channel.name,
            })}
        />
      </ChatStack.Navigator>
  );
}

async function registerForPushNotificationsAsync() {
  let token;

  if (Constants.isDevice && Platform.OS !== 'web') {
    const {
      status: existingStatus,
    } = await Notifications.getPermissionsAsync();
    let finalStatus = existingStatus;
    if (existingStatus !== 'granted') {
      const { status } = await Notifications.requestPermissionsAsync();
      finalStatus = status;
    }
    if (finalStatus !== 'granted') {
      console.log('Failed to get push token for push notification!');
      return;
    }

    token = (await Notifications.getExpoPushTokenAsync()).data;
  } else {
    console.log('Must use physical device for Push Notifications');
  }

  if (Platform.OS === 'android') {
    await Notifications.setNotificationChannelAsync('default', {
      name: 'default',
      importance: Notifications.AndroidImportance.MAX,
      vibrationPattern: [0, 250, 250, 250],
      lightColor: '#FF231F7C',
    });
  }

  return token;
}

With that, you should have the user's expo push token as the expo-push-token user property.

Setting up Expo push notification credentials

For iOS, the managed Expo workflow handles push notification credentials automatically when you run expo build:ios. However, for Android you'll need to add an Android app to your Firebase project, update your project, and upload your FCM server credentials to Expo.

Adding Firebase credentials to the app

From the Firebase console side menu, go to your "Project settings".

Screenshot: Firebase project settings

Go to the "Your apps" section and click the Android icon:

Screenshot: Screenshot: Firebase add app

Fill out the application details and register your android app

Screenshot: Screenshot: Firebase create android app register

Download the google-services.json file and add it to your Expo React Native project's root directory

Screenshot: Screenshot: Firebase create android app download

In your app.json inside your project's root directory, add an android.googleServicesFile property with the relative path to the google-services.json file, as well as an android.package property with your app's Android package name:

{
  "expo": {
    ...
    "android": {
      "package": "com.yourpackage.yourcoolapp",
      "googleServicesFile": "./google-services.json"
    }
    ...
  }
}

Uploading FCM Server Credentials to Expo

To allow Expo to send push notifications to your Android app, you'll need to upload your FCM server key. Before you can upload your server key to Expo, you'll need to create an Expo account.

After creating your Expo account, login into Expo by running

# enter your Expo credentials when prompted
expo login

From the "Project settings" section of your Firebase project, go to the "Cloud Messaging" tab, copy the "Server key" value and upload it to Expo:

# replace <your-token-here> with your server key
expo push:android:upload --api-key <your-token-here>

Cool, with Expo set up, let's create a ChatKitty chat function to use Expo to send a push notification when a ChatKitty notification event happens.

Adding Expo to your Chat Runtime

ChatKitty makes it easy to integrate your back-end and external services like Expo into a ChatKitty application using Chat Functions. Chat Functions let you write arbitrary code that runs any time a relevant event or action happens inside your app. We'll be using a chat function to send a push notification whenever an event occurs that a user should be notified about, and the user isn't online. With ChatKitty, you can use any NPM package inside your Chat Functions as a Chat Runtime dependency.

From your ChatKitty application dashboard, go to the "Functions" page:

Screenshot: ChatKitty side menu functions

Go to the "Runtime" tab and add a new dependency to the Expo Server SDK NPM package, expo-server-sdk. Version 3.6.0 was the latest version as of the time this article was written.

Screenshot: ChatKitty runtime add expo Remember to click the "Save" icon to confirm your chat runtime dependencies changes.

Now we're ready to define a chat function to send a push notification using Expo, whenever a user should be notified about an event, and the user is offline.

Sending push notifications using a chat function

From your ChatKitty application dashboard, go to the "Functions" page and select the "User Received Notification" event chat function:

Screenshot: ChatKitty chat functions

This chat function runs whenever an event a user can be notified about happens. Edit the chat function to send a push notification if the user isn't currently online.

const { Expo } = require('expo-server-sdk');

const expo = new Expo(); // create Expo client

async function handleEvent(
  event: UserReceivedNotificationEvent,
  context: Context
) {
  if (event.userHasActiveSession) return; // skip if this user is online

  const expoPushToken = event.user.properties['expo-push-token']; // get the expo push token registered

  if (!expoPushToken || !Expo.isExpoPushToken(expoPushToken)) return; // check expo push token is present and valid

  const notification = event.notification;

  // send push notification with Expo
  await expo.sendPushNotificationsAsync([
    {
      to: expoPushToken,
      sound: 'default',
      title: notification.title,
      body: notification.body,
      data: notification.data,
    },
  ]);
}

Screenshot: ChatKitty chat function user received notification Remember to click the "Save" icon to confirm your chat function changes.

If you close the app now, and send a message from another device as another user, you should see a push notification:

Screenshot: Push notification

Conclusion

Pretty cool, you've completed the third part of this tutorial series and successfully implemented push notifications, using the Expo framework and ChatKitty Chat Functions. You've also implemented in-app notifications that seamlessly inform your users when something they care about happens. Your users are now always in the loop.

What's next?

In the next post of this series, we'll be enhancing your chat app's user experience with direct messaging, typing indicators, and chat presence notifications. Stay tuned for more. 🔥

If you have any questions, comments or need help with any part of this article, please email me at aaron@chatkitty.com, and I'll be happy to help.

You can find the complete source code for this project inside this GitHub repository.

👉 Checkout the other blog posts in this series:


This article contains materials adapted from "Chat app with React Native" by Aman Mittal, originally published at Heartbeat.Fritz.Ai.

This article features an image by Volodymyr Hryshchenko.