Skip to main content

4 posts tagged with "Tutorial"

View All Tags

ยท 18 min read
Aaron Nwabuoku

Building a Chat App with React Native and Gifted Chat (Part 2)

In this tutorial series , I'll be showing you how to build a functional and secure chat app using the latest React Native libraries, including Gifted Chat and the Expo framework powered by the ChatKitty platform.


In the first article of this series, you learned how to use Firebase along with ChatKitty Chat Functions to implement a secure yet simple user login flow by proxying Firebase Authentication through ChatKitty. Along with that, you built a couple of screens with the react-native-paper UI library to allow users to register for your chat app and login into the app.

In this tutorial, you'll be using the Gifted Chat React Native library to create a full featured chat screen with its out of the box features. You'll also use ChatKitty's JavaScript Chat SDK to add real-time messaging to your chat app.

After reading this article, you will be able to:

  1. Create public channels for users to join

  2. View all channels a user can join and discover channels created by other users

  3. Integrate the react-native-gifted-chat library to implement a group chat screen

You can checkout our Expo React Native sample code any time on GitHub.

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
npm update chatkitty

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

What are channels?โ€‹

Channels are the backbone of the ChatKitty chat experience. Users can join channels and receive or send messages. ChatKitty broadcasts messages created in channels to channel member users with active chat sessions and sends notifications to offline members.

What are chat sessions?โ€‹

Before a user can begin sending and receiving real-time messages and use in-app chat features like typing indicators, delivery and read receipts, live reactions, etc, their device needs to start a chat session. A user device can start up to 10 chat sessions at a time.

With that, you have all the information you need build to chat into your app.

Let's go! ๐ŸŽ๏ธ

First, you'll start by creating a screen that shows a list of channels a user can chat in after logging in.

Displaying a user's channelsโ€‹

Start by changing the homeScreen.js you previously created to list the channels a logged in user is a member of.

The homeScreen.js file should contain:

src/screens/homeScreen.js
import { useIsFocused } from '@react-navigation/native';
import React, { useEffect, useState } from 'react';
import { FlatList, StyleSheet, View } from 'react-native';
import { Divider, List } from 'react-native-paper';

import chatkitty from '../chatkitty';
import Loading from '../components/loading';

export default function HomeScreen() {
const [channels, setChannels] = useState([]);
const [loading, setLoading] = useState(true);

const isFocused = useIsFocused();

useEffect(() => {
let isCancelled = false;

chatkitty.listChannels({ filter: { joined: true } }).then((result) => {
if (!isCancelled) {
setChannels(result.paginator.items);

if (loading) {
setLoading(false);
}
}
});

return () => {
isCancelled = true;
};
}, [isFocused, loading]);

if (loading) {
return <Loading />;
}

return (
<View style={styles.container}>
<FlatList
data={channels}
keyExtractor={(item) => item.id.toString()}
ItemSeparatorComponent={() => <Divider />}
renderItem={({ item }) => (
<List.Item
title={item.name}
description={item.type}
titleNumberOfLines={1}
titleStyle={styles.listTitle}
descriptionStyle={styles.listDescription}
descriptionNumberOfLines={1}
onPress={() => {
// TODO navigate to a chat screen.
}}
/>
)}
/>
</View>
);
}

const styles = StyleSheet.create({
container: {
backgroundColor: '#f5f5f5',
flex: 1,
},
listTitle: {
fontSize: 22,
},
listDescription: {
fontSize: 16,
},
});

If you run the app and log in now, it shouldn't look like much.

Screenshot: Home screen empty

Pretty empty huh? Soon you'll create a screen responsible for creating new channels, so the home screen can be populated.

Creating shared header components and a modal stack navigatorโ€‹

Before we create the CreateChannel screen, we should modify the app header bar to share options across different screens. The CreateChannel screen will be implemented as a modal, so we'll also need a separate stack navigator to wrap the home stack navigator and handle modals. Modals are screens that block interactions with the main view when displaying their content.

Modify your homeStack.js file in the src/navigation/ directory to apply header bar options across screens and define a stack navigator for app modal screens.

The homeStack.js file should contain:

src/navigation/homeStack.js
import { createStackNavigator } from '@react-navigation/stack';
import React from 'react';

import HomeScreen from '../screens/homeScreen';

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

export default function HomeStack() {
return (
<ModalStack.Navigator screenOptions={{
headerShown: false,
presentation: "modal"
}}>
<ModalStack.Screen name="ChatApp" component={ChatComponent} />
</ModalStack.Navigator>
);
}

function ChatComponent() {
return (
<ChatStack.Navigator
screenOptions={{
headerStyle: {
backgroundColor: '#5b3a70',
},
headerTintColor: '#ffffff',
headerTitleStyle: {
fontSize: 22,
},
}}
>
<ChatStack.Screen name="Home" component={HomeScreen} />
</ChatStack.Navigator>
);
}

Creating a channel creation screenโ€‹

Now we can create a new screen file createChannelScreen.js inside the src/screens/ directory. From this screen, users will create new public channels other users can join and chat in.

The createChannelScreen.js file should contain:

src/screens/createChannelScreen.js
import React, { useState } from 'react';
import { StyleSheet, View } from 'react-native';
import { IconButton, Title } from 'react-native-paper';

import chatkitty from '../chatkitty';
import FormButton from '../components/formButton';
import FormInput from '../components/formInput';

export default function CreateChannelScreen({ navigation }) {
const [channelName, setChannelName] = useState('');

function handleButtonPress() {
if (channelName.length > 0) {
chatkitty
.createChannel({
type: 'PUBLIC',
name: channelName,
})
.then(() => navigation.navigate('Home'));
}
}

return (
<View style={styles.rootContainer}>
<View style={styles.closeButtonContainer}>
<IconButton
icon="close-circle"
size={36}
color="#5b3a70"
onPress={() => navigation.goBack()}
/>
</View>
<View style={styles.innerContainer}>
<Title style={styles.title}>Create a new channel</Title>
<FormInput
labelName="Channel Name"
value={channelName}
onChangeText={(text) => setChannelName(text)}
clearButtonMode="while-editing"
/>
<FormButton
title="Create"
modeValue="contained"
labelStyle={styles.buttonLabel}
onPress={() => handleButtonPress()}
disabled={channelName.length === 0}
/>
</View>
</View>
);
}

const styles = StyleSheet.create({
rootContainer: {
flex: 1,
},
closeButtonContainer: {
position: 'absolute',
top: 30,
right: 0,
zIndex: 1,
},
innerContainer: {
flex: 1,
justifyContent: 'center',
alignItems: 'center',
},
title: {
fontSize: 24,
marginBottom: 10,
},
buttonLabel: {
fontSize: 22,
},
});

Okay, let's test the CreateChannel screen by adding a temporary button to open the screen in our home screen header bar, and creating a new channel.

The homeStack.js file should contain:

src/navigation/homeStack.js
import { createStackNavigator } from '@react-navigation/stack';
import React from 'react';
import { IconButton } from 'react-native-paper';

import CreateChannelScreen from '../screens/createChannelScreen';
import HomeScreen from '../screens/homeScreen';

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

export default function HomeStack() {
return (
<ModalStack.Navigator screenOptions={{
headerShown: false,
presentation: "modal"
}}>
<ModalStack.Screen name="ChatApp" component={ChatComponent} />
<ModalStack.Screen name="CreateChannel" component={CreateChannelScreen} />
</ModalStack.Navigator>
);
}

function ChatComponent() {
return (
<ChatStack.Navigator
screenOptions={{
headerStyle: {
backgroundColor: '#5b3a70',
},
headerTintColor: '#ffffff',
headerTitleStyle: {
fontSize: 22,
},
}}
>
<ChatStack.Screen
name="Home"
component={HomeScreen}
options={({ navigation }) => ({
headerRight: () => (
<IconButton
icon= "plus"
size={28}
iconColor="#ffffff"
onPress={() => navigation.navigate('CreateChannel')}
/>
),
})}
/>
</ChatStack.Navigator>
);
}

