npx expo install expo-notifications
npx expo install expo-task-manager
npx expo install @notifee/react-native
Expo
This guide discusses how to set up your Expo app to get push notifications from Stream for the non-ringing calls that your user will receive.
Add push provider credentials to Stream
Please follow the below guides for adding appropriate push providers to Stream:
- Android - Firebase Cloud Messaging
- iOS - Apple Push Notification Service (APNs)
Install Dependencies
So what did we install precisely?
expo-notifications
andexpo-task-manager
for handling incoming Firebase Cloud Messaging notifications on Android and iOS.@notifee/react-native
- is used to customize and display push notifications.
Add Firebase credentials
To create a Firebase project, go to the Firebase console and click on Add project.
In the console, click the setting icon next to Project overview and open Project settings. Then, under Your apps, click the Android icon to open Add Firebase to your Android app and follow the steps. Make sure that the Android package name you enter is the same as the value of
android.package
from your app.json.After registering the app, download the google-services.json file and place it in your project’s root directory.
In app.json, add an
android.googleServicesFile
field with the relative path to the downloaded google-services.json file. If you placed it in the root directory, the path is:
{
"android": {
"googleServicesFile": "./google-services.json"
}
}
The google-services.json file contains unique and non-secret identifiers of your Firebase project. For more information, see Understand Firebase Projects.
Add the config plugin property
In app.json, in the plugins
field, add true to the enableNonRingingPushNotifications
property in the @stream-io/video-react-native-sdk
plugin.
{
"plugins": [
[
"@stream-io/video-react-native-sdk",
{
"enableNonRingingPushNotifications": true
}
],
// your other plugins
]
}
If Expo EAS build is not used, please do npx expo prebuild --clean
to generate the native directories again after adding the config plugins.
Setup the push config for the SDK
The SDK automatically processes the non ringing call push notifications once the setup above is done if the push config has been set using StreamVideoRN.setPushConfig
. To do this follow the steps below,
Add the ability to statically navigate to screens in your app
When a user taps on the push notification and the JS engine is not ready, they should still be able to navigate to the screen that shows the active call. You can achieve this by using imperative navigation in the expo router.
The following is an example implementation of a utility file that has helpers to statically navigate in the app:
import { User } from '@stream-io/video-react-native-sdk';
import { router } from 'expo-router';
/**
* This is used to run the navigation logic from root level
*/
export const staticNavigateToActiveCall = () => {
const intervalId = setInterval(async () => {
// add any requirements here (like authentication)
if (GlobalState.hasAuthentication) {
clearInterval(intervalId);
router.push('/activecall');
}
}, 300);
};
export const staticNavigateToLivestreamCall = () => {
const intervalId = setInterval(async () => {
// add any requirements here (like authentication)
if (GlobalState.hasAuthentication) {
clearInterval(intervalId);
router.push('/livestream');
}
}, 300);
};
Add Push message handlers
To process the incoming push notifications, the SDK provides the utility functions that you must add to your existing or new notification listeners.
Add callbacks to process notifications and displaying it
To process notifications on Android, we can use either @react-native-firebase
library or expo-task-manager
. The disadvantage of expo-task-manager
is that it does not work when push is delivered to an app that has its underlying process in a killed state. So we recommend using the @react-native-firebase
library. For iOS, we only need the expo-notifications
library.
First we have to install the react-native-firebase
library.
yarn add @react-native-firebase/app
yarn add @react-native-firebase/messaging
Below is the snippet to add message handlers:
import messaging from '@react-native-firebase/messaging';
import {
isFirebaseStreamVideoMessage,
firebaseDataHandler,
} from '@stream-io/video-react-native-sdk';
import { Platform } from 'react-native';
import * as Notifications from 'expo-notifications';
export const setPushMessageListeners = () => {
// Set up the background message handler for Android
messaging().setBackgroundMessageHandler(async (msg) => {
if (isFirebaseStreamVideoMessage(msg)) {
await firebaseDataHandler(msg.data);
} else {
// your other messages (if any)
}
});
// Set up the foreground message handler for Android
messaging().onMessage((msg) => {
if (isFirebaseStreamVideoMessage(msg)) {
firebaseDataHandler(msg.data);
} else {
// your other messages (if any)
}
});
if (Platform.OS === 'ios') {
// show notification on foreground on iOS
Notifications.setNotificationHandler({
// example configuration below to show alert and play sound
handleNotification: async (notification) => ({
shouldShowAlert: true,
shouldPlaySound: true,
shouldSetBadge: false,
}),
});
}
};
The Firebase message handlers
- The
isFirebaseStreamVideoMessage
method is used to check if this push message is a video related message. And only this needs to be processed by the SDK. - The
firebaseDataHandler
method is the callback to be invoked to process the message. This callback reads the message and uses the@notifee/react-native
library to display push notifications.
Disable Firebase initialisation on iOS
React Native Firebase Messaging automatically registers the device with APNs to receive remote messages. But since we do not use Firebase on iOS, we can disable it via the firebase.json
file that we can newly create:
/firebase.json">{
"react-native": {
"messaging_ios_auto_register_for_remote_messages": false
}
}
First we have to install the expo-task-manager
library.
npx expo install expo-task-manager
Below is the snippet to add message handlers:
const BACKGROUND_NOTIFICATION_TASK =
'STREAM-VIDEO-BACKGROUND-NOTIFICATION-TASK';
import {
isFirebaseStreamVideoMessage,
firebaseDataHandler,
isExpoNotificationStreamVideoEvent,
} from '@stream-io/video-react-native-sdk';
import { Platform } from 'react-native';
import * as Notifications from 'expo-notifications';
export const setPushMessageListeners = () => {
TaskManager.defineTask(
BACKGROUND_NOTIFICATION_TASK,
({ data, error }) => {
if (error) {
return;
}
// @ts-ignore
const dataToProcess = data.notification?.data;
if (data?.sender === 'stream.video'} {
firebaseDataHandler(dataToProcess);
}
}
);
// background handler (does not handle on app killed state)
Notifications.registerTaskAsync(BACKGROUND_NOTIFICATION_TASK);
// foreground handler
Notifications.setNotificationHandler({
handleNotification: async (notification) => {
if (Platform.OS === 'android' && isExpoNotificationStreamVideoEvent(notification)) {
const data = notification.request.trigger.remoteMessage?.data!;
await firebaseDataHandler(data, pushConfig);
// do not show this message, it processed by the above handler
return { shouldShowAlert: false, shouldPlaySound: false, shouldSetBadge: false };
} else {
// configuration for iOS call notification && your other messages, example below to show alert and play sound
return { shouldShowAlert: true, shouldPlaySound: true, shouldSetBadge: false };
}
},
});
};
The firebaseDataHandler
method is the callback to be invoked to process the message. This callback reads the message and uses the @notifee/react-native
library to display push notifications.
Add notification button listeners
Below is the snippet of how to add the notification button listeners:
import {
isNotifeeStreamVideoEvent,
onAndroidNotifeeEvent,
oniOSNotifeeEvent,
} from '@stream-io/video-react-native-sdk';
import { Platform } from 'react-native';
import notifee from '@notifee/react-native';
export const setNotifeeListeners = () => {
// on press handlers of background notifications for Android
notifee.onBackgroundEvent(async (event) => {
if (isNotifeeStreamVideoEvent(event)) {
await onAndroidNotifeeEvent({ event, isBackground: true });
} else {
// your other notifications (if any)
}
});
// on press handlers of foreground notifications for Android
notifee.onForegroundEvent((event) => {
if (Platform.OS === "android" && isNotifeeStreamVideoEvent(event)) {
onAndroidNotifeeEvent({ event, isBackground: false });
} else {
// your other notifications (if any)
}
});
};
The Notifee event handlers
- The
isNotifeeStreamVideoEvent
method is used to check if the event was a video related notifee event. And only this needs to be processed by the SDK. - The
onAndroidNotifeeEvent
method is the callback to be invoked to process the event. This callback reads the event and makes sure that the call is accepted or declined.
Adding handler for iOS
Add the following useEffect
in the root component of your App, this is most likely in App.tsx
.
import * as Notifications from 'expo-notifications';
useEffect(() => {
if (Platform.OS === 'ios') {
const subscription = Notifications.addNotificationReceivedListener(
(notification) => {
if (isExpoNotificationStreamVideoEvent(notification)) {
oniOSExpoNotificationEvent(notification);
} else {
// your other notifications (if any)
}
},
);
return () => {
subscription.remove();
};
}
}, []);
Setup the push config
Once we have set up the methods to navigate the app from a static method we are ready to call the StreamVideoRN.setPushConfig
method. Below is an example of how this method can be called,
import {
StreamVideoClient,
StreamVideoRN,
} from '@stream-io/video-react-native-sdk';
import { AndroidImportance } from '@notifee/react-native';
import AsyncStorage from '@react-native-async-storage/async-storage';
import { STREAM_API_KEY } from '../../constants';
import { staticNavigateToRingingCall, staticNavigateToLivestreamCall } from './staticNavigationUtils';
export function setPushConfig() {
StreamVideoRN.setPushConfig({
// pass true to inform the SDK that this is an expo app
isExpo: true,
ios: {
// add your push_provider_name for iOS that you have setup in Stream dashboard
pushProviderName: __DEV__ ? 'apn-video-staging' : 'apn-video-production',
},
android: {
// add your push_provider_name for Android that you have setup in Stream dashboard
pushProviderName: __DEV__
? 'firebase-video-staging'
: 'firebase-video-production',
// configure the notification channel to be used for non ringing calls for Android.
callChannel: {
id: 'stream_call_notifications',
name: 'Call notifications',
// This importance will ensure that the notification will appear on-top-of applications.
importance: AndroidImportance.HIGH,
sound: "default",
},
// configure the functions to create the texts shown in the notification
// for non ringing calls in Android.
callNotificationTextGetters: {
getTitle(type, createdUserName) {
if (type === 'call.live_started') {
return `Call went live, it was started by ${createdUserName}`;
} else {
return `${createdUserName} is notifying you about a call`;
}
},
getBody(_type, createdUserName) {
return 'Tap to open the call';
},
},
},
// optional: add the callback to be executed when a non ringing call notification is tapped
onTapNonRingingCallNotification: () => {
const [callType, callId] = call_cid.split(':');
if (callType === 'livestream') {
staticNavigateToLivestreamCall();
} else {
staticNavigateToActiveCall();
}
},
// add the async callback to create a video client
// for incoming calls in the background on a push notification
createStreamVideoClient: async () => {
// note that since the method is async,
// you can call your server to get the user data or token or retrieve from offline storage.
const userId = await AsyncStorage.getItem('@userId');
const userName = await AsyncStorage.getItem('@userName');
// an example promise to fetch token from your server
const tokenProvider = () => yourServer.getTokenForUser(userId).then((auth) => auth.token);
const user = { id: userId, name: userName };
return StreamVideoClient.getOrCreateInstance({
apiKey: STREAM_API_KEY, // pass your stream api key
user,
tokenProvider,
});
},
});
}
Call the created methods outside of the application lifecycle
Call the methods we have created outside of your application cycle. That is inside index.js
or the equivalent entry point file. This is because the app can be opened from a dead state through a push notification and in that case, we need to use the configuration and notification callbacks as soon as the JS bridge is initialized.
Following is an example,
import 'expo-router/entry';
import { setPushConfig } from 'src/utils/setPushConfig';
import { setNotifeeListeners } from 'src/utils/setNotifeeListeners';
import { setPushMessageListeners } from 'src/utils/setPushMessageListeners';
setPushConfig();
setNotifeeListeners();
setPushMessageListeners();
Request for notification permissions
At an appropriate place in your app, request for notification permissions from the user. Below is a small example of how to request permissions in Expo:
import * as Notifications from 'expo-notifications';
await Notifications.requestPermissionsAsync();
Disabling push - usually on logout
In some cases you would want to disable push from happening. For example, if user logs out of your app. Or if the user switches. You can disable push like below:
import { StreamVideoRN } from '@stream-io/video-react-native-sdk';
await StreamVideoRN.onPushLogout();
Troubleshooting
- During development, you may be facing a situation where push notification is shown but its events like accepting or rejecting a call don’t work. This is because, during hot module reloading the global event listeners may get de-registered. To properly test during development, make sure that you fully restart the app or test in release mode without the metro packager.
- You can check the “Webhook & Push Logs” section in the Stream Dashboard to see if Notifications were sent by Stream.
- If you are still having trouble with Push Notifications, please submit a ticket to us at support.
Closed notification behavior on Android
On Android, users can set certain OS-level settings, usually revolving around performance and battery optimization, that can prevent notifications from being delivered when the app is in a killed state. For example, one such setting is the Deep Clear option on OnePlus devices using Android 9 and lower versions.
- Add push provider credentials to Stream
- Install Dependencies
- Add Firebase credentials
- Add the config plugin property
- Setup the push config for the SDK
- Add Push message handlers
- Setup the push config
- Call the created methods outside of the application lifecycle
- Request for notification permissions
- Disabling push - usually on logout
- Troubleshooting