If you run the app now, you should see a plus icon in the header bar:

Screenshot: Home screen add

Tap the button and create a new channel:

Screenshot: Create channel screen

Tap "Create", and you should be redirected back to the home screen with your new channel:

Screenshot: Home screen added

You now have a channel to send messages, receive messages and chat in. Next, let's get started building a channel chat screen with the react-native-gifted-chat library.

Creating a chat screenโ€‹

Gifted Chat is an open-source React Native library that saves you tons of time and development effort building chat UIs. The library is extensible and customizable with a large online community making it a great option to build chat.

To use Gifted Chat, add its NPM package to your Expo React Native project:

npm i react-native-gifted-chat

Next, create a file chatScreen.js inside the src/screens/ directory. This screen will render a chat screen for users to send new messages and view messages they've sent and received. We'll be updating this screen throughout the rest of this tutorial series to add more advanced and sophisticated chat features.

chatScreen.js will need quite a few things, so let's break down what you'll be doing:

  • Import GiftedChat since we need a GiftedChat component to add the chat UI and functionality.

  • Retrieve the current user from our authentication context, so we can show messages as created by the current user and perform other current user specific functions.

  • Retrieve the channel to start this chat with using the route props.

  • Create a ChatScreen functional React component, and inside it define a messages state variable. This array will hold message data objects representing the chat message history. This variable is initially an empty array.

  • Define a couple of helper functions mapUser and mapMessage to map the current user and message objects we get from ChatKitty into a schema Gifted Chat recognizes.

  • Use an useEffect React hook to start a new chat session with the ChatScreen channel using the ChatKitty startChatSession function. Register an onMessageReceived function that appends new messages received from ChatKitty into the existing Gifted Chat managed messages, and replace the messages state when a new message is received. After starting the chat session, fetch the channel's last messages using listMessages then replace the messages state. As part of cleaning up when the component is about to be destroyed, return the ChatSession's end function to the useEffect function to end the chat session and free up ChatKitty resources.

  • Define a helper function handleSend, to send a new message using the ChatKitty sendMessage function.

  • Return to be rendered a GiftedChat with the messages state, a mapped GiftedChat current user chatUser, and the handleSend helper function.

The chatScreen.js file should contain:

src/screens/chatScreen.js
import React, { useContext, useEffect, useState } from 'react';
import { Bubble, GiftedChat } from 'react-native-gifted-chat';

import chatkitty from '../chatkitty';
import Loading from '../components/loading';
import { AuthContext } from '../navigation/authProvider';

export default function ChatScreen({ route }) {
const { user } = useContext(AuthContext);
const { channel } = route.params;

const [messages, setMessages] = useState([]);
const [loading, setLoading] = useState(true);

useEffect(() => {
const startChatSessionResult = chatkitty.startChatSession({
channel: channel,
onMessageReceived: (message) => {
setMessages((currentMessages) =>
GiftedChat.append(currentMessages, [mapMessage(message)])
);
},
});

chatkitty
.listMessages({
channel: channel,
})
.then((result) => {
setMessages(result.paginator.items.map(mapMessage));

setLoading(false);
});

return startChatSessionResult.session.end;
}, [user, channel]);

async function handleSend(pendingMessages) {
await chatkitty.sendMessage({
channel: channel,
body: pendingMessages[0].text,
});
}

function renderBubble(props) {
return (
<Bubble
{...props}
wrapperStyle={{
left: {
backgroundColor: '#d3d3d3',
},
}}
/>
);
}

if (loading) {
return <Loading />;
}

return (
<GiftedChat
messages={messages}
onSend={handleSend}
user={mapUser(user)}
renderBubble={renderBubble}
/>
);
}

function mapMessage(message) {
return {
_id: message.id,
text: message.body,
createdAt: new Date(message.createdTime),
user: mapUser(message.user),
};
}

function mapUser(user) {
return {
_id: user.id,
name: user.displayName,
avatar: user.displayPictureUrl,
};
}

Now, let's add the Chat screen to the home stack navigator. Edit homeStack.js in src/navigation/ with a new screen entry.

The homeStack.js file should contain:

src/navigation/homeStack.js
import { createStackNavigator } from '@react-navigation/stack';
import React from 'react';
import { IconButton } from 'react-native-paper';

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 screenOptions={{
headerShown: false,
presentation: "modal"
}}>
<ModalStack.Screen name="ChatApp" component={ChatComponent} />
<ModalStack.Screen name="CreateChannel" component={CreateChannelScreen} />
</ModalStack.Navigator>
);
}

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

Before we can begin chatting, you'll need to update the homeScreen.js component to redirect to a Channel screen.

The homeScreen.js file should contain:

src/screens/homeScreen.js
import { useIsFocused } from '@react-navigation/native';
import React, { useEffect, useState } from 'react';
import { FlatList, StyleSheet, View } from 'react-native';
import { Divider, List } from 'react-native-paper';

import chatkitty from '../chatkitty';
import Loading from '../components/loading';

export default function HomeScreen({ navigation }) {
const [channels, setChannels] = useState([]);
const [loading, setLoading] = useState(true);

const isFocused = useIsFocused();

useEffect(() => {
let isCancelled = false;

chatkitty.listChannels({ filter: { joined: true } }).then((result) => {
if (!isCancelled) {
setChannels(result.paginator.items);

if (loading) {
setLoading(false);
}
}
});

return () => {
isCancelled = true;
};
}, [isFocused, loading]);

if (loading) {
return <Loading />;
}

return (
<View style={styles.container}>
<FlatList
data={channels}
keyExtractor={(item) => item.id.toString()}
ItemSeparatorComponent={() => <Divider />}
renderItem={({ item }) => (
<List.Item
title={item.name}
description={item.type}
titleNumberOfLines={1}
titleStyle={styles.listTitle}
descriptionStyle={styles.listDescription}
descriptionNumberOfLines={1}
onPress={() => navigation.navigate('Chat', { channel: item })}
/>
)}
/>
</View>
);
}

const styles = StyleSheet.create({
container: {
backgroundColor: '#f5f5f5',
flex: 1,
},
listTitle: {
fontSize: 22,
},
listDescription: {
fontSize: 16,
},
});

If you run the app, you should now be able to send messages in the channel you created:

Screenshot: Channel chat screen sent message

Awesome! You've successfully added chat into your app. However, before we bring out the champagne there's a couple of things we should do. Firstly, we're currently only getting a slice of the channel message history, as the ChatKitty API paginates all collections. Currently, if the channel had more than the default messages page size (25 items) messages, then the messages response would be truncated and messages older than the 25th message would not be returned.

Screenshot: Channel chat screen no pagination

We can't see messages before 25.

Secondly, although it's nice we can chat with ourselves, it'll be really cool if users can find channels created by other users, or by your backend that they have permission to join. We can create a browse channel screen for users to see public channels created by other users.

Loading earlier messagesโ€‹

To load older messages in pages before the last channel page, we can use the paginator object returned with the ChatKitty getMessages result to fetch more pages and prepend the messages fetched into our messages collection.

// Fetching a next page:

const result = kitty.getMessages({ channel: channel });

const paginator = result.paginator; // paginator from result

if (paginator.hasNextPage) { // check if there are more pages
const nextPaginator = await messagePaginator.nextPage();

const nextMessages = nextPaginator.items;
}

Now, let's use paginators to load more messages into our chat. Edit the chatScreen.js file in src/screens/ to load more messages using Gifted Chat.

The chatScreen.js file should contain:

src/screens/chatScreen.js
import React, { useContext, useEffect, useState } from 'react';
import { Bubble, GiftedChat } from 'react-native-gifted-chat';

import chatkitty from '../chatkitty';
import Loading from '../components/loading';
import { AuthContext } from '../navigation/authProvider';

export default function ChatScreen({ route }) {
const { user } = useContext(AuthContext);
const { channel } = route.params;

const [messages, setMessages] = useState([]);
const [loading, setLoading] = useState(true);
const [loadEarlier, setLoadEarlier] = useState(false);
const [isLoadingEarlier, setIsLoadingEarlier] = useState(false);
const [messagePaginator, setMessagePaginator] = useState(null);

useEffect(() => {
const startChatSessionResult = chatkitty.startChatSession({
channel: channel,
onMessageReceived: (message) => {
setMessages((currentMessages) =>
GiftedChat.append(currentMessages, [mapMessage(message)])
);
},
});

chatkitty
.listMessages({
channel: channel,
})
.then((result) => {
setMessages(result.paginator.items.map(mapMessage));

setMessagePaginator(result.paginator);
setLoadEarlier(result.paginator.hasNextPage);

setLoading(false);
});

return startChatSessionResult.session.end;
}, [user, channel]);

async function handleSend(pendingMessages) {
await chatkitty.sendMessage({
channel: channel,
body: pendingMessages[0].text,
});
};

async function handleLoadEarlier() {
if (!messagePaginator.hasNextPage) {
setLoadEarlier(false);

return;
}

setIsLoadingEarlier(true);

const nextPaginator = await messagePaginator.nextPage();

setMessagePaginator(nextPaginator);

setMessages((currentMessages) =>
GiftedChat.prepend(currentMessages, nextPaginator.items.map(mapMessage))
);

setIsLoadingEarlier(false);
}

function renderBubble(props) {
return (
<Bubble
{...props}
wrapperStyle={{
left: {
backgroundColor: '#d3d3d3',
},
}}
/>
);
}

if (loading) {
return <Loading />;
}

return (
<GiftedChat
messages={messages}
onSend={handleSend}
user={mapUser(user)}
loadEarlier={loadEarlier}
isLoadingEarlier={isLoadingEarlier}
onLoadEarlier={handleLoadEarlier}
renderBubble={renderBubble}
/>
);
}

function mapMessage(message) {
return {
_id: message.id,
text: message.body,
createdAt: new Date(message.createdTime),
user: mapUser(message.user),
};
}

function mapUser(user) {
return {
_id: user.id,
name: user.displayName,
avatar: user.displayPictureUrl,
};
}

If you run the app now, you should see an option to load more messages if you have a lot of messages.

Screenshot: Channel chat screen paginated

If you tap it, the next set of messages loads, repeatable until the beginning of the conversation.

Screenshot: Channel chat screen paginated loaded more

Much better. Now, let's provide a screen for users to discover new channels.

Creating a browse channels screenโ€‹

The browse channels screen is going to be very similar to the home screen, but instead of listing channels a user is already a member of, it will list channels a user can join.

Create a new file browseChannelsScreen.js in src/screens/. This component will display a list of joinable channels from ChatKitty.

The browseChannelsScreen.js file should contain:

src/screens/browseChannelsScreen.js
import { useIsFocused } from '@react-navigation/native';
import React, { useEffect, useState } from 'react';
import { FlatList, StyleSheet, View } from 'react-native';
import { Divider, List } from 'react-native-paper';

import { kitty } from '/blog/chatkitty';
import Loading from '/blog/components/Loading';

export default function BrowseChannelsScreen({ navigation }) {
const [channels, setChannels] = useState([]);
const [loading, setLoading] = useState(true);

const isFocused = useIsFocused();

useEffect(() => {
kitty.getChannels({ filter: { joined: false } }).then((result) => {
setChannels(result.paginator.items);

if (loading) {
setLoading(false);
}
});
}, [isFocused, loading]);

async function handleJoinChannel(channel) {
const result = await kitty.joinChannel({ channel: channel });

navigation.navigate('Chat', { channel: result.channel });
}

if (loading) {
return <Loading />;
}

return (
<View style={styles.container}>
<FlatList
data={channels}
keyExtractor={(item) => item.id.toString()}
ItemSeparatorComponent={() => <Divider />}
renderItem={({ item }) => (
<List.Item
title={item.name}
description={item.type}
titleNumberOfLines={1}
titleStyle={styles.listTitle}
descriptionStyle={styles.listDescription}
descriptionNumberOfLines={1}
onPress={() => handleJoinChannel(item)}
/>
)}
/>
</View>
);
}

const styles = StyleSheet.create({
container: {
backgroundColor: '#f5f5f5',
flex: 1,
},
listTitle: {
fontSize: 22,
},
listDescription: {
fontSize: 16,
},
});

Next, let's add our new screen to our home stack. Open the homeStack.js file in src/navigation/ and add the BrowseChannels screen. Also, let's make it so that clicking the "Add" icon button from the home screen takes us to the BrowseChannels screen. Then, let's create another "Add" icon button for the BrowseChannel screen that now opens the CreateChannel screen.

The homeStack.js file should contain:

src/navigation/homeStack.js
import { createStackNavigator } from '@react-navigation/stack';
import React from 'react';
import { IconButton } from 'react-native-paper';

import BrowseChannelsScreen from '/blog/screens/BrowseChannelsScreen';
import ChatScreen from '/blog/screens/ChatScreen';
import CreateChannelScreen from '/blog/screens/CreateChannelScreen';
import HomeScreen from '/blog/screens/HomeScreen';

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

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

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

Let's test the browse channels functionality by logging in as another user.

Screenshot: Login

After logging in, if you navigate to the browse screen using the "Add" icon button, you should see the channel created earlier by the other user.

Screenshot: Browse channels screen

Let's say hello!

Screenshot: Channel chat screen another user

Conclusionโ€‹

Amazing, you've completed the second part of this tutorial series and made your chat app even better. By implementing screens and functionality to allow users to create, discover and join channels, your users can beginning having meaningful conversations. You also used the Gifted Chat React Native library and ChatKitty to build a chat screen to send and receive real-time messages in minutes. It doesn't get much easier than that. Congratulations! ๐Ÿพ

What's next?โ€‹

In the next post of this series, we'll be handling what happens when a user is away from a chat screen or offline. We'll be using in-app notifications to inform a user when a new message is received, or relevant action happens in a channel they've joined. We'll also be using Expo push notifications to inform users about new messages when they're not connected to your app and are offline. Stay tuned for more. ๐Ÿ”ฅ

Like always, if you have any questions, comments or need help with any part of this article, join our Discord Server where you can ask questions, discuss what you're working on, and I'll be more than 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.

ยท 25 min read
Aaron Nwabuoku

Building a Chat App with React Native and Firebase

In this tutorial series, I'll be showing you how to build a functional and secure chat app using the latest React Native libraries, the Expo framework, and Firebase, powered by the ChatKitty platform. Part 1 covers using Firebase Authentication and ChatKitty Chat Functions to securely implement user registration and login.


This is the first article of this series. After reading this article, you will be able to:

  1. Create an Expo React Native application

  2. Create a Firebase project for user authentication

  3. Create a ChatKitty project and connect to ChatKitty to provide real-time chat functionality

  4. Use Firebase Authentication and ChatKitty Chat Functions to securely implement user login

What is React Native?โ€‹

React Native is a great way to develop both web and mobile applications very quickly, while sharing a lot of code when targeting multiple platforms. With a mature ecosystem of libraries and tooling, using React Native is not only fast but also reliable. Trusted by organizations like Facebook, Shopify, and Tesla - React Native is a stable framework for building both iOS and Android apps.

What is Expo?โ€‹

The Expo framework builds on top of React Native to allow developers to build universal React applications in minutes. With Expo, you can develop, build, deploy and quickly iterate on iOS, Android and web apps from the same JavaScript code. Expo has made creating both web and mobile applications very accessible, handling would-be complex workflows like multi-platform deployment and advanced features like push notifications.

What is Firebase?โ€‹

Firebase is a Backend-as-a-Service offering by Google. It provides developers a wide array of tools and services to develop quality apps without having to manage servers. Firebase provides key features like authentication, a real-time database, and hosting.

What are ChatKitty Chat Functions?โ€‹

ChatKitty provides Chat Functions, serverless cloud functions that allow you to define custom logic for complex tasks like user authentication, and respond to chat events that happen within your application. With ChatKitty Chat Functions, there's no need for you to build a backend to develop chat apps. ChatKitty Chat Functions auto-scale for you, and only cost you when they run. Chat Functions lower the total cost of maintaining your chat app, enabling you to build more logic, faster.

Prerequisitesโ€‹

To develop apps with Expo and React Native, you should be able to write and understand JavaScript or TypeScript code. To define ChatKitty Chat Functions, you'll need to be familiar with basic JavaScript.

You'll need a version of Node.js above 10.x.x installed on your local machine to build this React Native app.

You'll need to install the Expo CLI tool through npx.

For a complete walk-through on how to set up a development environment for Expo, you can go through the official documentation here.

You can checkout our Expo React Native sample code any time on GitHub.

Creating project and installing librariesโ€‹

First, initialize a new Expo project with the blank managed workflow. To do so, you're going to need to open a terminal window and execute:

npx expo init chatkitty-example-react-native

After creating the initial application. You can enter the app root directory and run the app:

# navigate inside the project directory
cd chatkitty-example-react-native

# for android
npx expo start --android

# for ios
npx expo start --ios

# for web
npx expo start --web

If you run your newly created React Native application using Expo, you should see:

Screenshot: Created Project

Now we have our blank project, we can install the React Native libraries we'll need:

# install following libraries for React Native
expo install @react-navigation/native @react-navigation/stack react-native-reanimated react-native-gesture-handler react-native-screens react-native-safe-area-context @react-native-community/masked-view react-native-paper react-native-vector-icons firebase

Creating reusable form elementsโ€‹

We'll be creating Login and Signup screens soon which share similar logic. To prevent us from violating the DRY principle, let's create some reusable form components that we can share across these two screens. We'll also create a loading spinner component to provide a good user experience whenever a user waits for a long screen transition.

We'll create reusable FormInput, FormButton, and Loading UI components. At the root of this Expo React Native app, create a src/ directory and inside it create another new components/ directory.

Inside the src/components/ directory, create a new JavaScript file formInput.js. In this file, we'll define a React component to provide a text input field for our Login and Signup screens to use for the user to enter their credentials.

The formInput.js file should contain the following code snippet:

src/components/formInput.js
import React from 'react';
import { Dimensions, StyleSheet } from 'react-native';
import { TextInput } from 'react-native-paper';

const { width, height } = Dimensions.get('screen');

export default function FormInput({ labelName, ...rest }) {
return (
<TextInput
label={labelName}
style={styles.input}
numberOfLines={1}
{...rest}
/>
);
}

const styles = StyleSheet.create({
input: {
marginTop: 10,
marginBottom: 10,
width: width / 1.5,
height: height / 15,
},
});

Our next reusable component is going to be in another file formButton.js. We use it to display a button for a user to confirm their credentials.

The formButton.js file should contain:

src/components/formButton.js
import React from 'react';
import { Dimensions, StyleSheet } from 'react-native';
import { Button } from 'react-native-paper';

const { width, height } = Dimensions.get('screen');

export default function FormButton({ title, modeValue, ...rest }) {
return (
<Button
mode={modeValue}
{...rest}
style={styles.button}
contentStyle={styles.buttonContainer}
>
{title}
</Button>
);
}

const styles = StyleSheet.create({
button: {
marginTop: 10,
},
buttonContainer: {
width: width / 2,
height: height / 15,
},
});

Finally, create a loading.js file. We'll use it to display a loading spinner when a user waits for a screen transition.

The loading.js file should contain:

src/components/loading.js
import React from 'react';
import { ActivityIndicator, StyleSheet, View } from 'react-native';

export default function Loading() {
return (
<View style={styles.loadingContainer}>
<ActivityIndicator size="large" color="#5b3a70" />
</View>
);
}

const styles = StyleSheet.create({
loadingContainer: {
flex: 1,
alignItems: 'center',
justifyContent: 'center',
},
});

Now we have our reusable form components, we can create a login screen for users to enter our chat app.

Creating a login screenโ€‹

The first screen we'll be creating is the login screen. We will ask an existing user for their email and password to authenticate and provide a link to a sign up form for new users to register with our app.


The login screen should look like this after you're done:

Screenshot: Login screen

Inside the src/, create a screens/ directory, inside this directory create a loginScreen.js file.

The loginScreen.js file should contain:

src/screens/loginScreen.js
import React, { useState } from 'react';
import { StyleSheet, View } from 'react-native';
import { Title } from 'react-native-paper';

import FormButton from '../components/formButton.js';
import FormInput from '../components/formInput.js';

export default function LoginScreen({ navigation }) {
const [email, setEmail] = useState('');
const [password, setPassword] = useState('');

return (
<View style={styles.container}>
<Title style={styles.titleText}>Welcome!</Title>
<FormInput
labelName="Email"
value={email}
autoCapitalize="none"
onChangeText={(userEmail) => setEmail(userEmail)}
/>
<FormInput
labelName="Password"
value={password}
secureTextEntry={true}
onChangeText={(userPassword) => setPassword(userPassword)}
/>
<FormButton
title="Login"
modeValue="contained"
labelStyle={styles.loginButtonLabel}
onPress={() => {
// TODO
}}
/>
<FormButton
title="Sign up here"
modeValue="text"
uppercase={false}
labelStyle={styles.navButtonText}
onPress={() => navigation.navigate('Signup')}
/>
</View>
);
}

const styles = StyleSheet.create({
container: {
backgroundColor: '#f5f5f5',
flex: 1,
justifyContent: 'center',
alignItems: 'center',
},
titleText: {
fontSize: 24,
marginBottom: 10,
},
loginButtonLabel: {
fontSize: 22,
},
navButtonText: {
fontSize: 16,
},
});

Later, you'll hook up this login screen to ChatKitty to log in users into your app. We've also configured the navigation prop to navigate the user to the signup screen you'll soon be creating. For now, let's add a stack navigator to direct users to the initial login route.

Create a new directory src/navigation/. This will contain all the routes and components needed to build the app's navigation. Inside this directory, create a authStack.js file.

The authStack.js file should contain:

src/navigation/authStack.js
import { createStackNavigator } from '@react-navigation/stack';
import React from 'react';

import LoginScreen from '../screens/loginScreen';

const Stack = createStackNavigator();

export default function AuthStack() {
return (
<Stack.Navigator initialRouteName="Login" headerMode="none">
<Stack.Screen name="Login" component={LoginScreen} />
</Stack.Navigator>
);
}

Later, you'll be adding another route for the Signup screen to our navigator.

Next, you'll need a navigation container to hold the app's stacks, starting with the auth stack. Create a routes.js file inside the src/navigation/ directory.

The routes.js file should contain:

src/navigation/routes.js
import { NavigationContainer } from '@react-navigation/native';
import React from 'react';

import AuthStack from './authStack';

export default function Routes() {
return (
<NavigationContainer>
<AuthStack />
</NavigationContainer>
);
}

We'll also be wrapping the app's routes in a paper provider that provides a theme for all the app components.

Create an index.js inside src/navigation/. This file should contain:

src/navigation/index.js
import React from 'react';
import { DefaultTheme, Provider as PaperProvider } from 'react-native-paper';

import Routes from './routes';

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

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

Next, let's update the App.js file in the project root directory to use our providers.

The App.js file should now contain:

App.js
import React from 'react';

import Providers from './src/navigation';

export default function App() {
return <Providers />;
}

Now, if you run the app, you should see the login screen:

Screenshot: Login screen

Creating a sign up screenโ€‹

You'll need a way for a new user to sign up for your chat app, so let's build a sign up screen. We'll ask a new user for their email with a new password, as well as a display name that will be shown to other users in the app.

The sign up screen should look like this after you're done:

Screenshot: Signup screen

Inside the src/screens/ directory, create a signupScreen.js file to hold your Signup screen component.

The signupScreen.js file should contain:

src/screens/signupScreen.js
import React, { useContext, useState } from 'react';
import { StyleSheet, View } from 'react-native';
import { IconButton, Title } from 'react-native-paper';

import FormButton from '../components/formButton';
import FormInput from '../components/formInput';

export default function SignupScreen({ navigation }) {
const [displayName, setDisplayName] = useState('');
const [email, setEmail] = useState('');
const [password, setPassword] = useState('');

return (
<View style={styles.container}>
<Title style={styles.titleText}>Let's get started!</Title>
<FormInput
labelName="Display Name"
value={displayName}
autoCapitalize="none"
onChangeText={(userDisplayName) => setDisplayName(userDisplayName)}
/>
<FormInput
labelName="Email"
value={email}
autoCapitalize="none"
onChangeText={(userEmail) => setEmail(userEmail)}
/>
<FormInput
labelName="Password"
value={password}
secureTextEntry={true}
onChangeText={(userPassword) => setPassword(userPassword)}
/>
<FormButton
title="Signup"
modeValue="contained"
labelStyle={styles.loginButtonLabel}
onPress={() => {
// TODO
}}
/>
<IconButton
icon="keyboard-backspace"
size={30}
style={styles.navButton}
color="#5b3a70"
onPress={() => navigation.goBack()}
/>
</View>
);
}

const styles = StyleSheet.create({
container: {
backgroundColor: '#f5f5f5',
flex: 1,
justifyContent: 'center',
alignItems: 'center',
},
titleText: {
fontSize: 24,
marginBottom: 10,
},
loginButtonLabel: {
fontSize: 22,
},
navButtonText: {
fontSize: 18,
},
navButton: {
marginTop: 10,
},
});

Later, you'll hook up this screen to Firebase and ChatKitty to create the new user profile and begin a new chat session. Now, like with the login screen, let's add this screen to the auth stack navigator.

Edit the authStack.js file you created earlier in src/navigation/ with a stack screen component for the sign up screen.

The authStack.js file should contain:

src/navigation/authStack.js
import { createStackNavigator } from '@react-navigation/stack';
import React from 'react';

import LoginScreen from '../screens/loginScreen';
import SignupScreen from '../screens/signupScreen';

const Stack = createStackNavigator();

export default function AuthStack() {
return (
<Stack.Navigator initialRouteName="Login" headerMode="none">
<Stack.Screen name="Login" component={LoginScreen} />
<Stack.Screen name="Signup" component={SignupScreen} />
</Stack.Navigator>
);
}

If you run the app and tap the "Sign up here" form button text on the login screen, you should see the sign up screen:

Screenshot: Signup screen

With both screens we need for user authentication, let's create a home screen to redirect users after they authenticate.

Creating a home screenโ€‹

For now let's define a placeholder home screen to redirect users after they log in or sign up. In future tutorials, we'll flesh out this screen with complete real-time chat functionality.

Inside the src/screens/ directory, create a homeScreen.js file.

The homeScreen.js file should contain:

src/screens/homeScreen.js
import React from 'react';
import { StyleSheet, View } from 'react-native';
import { Title } from 'react-native-paper';

import FormButton from '../components/formButton';

export default function HomeScreen() {
return (
<View style={styles.container}>
<Title>ChatKitty Example</Title>
<FormButton modeValue="contained" title="Logout" />
</View>
);
}

const styles = StyleSheet.create({
container: {
backgroundColor: '#f5f5f5',
flex: 1,
justifyContent: 'center',
alignItems: 'center',
},
});

You'll need another navigation stack to handle screen routes after the user logs in.

Create a homeStack.js file inside the src/navigation directory.

The homeStack.js file should contain:

src/navigation/homeStack.js
import { createStackNavigator } from '@react-navigation/stack';
import React from 'react';

import HomeScreen from '../screens/HomeScreen';

const Stack = createStackNavigator();

export default function HomeStack() {
return (
<Stack.Navigator>
<Stack.Screen name="Home" component={HomeScreen} />
</Stack.Navigator>
);
}

We'll need an authentication provider to check if a user is authenticated.

Inside the src/navigation directory create a authProvider.js file.

The authProvider.js file should contain:

src/navigation/authProvider.js
import React, { createContext, useState } from 'react';

export const AuthContext = createContext({});

export const AuthProvider = ({ children }) => {
const [user, setUser] = useState(null);
const [loading, setLoading] = useState(false);

return (
<AuthContext.Provider
value={{
user,
setUser,
loading,
setLoading,
login: async (email, password) => {
// TODO
},
register: async (displayName, email, password) => {
// TODO
},
logout: async () => {
// TODO
},
}}
>
{children}
</AuthContext.Provider>
);
};

Later, you'll be updating the authentication provider to login, register, and logout the user using Firebase and ChatKitty.

To get the authentication context inside the app components, you'll need to wrap the app routes with the authentication provider.

Edit the src/navigation/index.js file to wrap the app routes with the authentication provider.

The index.js file should now contain:

src/navigation/index.js
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>
<Routes />
</AuthProvider>
</PaperProvider>
);
}

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

Now, we have screens for user authentication, and a place to redirect users after they authenticate. Let's hook the app up to a Firebase and ChatKitty backend.

Creating a Firebase projectโ€‹

You'll be using Firebase to manage user passwords and authentication, so you'll need a Firebase project. If you don't already have one, create a new project using the Firebase console.

From the Firebase console home, create a project:

Screenshot: Firebase create project

Fill out the details of your Firebase project:

Screenshot: Firebase create project details

When you're done, click the "Continue" button, you'll be redirected to a dashboard screen for your new Firebase project.

Screenshot: Firebase create project complete

To support user email and password login, you'll need to enable the Firebase email sign-in method. From the Firebase console side menu, navigate to the Authentication section.

Screenshot: Firebase side menu authentication

Go to the "Sign-in method" tab and enable the email sign-in provider.

Screenshot: Firebase select sign in method

Screenshot: Firebase enable email password sign in method

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 Web icon:

Screenshot: Firebase add app

Fill out the application details

Screenshot: Firebase create web app register

Then continue to the console settings page

Screenshot: Firebase create web app complete

Now, you should be able to copy your Firebase web app config to add to your Expo React Native project. From the "Your apps" section, select the "Config" Firebase SDK snippet for your web app and copy it:

Screenshot: Firebase web app config

In your Expo React Native project, inside the src/ directory, create a firebase/ directory. Inside the src/firebase directory, create an index.js file to hold your Firebase web app credentials.

The index.js file should contain:

src/firebase/index.js
import { initializeApp } from "firebase/app";
import { getAuth } from "firebase/auth";

// Replace this with your Firebase SDK config snippet
const firebaseConfig = {
/* YOUR FIREBASE CONFIG OBJECT HERE */
};

// Initialize Firebase
const app = initializeApp(firebaseConfig);

// Initialize Firebase Authentication and get a reference to the service
const auth = getAuth(app);

export { auth };

Replacing firebaseConfig with the value from your Firebase SDK config snippet.

That's it. You've now configured your Expo React Native app to use Firebase.

Hooking up user registration to Signup screenโ€‹

You now have everything needed to create new users in Firebase. Let's partially implement the register function stub in the authentication provider to add new users to Firebase.

Edit the src/navigation/authProvider.js file to use Firebase to register new users.

The authProvider.js file should now contain:

src/navigation/authProvider.js
import React, { createContext, useState } from 'react';

import { createUserWithEmailAndPassword, updateProfile } from "firebase/auth";
import { auth } from "../firebase";

export const AuthContext = createContext({});

export const AuthProvider = ({ children }) => {
const [user, setUser] = useState(null);
const [loading, setLoading] = useState(false);

return (
<AuthContext.Provider
value={{
user,
setUser,
loading,
setLoading,
login: async (email, password) => {
// TODO
},
register: (displayName, email, password) => {
setLoading(true);
createUserWithEmailAndPassword(auth, email, password)
.then((userCredential) => {
// Signed in
const currentUser = userCredential.user;
updateProfile(auth.currentUser, {
displayName: displayName
}).then(() => {
// Profile updated!
})
})
.catch((error) => {
const errorCode = error.code;
const errorMessage = error.message;
console.error(error);
});
setLoading(false)
},
logout: async () => {
// TODO
},
}}
>
{children}
</AuthContext.Provider>
);
};

We can now hook the register function to the Signup screen.

Edit the src/screens/signupScreen.js file to call the register function.

The signupScreen.js file should now contain:

src/screens/signupScreen.js
import React, { useContext, useState } from 'react';
import { StyleSheet, View } from 'react-native';
import { IconButton, Title } from 'react-native-paper';

import FormButton from '../components/formButton';
import FormInput from '../components/formInput';
import Loading from '../components/loading';
import { AuthContext } from '../navigation/authProvider';

export default function SignupScreen({ navigation }) {
const [displayName, setDisplayName] = useState('');
const [email, setEmail] = useState('');
const [password, setPassword] = useState('');

const { register, loading } = useContext(AuthContext);

if (loading) {
return <Loading />;
}

return (
<View style={styles.container}>
<Title style={styles.titleText}>Let's get started!</Title>
<FormInput
labelName="Display Name"
value={displayName}
autoCapitalize="none"
onChangeText={(userDisplayName) => setDisplayName(userDisplayName)}
/>
<FormInput
labelName="Email"
value={email}
autoCapitalize="none"
onChangeText={(userEmail) => setEmail(userEmail)}
/>
<FormInput
labelName="Password"
value={password}
secureTextEntry={true}
onChangeText={(userPassword) => setPassword(userPassword)}
/>
<FormButton
title="Signup"
modeValue="contained"
labelStyle={styles.loginButtonLabel}
onPress={() => register(displayName, email, password)}
/>
<IconButton
icon="keyboard-backspace"
size={30}
style={styles.navButton}
color="#5b3a70"
onPress={() => navigation.goBack()}
/>
</View>
);
}

const styles = StyleSheet.create({
container: {
backgroundColor: '#f5f5f5',
flex: 1,
justifyContent: 'center',
alignItems: 'center',
},
titleText: {
fontSize: 24,
marginBottom: 10,
},
loginButtonLabel: {
fontSize: 22,
},
navButtonText: {
fontSize: 18,
},
navButton: {
marginTop: 10,
},
});

Run the app now and submit a sign up form:

Screenshot: Sign up screen filled

A new Firebase user should be created. You can verify this by checking the Firebase dashboard, Under the "Authentication" "Users" section, you should see your user:

Screenshot: Firebase created user

To complete the sign up and login process, you'll need a ChatKitty project to connect to the ChatKitty JavaScript SDK and create chat sessions for your users. Let's create a ChatKitty project now.

Creating a ChatKitty projectโ€‹

You'll be using ChatKitty to provide real-time chat functionality for your chat app. You can create a free ChatKitty account here.

Once you've created a ChatKitty account, create an application for your Expo React Native project:

Screenshot: ChatKitty create application

Adding Firebase to your Chat Runtimeโ€‹

Now, let's integrate Firebase into your ChatKitty project. ChatKitty makes it easy to integrate any back-end 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. You can send push notifications, create emails or make HTTP calls to your backend, as well as use a pre-initialized server-side ChatKitty SDK to make changes to your application in response to user actions. With ChatKitty, you can use any NPM package inside your Chat Functions as a Chat Runtime dependency.

Let's now add Firebase 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 Firebase JavaScript SDK NPM package, firebase-admin. Version 11.4.1 was the latest version as of the time this article was written.

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

Before you can use Firebase within your chat functions, the Firebase SDK needs to be initialized. You can add arbitrary code that runs before each chat function using a chat runtime initialization script. Let's add an initialization script to initialize the Firebase SDK.

From the "Runtime" tab, click the drop down and select "Initialization Script". You import NPM module using the CommonJS require function. Import the Firebase Admin NPM module and initialize Firebase using a Firebase Service Account Key for your project. To retrieve a Service Account Key, go to your Firebase project and then go to the "Service Account" tab. From here can click on "Generate Private Key" to retrieve the serviceAccount object.

Screenshot: Generate private Firebase service account key

Now, we will go back to the ChatKitty dashboard and add the following into the Initialization Script:

const admin = require("firebase-admin");

const serviceAccount = {
// ADD YOUR SERVICE ACCOUNT KEY HERE
}

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

Screenshot: ChatKitty runtime initialization script Replacing serviceAccount with the value from your Firebase Service Account Key snippet. Remember to click the "Save" icon to confirm your changes.

Now we're ready to define a chat function to check if a user's email and password exists and matches what we expect from Firebase, whenever a user tries to begin a new chat session.

Checking user credentials using a chat functionโ€‹

Before we let users begin a chat session and access sensitive data like messages, we need to be sure their credentials match what's stored in Firebase using a chat function.

From your ChatKitty application dashboard, go to the "Functions" page. The "User Attempted Start Session" event chat function should be selected:

Screenshot: ChatKitty chat functions

This chat function runs whenever a user attempts to start a chat session. Edit the chat function to delegate user authentication to Firebase.

const firebase = require('firebase-admin');

async function handleEvent(event: UserAttemptedStartSessionEvent, context: Context) {
const username = event.username;

const idToken = event.authParams.idToken;

const { uid, name } = await firebase.auth().verifyIdToken(idToken);

if (username !== uid) throw new Error("This token was not issued for this user");

const userApi = context.getUserApi();

await userApi.getUserExists(username).catch(async () => {
await userApi.createUser({
name: username,
displayName: name || "anon",
isGuest: false,
});
};
}

Screenshot: ChatKitty chat function user attempted start session Remember to click the "Save" icon to confirm your chat function changes.

Now, whenever a user tries to log in, ChatKitty checks if a user with their login credentials exists from your Firebase backend, and if so, begins a chat session. With that, we're ready to connect your Expo React Native app to the ChatKitty JavaScript Chat SDK.

Connecting to ChatKitty JS SDKโ€‹

To use the ChatKitty JavaScript Chat SDK, you'll need to add the ChatKitty JavaScript SDK NPM package to your Expo React Native project:

npm install @chatkitty/core

Next, you'll need to configure the ChatKitty SDK with your ChatKitty API key. You can find your API key on the ChatKitty dashboard, inside the "Settings" page.

Screenshot: ChatKitty side menu settings

Copy the string value, under "API Key", you'll need it to initialize a ChatKitty client instance:

Screenshot: ChatKitty settings api key

In your Expo React Native project, inside the src/ directory, create a chatkitty/ directory. Inside the src/chatkitty directory, create an index.js file to hold your ChatKitty client instance.

The index.js file should contain:

src/chatkitty/index.js
import ChatKitty from '@chatkitty/core';

const chatkitty = ChatKitty.getInstance('YOUR CHATKITTY API KEY');

export default chatkitty;

With the API key from the ChatKitty dashboard

Let's now complete the chat login flow with the initialized ChatKitty client instance.

Completing user login and sign up with ChatKittyโ€‹

You now have everything needed to implement a complete and secure chat app signup and login. Let's fill out the missing pieces left in our authentication provider.

Edit the src/navigation/authProvider.js file to use ChatKitty to login users and create chat sessions.

The authProvider.js file should now contain:

src/navigation/authProvider.js
import React, { createContext, useState } from 'react';
import { createUserWithEmailAndPassword, updateProfile, getIdToken, signInWithEmailAndPassword } from "firebase/auth";
import { auth } from "../firebase";
import chatkitty from "../chatkitty";

export const AuthContext = createContext({});

export const AuthProvider = ({ children }) => {
const [user, setUser] = useState(null);
const [loading, setLoading] = useState(false);

return (
<AuthContext.Provider
value={{
user,
setUser,
loading,
setLoading,
login: async (email, password) => {
setLoading(true);
signInWithEmailAndPassword(auth, email, password)
.then(async (userCredential) => {
// Signed in
const currentUser = userCredential.user;
const result = await chatkitty.startSession({
username: currentUser.uid,
authParams: {
idToken: await currentUser.getIdToken()
}
});
if (result.failed) {
console.log('could not login')
}
})
.catch((error) => {
const errorCode = error.code;
const errorMessage = error.message;
console.log(error);
});
setLoading(false);
},

register: (displayName, email, password) => {
setLoading(true);
createUserWithEmailAndPassword(auth, email, password)
.then((userCredential) => {
// Signed in
const currentUser = userCredential.user;
updateProfile(auth.currentUser, {
displayName: displayName
}).then(async () => {
const result = await chatkitty.startSession({
username: currentUser.uid,
authParams: {
idToken: await currentUser.getIdToken()
}
});
if (result.failed) {
console.log('Could not sign up');
}
})
})
.catch((error) => {
const errorCode = error.code;
const errorMessage = error.message;
console.error(error);
});
setLoading(false)
},
logout: async () => {
try {
await chatkitty.endSession();
} catch (error) {
console.error(error);
}
},
}}
>
{children}
</AuthContext.Provider>
);
};

Now, we can check if a user is logged in, and if so, route them to the home screen.

Edit the src/navigation/routes.js file to change the current navigation stack depending on if a user is logged in or not.

The routes.js file should now contain:

src/navigation/routes.js
import React, { useContext, useState } from 'react';
import { StyleSheet, View } from 'react-native';
import { Title } from 'react-native-paper';

import FormButton from '../components/formButton.js';
import FormInput from '../components/formInput.js';
import Loading from '../components/loading.js';
import { AuthContext } from '../navigation/authProvider.js';

export default function LoginScreen({ navigation }) {
const [email, setEmail] = useState('');
const [password, setPassword] = useState('');

const { login, loading } = useContext(AuthContext);

if (loading) {
return <Loading />
}

return (
<View style={styles.container}>
<Title style={styles.titleText}>Welcome!</Title>
<FormInput
labelName="Email"
value={email}
autoCapitalize="none"
onChangeText={(userEmail) => setEmail(userEmail)}
/>
<FormInput
labelName="Password"
value={password}
secureTextEntry={true}
onChangeText={(userPassword) => setPassword(userPassword)}
/>
<FormButton
title="Login"
modeValue="contained"
labelStyle={styles.loginButtonLabel}
onPress={() => {
login(email, password)
}}
/>
<FormButton
title="Sign up here"
modeValue="text"
uppercase={false}
labelStyle={styles.navButtonText}
onPress={() => navigation.navigate('Signup')}
/>
</View>
);
}

const styles = StyleSheet.create({
container: {
backgroundColor: '#f5f5f5',
flex: 1,
justifyContent: 'center',
alignItems: 'center',
},
titleText: {
fontSize: 24,
marginBottom: 10,
},
loginButtonLabel: {
fontSize: 22,
},
navButtonText: {
fontSize: 16,
},
});

Finally, hook the logout authentication function to the Home screen and show the user's display name.

Edit the src/screens/homeScreen.js file to call the logout function and greet your user.

The homeScreen.js file should now contain:

src/screens/homeScreen.js
import React, { useContext } from 'react';
import { StyleSheet, View } from 'react-native';
import { Title } from 'react-native-paper';

import FormButton from '../components/formButton';
import { AuthContext } from '../navigation/authProvider';

export default function HomeScreen() {
const { user, logout } = useContext(AuthContext);

return (
<View style={styles.container}>
<Title>ChatKitty Example</Title>
<FormButton
modeValue="contained"
title="Logout"
onPress={() => logout()}
/>
</View>
);
}

const styles = StyleSheet.create({
container: {
backgroundColor: '#f5f5f5',
flex: 1,
justifyContent: 'center',
alignItems: 'center',
},
});

If you run the application and try to login with the test user credentials you created earlier

Screenshot: Login screen filled

You should be redirected to your home screen

Screenshot: Home screen

Conclusionโ€‹

Awesome! You've completed the first part of this tutorial series and successfully implemented secure user authentication for your Expo React Native chat app, using Firebase and powered by the ChatKitty chat platform. By defining your authentication logic in a chat function while delegating user credentials checking to Firebase, you were able to extend the ChatKitty login flow to handle your specific case, without having to build your own backend. In future parts of this series, we'll explore more features you can implement with chat functions like sending push notifications to a user's device when the user isn't online.

What's next?โ€‹

In the next post of this series, we'll implement more chat features including creating discussion channels for users to discover, join and chat. Stay tuned for more. ๐Ÿ”ฅ

If you have any questions, comments or need help with any part of this article, join our Discord Server where you can ask questions, discuss what you're working on, and I'll be more than 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.

ยท 22 min read
Aaron Nwabuoku

Building a Chat App with React Native and Gifted Chat (Part 4)

In this tutorial series, I'll be showing you how to build a functional and secure chat app using the latest React Native libraries, including Gifted Chat and the Expo framework, powered by the ChatKitty platform.


So far 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, adding screens for users to create public channels, discover new channels, and view their channels. In the third article of this series, you enhanced that chat experience by implementing in-app and push notifications to notify your users when chat events occur.

In this tutorial, you'll be building on the group chat experience you created adding direct messaging to allow users to communicate privately. You'll also be adding a few enhancements to the chat experience including typing indicators, and chat room presence notifications.

After reading this article, you will be able to:

  1. Create direct channels for users to chat privately

  2. Integrate Gifted Chat's in-built typing indicator and implement a custom more detailed indicator

  3. Notify chat users when a user enters or leaves the chat from a chat screen

  4. Allow users to leave chat channels they are no longer interested in

If you followed along the previous articles, 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.

Direct channelsโ€‹

Previously, we learned about ChatKitty chat channels, specifically public channels that users can discover and join, or be invited to join. Direct channels, on the other hand, let users have private conversations between up to 9 other users. New users cannot be added to a direct channel and there can only exist one direct channel between a set of users. Direct channels are perfect for one-off conversations that don't require an entire channel to discuss.

Entering a chatโ€‹

When a user starts a chat session and has no other active chat sessions in the session channel, the user has entered a chat. In other words, users who have entered a chat have at least one active chat session for that chat channel, and are active participants of the conversation. Active chat participants receive real-time messaging events, and are present to reply immediately.

Leaving a chatโ€‹

After a user ends a chat session and has no other active chat sessions in the session channel, the user has left a chat. Users leave a chat when there is no longer at least one active chat session for that chat channel, and are no longer active participants of the conversation. After leaving a chat, users begin to get notifications of events that happened in the chat while they are away.

Leaving a channelโ€‹

After a user joins a channel, the user becomes a channel member. Channel members can send messages in a channel, and receives messages and notifications related to the channel. If a user is no longer interested in a channel, the user can leave the channel and is no longer a channel member.

Okay, let's get started! ๐ŸŽ๏ธ

Next, you'll be adding direct messaging functionality to your chat app.

Creating a direct messaging channelโ€‹

Edit the ChatScreen.js screen file you previous created to destructure its navigation prop:

export default function ChatScreen({ route, navigation /* Add this */ }) {
const {user} = useContext(AuthContext);
const {channel} = route.params;

// Unchanged
}

Now, you can customize your Gifted Chat message avatar to create or get a direct channel, and navigate the user to a new chat screen with the direct channel when it's pressed.

Define a new method renderAvatar to pass into your GiftedChat component:

import { Avatar, Bubble, GiftedChat } from 'react-native-gifted-chat';

// Unchanged

function renderAvatar(props) {
return (
<Avatar
{...props}
onPressAvatar={(avatarUser) => {
kitty
.createChannel({
type: 'DIRECT',
members: [{ id: avatarUser._id }],
})
.then((result) => {
navigation.navigate('Chat', { channel: result.channel });
});
}}
/>
);
}

Set the GiftedChat renderAvatar prop to the method you defined:

return (
<GiftedChat
messages={messages}
onSend={handleSend}
user={mapUser(user)}
loadEarlier={loadEarlier}
isLoadingEarlier={isLoadingEarlier}
onLoadEarlier={handleLoadEarlier}
renderBubble={renderBubble}
renderAvatar={renderAvatar} /* Add this */
/>
);

After these changes, ChatScreen.js should look like this:

import React, { useContext, useEffect, useState } from 'react';
import { Avatar, Bubble, GiftedChat } from 'react-native-gifted-chat';

import { kitty } from '/blog/chatkitty';
import Loading from '/blog/components/Loading';
import { AuthContext } from '/blog/navigation/AuthProvider';

export default function ChatScreen({ route, navigation }) {
const { user } = useContext(AuthContext);
const { channel } = route.params;

const [messages, setMessages] = useState([]);
const [loading, setLoading] = useState(true);
const [loadEarlier, setLoadEarlier] = useState(false);
const [isLoadingEarlier, setIsLoadingEarlier] = useState(false);
const [messagePaginator, setMessagePaginator] = useState(null);

useEffect(() => {
const startChatSessionResult = kitty.startChatSession({
channel: channel,
onReceivedMessage: (message) => {
setMessages((currentMessages) =>
GiftedChat.append(currentMessages, [mapMessage(message)])
);
},
});

kitty
.getMessages({
channel: channel,
})
.then((result) => {
setMessages(result.paginator.items.map(mapMessage));

setMessagePaginator(result.paginator);
setLoadEarlier(result.paginator.hasNextPage);

setLoading(false);
});

return startChatSessionResult.session.end;
}, [user, channel]);

async function handleSend(pendingMessages) {
await kitty.sendMessage({
channel: channel,
body: pendingMessages[0].text,
});
}

async function handleLoadEarlier() {
if (!messagePaginator.hasNextPage) {
setLoadEarlier(false);

return;
}

setIsLoadingEarlier(true);

const nextPaginator = await messagePaginator.nextPage();

setMessagePaginator(nextPaginator);

setMessages((currentMessages) =>
GiftedChat.prepend(currentMessages, nextPaginator.items.map(mapMessage))
);

setIsLoadingEarlier(false);
}

function renderBubble(props) {
return (
<Bubble
{...props}
wrapperStyle={{
left: {
backgroundColor: '#d3d3d3',
},
}}
/>
);
}

function renderAvatar(props) {
return (
<Avatar
{...props}
onPressAvatar={(clickedUser) => {
kitty
.createChannel({
type: 'DIRECT',
members: [{ id: clickedUser._id }],
})
.then((result) => {
navigation.navigate('Chat', { channel: result.channel });
});
}}
/>
);
}

if (loading) {
return <Loading />;
}

return (
<GiftedChat
messages={messages}
onSend={handleSend}
user={mapUser(user)}
loadEarlier={loadEarlier}
isLoadingEarlier={isLoadingEarlier}
onLoadEarlier={handleLoadEarlier}
renderBubble={renderBubble}
renderAvatar={renderAvatar}
/>
);
}

function mapMessage(message) {
return {
_id: message.id,
text: message.body,
createdAt: new Date(message.createdTime),
user: mapUser(message.user),
};
}

function mapUser(user) {
return {
_id: user.id,
name: user.displayName,
avatar: user.displayPictureUrl,
};
}

If you run the app now and go to a public chat screen, you should see:

Screenshot: Public chat

Tapping a message avatar should take you to a new chat screen where you can have a direct private conversation.

Screenshot: Direct chat

ChatKitty automatically creates a unique channel name for direct channels, so we see a UUID in the app title bar. Let's use a more appropriate name for the chat screen title.

Add a helper method getChannelDisplayName to the index.js file in the src/chatkitty directory:

export function getChannelDisplayName(channel) {
if (channel.type === 'DIRECT') {
return channel.members.map((member) => member.displayName).join(', ');
} else {
return channel.name;
}
}

After this change, index.js should look like this:

import ChatKitty from 'chatkitty';

export const kitty = ChatKitty.getInstance('YOUR CHATKITTY API KEY HERE');

export function getChannelDisplayName(channel) {
if (channel.type === 'DIRECT') {
return channel.members.map((member) => member.displayName).join(', ');
} else {
return channel.name;
}
}

You can now update your app to use a more readable channel display name.

Update HomeStack.js in src/navigation to use the getChannelDisplayName method:

import { getChannelDisplayName, kitty } from '/blog/chatkitty';

// Unchanged

<ChatStack.Screen
name="Chat"
component={ChatScreen}
options={({ route }) => ({
title: getChannelDisplayName(route.params.channel), /* Add this */
})}
/>

Also update BrowseChannelsScreen.js and HomeScreen.js in src/screens to use the helper method:

import { getChannelDisplayName, kitty } from '/blog/chatkitty';

// Unchanged

<List.Item
title={getChannelDisplayName(item)} /* Add this */
// Unchanged
/>

Running the app now shows the display names of a direct channel's members in the title bar

Screenshot: Direct chat name

Great! Now you can privately chat with other channel members. Now let's move on to enhancing your chat app's experience with a typing indicator.

Adding a typing indicator with Gifted Chat and ChatKittyโ€‹

The Gifted Chat React Native library saves you a lot of time when creating a chat UI. By providing a bunch of component props, you can customize the chat UI and implement chat features like typing indicators, using a chat service like ChatKitty.

You'll be using the isTyping Gifted Chat prop to display a typing indicator when another user is typing.

Screenshot: Simple typing indicator partial

ChatKitty tracks the typing state of users sending typing keystrokes in a channel. You'll need to send typing keystrokes to ChatKitty to let it know when a user is typing. When starting a chat session, you can register handler methods with ChatKitty to handle chat events like when a user starts or stops typing, enters or leaves a chat, enters keystrokes, etc. You can track when a user starts and stops typing with ChatKitty handler methods, storing the current user typing in your component state. Using the typing user state, you can set Gifted Chat's isTyping prop to display the typing indicator.

Add a helper method handleInputTextChanged in ChatScreen.js to sending typing keystrokes to ChatKitty, so it can know when a user is typing:

 function handleInputTextChanged(text) {
kitty.sendKeystrokes({
channel: channel,
keys: text,
});
}

Next, on the GiftedChat component, set the onInputTextChanged prop to handleInputTextChanged.

return (
<GiftedChat
messages={messages}
onSend={handleSend}
user={mapUser(user)}
loadEarlier={loadEarlier}
isLoadingEarlier={isLoadingEarlier}
onLoadEarlier={handleLoadEarlier}
onInputTextChanged={handleInputTextChanged} /* Add this */
renderBubble={renderBubble}
renderAvatar={renderAvatar}
/>
);

ChatKitty is now able to know when your users start and stop typing.

Next, define a new state variable to track the current typing user.

export default function ChatScreen({ route, navigation }) {
const {user} = useContext(AuthContext);
const {channel} = route.params;

const [messages, setMessages] = useState([]);
const [loading, setLoading] = useState(true);
const [loadEarlier, setLoadEarlier] = useState(false);
const [isLoadingEarlier, setIsLoadingEarlier] = useState(false);
const [messagePaginator, setMessagePaginator] = useState(null);
const [typing, setTyping] = useState(null); /* Add this */

// Unchanged
}

You can now register chat event handlers to control the typing state. Register both onTypingStarted and onTypingStopped in ChatKitty.startChatSession to set the typing state.

useEffect(() => {
const startChatSessionResult = kitty.startChatSession({
channel: channel,
onReceivedMessage: (message) => {
setMessages((currentMessages) =>
GiftedChat.append(currentMessages, [mapMessage(message)])
);
},
onTypingStarted: (typingUser) => { /* Add this */
if (typingUser.id !== user.id) {
setTyping(typingUser);
}
},
onTypingStopped: (typingUser) => { /* Add this */
if (typingUser.id !== user.id) {
setTyping(null);
}
},
});

// Unchanged
}, [user, channel]);

typing now holds the current typing user, so you can set the GiftedChat component isTyping prop to typing != null.

return (
<GiftedChat
messages={messages}
onSend={handleSend}
user={mapUser(user)}
loadEarlier={loadEarlier}
isLoadingEarlier={isLoadingEarlier}
onLoadEarlier={handleLoadEarlier}
onInputTextChanged