Expo

If you are on version 1.2 or below, you would need to upgrade to 1.3 or above to follow the setup. As 1.3 release had breaking changes with respect to setting up of push notifications. We recommend to update to the current latest version.

Add push notifications for ringing calls to your Expo project. Covers both Android and iOS setup.

Users receive push notifications for incoming calls and can accept or reject directly from the notification.

Android previewiOS preview
Android preview of the Firebase push notification
iOS preview of VoIP notification using Apple Push Notification service (APNs)

Full-screen notifications are displayed when the phone screen is locked or the app is active (foreground state). However, when the app is terminated or in the background and the screen is awake, notifications may appear as heads-up notifications instead of a full-screen alerts.

Install Dependencies

npx expo install \
  @react-native-firebase/app \
  @react-native-firebase/messaging \
  @notifee/react-native \
  react-native-voip-push-notification \
  react-native-callkeep \
  @config-plugins/react-native-callkeep

Package purposes:

  • @react-native-firebase/app, @react-native-firebase/messaging - Handle Firebase Cloud Messaging on Android
  • @notifee/react-native - Customize and display push notifications
  • react-native-voip-push-notification - Handle PushKit notifications on iOS
  • react-native-callkeep, @config-plugins/react-native-callkeep - Report calls to iOS CallKit

Add Firebase credentials

  1. Create a Firebase project at Firebase console
  2. Add your Android app in Project settings > Your apps. Use the same package name as android.package in app.json
  3. Download google-services.json to your project root
  4. Add to app.json:
{
  "android": {
    "googleServicesFile": "./google-services.json"
  }
}
  1. For iOS, add your Apple app in Project settings > Your apps. Use the same bundle ID as ios.bundleIdentifier in app.json
  2. Download GoogleService-Info.plist to your project root
  3. Add to app.json:
{
  "ios": {
    "googleServicesFile": "./GoogleService-Info.plist"
  }
}

The google-services.json and GoogleService-Info.plist files contain unique and non-secret identifiers of your Firebase project. For more information, see Understand Firebase Projects.

We will not be using firebase for iOS. But it is necessary for the setup for react-native-firebase to have the GoogleService-Info.plist file.

iOS - Notifications entitlement

For Expo SDK 51+, add the notifications entitlement to app.json:

{
  "expo": {
    "ios": {
      "entitlements": {
        "aps-environment": "production"
      }
    }
  }
}

Add the config plugin properties

Add ringingPushNotifications to the SDK plugin and include @config-plugins/react-native-callkeep:

{
  "plugins": [
    [
      "@stream-io/video-react-native-sdk",
      {
        "ringingPushNotifications": {
          "disableVideoIos": false,
          "includesCallsInRecentsIos": false,
          "showWhenLockedAndroid": true
        },
        "androidKeepCallAlive": true
      }
    ],
    "@config-plugins/react-native-callkeep",
    [
      "@config-plugins/react-native-webrtc",
      {
        "cameraPermission": "$(PRODUCT_NAME) requires camera access in order to capture and transmit video",
        "microphonePermission": "$(PRODUCT_NAME) requires microphone access in order to capture and transmit audio"
      }
    ],
    "@react-native-firebase/app",
    "@react-native-firebase/messaging",
    [
      "expo-build-properties",
      {
        "ios": {
          "useFrameworks": "static",
          "forceStaticLinking": [
            "RNFBApp",
            "RNFBMessaging",
            "stream-react-native-webrtc"
          ]
        }
      }
    ]
    // your other plugins
  ]
}
  • The disableVideoIos field is used for apps with audio only calls. Pass true to this property to disable video in iOS CallKit.
  • The includesCallsInRecentsIos field is used to show call history. Pass true to show the history of calls made in the iOS native dialer
  • The showWhenLockedAndroid field is used to display a full-screen notification for the incoming call when the phone is locked. Pass true to enable it.
  • For iOS only,
    • firebase-ios-sdk requires static frameworks then you want to configure expo-build-properties by adding "useFrameworks": "static".
    • Since Expo 54, forceStaticLinking is required for certain libraries when "useFrameworks": "static" is used.

The plugin adds a foreground service and the necessary permissions for Android. It shows incoming call notifications and keeps video/audio calls active when the app is in the background.

When uploading the app to the Play Store, declare these permissions in the Play Console and explain their usage, including a link to a video demonstrating the service. This is a one-time requirement. For more information, click here.

The added permissions are:

  • android.permission.FOREGROUND_SERVICE_CAMERA - To access camera when app goes to background

  • android.permission.FOREGROUND_SERVICE_MICROPHONE - To access microphone when app goes to background

  • android.permission.FOREGROUND_SERVICE_MEDIA_PLAYBACK - To play video and audio tracks when the app goes to background

If Expo EAS build is not used, please do npx expo prebuild --clean to generate the native directories again after adding the config plugins.

Optional: Disable Firebase integration on iOS

Disable Firebase APNs registration since iOS uses VoIP push. Create firebase.json:

{
  "react-native": {
    "messaging_ios_auto_register_for_remote_messages": false
  }
}

Add Firebase message handlers

Add SDK utility functions to Firebase notification listeners:

import messaging from "@react-native-firebase/messaging";
import notifee from "@notifee/react-native";
import {
  isFirebaseStreamVideoMessage,
  firebaseDataHandler,
  onAndroidNotifeeEvent,
  isNotifeeStreamVideoEvent,
} from "@stream-io/video-react-native-sdk";

export const setFirebaseListeners = () => {
  // Set up the background message handler
  messaging().setBackgroundMessageHandler(async (msg) => {
    if (isFirebaseStreamVideoMessage(msg)) {
      await firebaseDataHandler(msg.data);
    } else {
      // your other background notifications (if any)
    }
  });

  // on press handlers of background notifications
  notifee.onBackgroundEvent(async (event) => {
    if (isNotifeeStreamVideoEvent(event)) {
      await onAndroidNotifeeEvent({ event, isBackground: true });
    } else {
      // your other background notifications (if any)
    }
  });

  // Optionally: set up the foreground message handler
  messaging().onMessage((msg) => {
    if (isFirebaseStreamVideoMessage(msg)) {
      firebaseDataHandler(msg.data);
    } else {
      // your other foreground notifications (if any)
    }
  });
  //  Optionally: on press handlers of foreground notifications
  notifee.onForegroundEvent((event) => {
    if (isNotifeeStreamVideoEvent(event)) {
      onAndroidNotifeeEvent({ event, isBackground: false });
    } else {
      // your other foreground notifications (if any)
    }
  });
};

Firebase message handlers:

  • onMessage - Skip if foreground notifications not needed (incoming call screen shows automatically)
  • isFirebaseStreamVideoMessage - Checks if push message is video-related
  • firebaseDataHandler - Processes message and displays notification via @notifee/react-native

Notifee event handlers:

  • onForegroundEvent - Skip if foreground notifications not added
  • isNotifeeStreamVideoEvent - Checks if event is video-related
  • onAndroidNotifeeEvent - Processes accept/decline actions

If you have disabled the initialization of Firebase on iOS, add the above method only for Android using the Platform-specific extensions for React Native.

For example, say you add the following files in your project:

setFirebaseListeners.android.ts
setFirebaseListeners.ts

The method above must only be added to the file that .android extension. The other file must add the method but do nothing like below:

export const setFirebaseListeners = () => {
  // do nothing
};

Setup the push notifications configuration for the SDK

Configure push via StreamVideoRN.setPushConfig. Override notification texts, set push provider names, and customize ringtone:

import {
  StreamVideoClient,
  StreamVideoRN,
  User,
} 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";

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: {
      // the name of android notification icon (Optional, defaults to 'ic_launcher')
      smallIcon: "ic_notification",
      // 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 incoming calls for Android.
      incomingCallChannel: {
        id: "stream_incoming_call",
        name: "Incoming call notifications",
        // This is the advised importance of receiving incoming call notifications.
        // This will ensure that the notification will appear on-top-of applications.
        importance: AndroidImportance.HIGH,
        // optional: if you dont pass a sound, default ringtone will be used
        sound: "<url to the ringtone>",
      },
      // configure the functions to create the texts shown in the notification
      // for incoming calls in Android.
      incomingCallNotificationTextGetters: {
        getTitle: (userName: string) => `Incoming call from ${userName}`,
        getBody: (_userName: string) => "Tap to answer the call",
        getAcceptButtonTitle: () => "Accept",
        getDeclineButtonTitle: () => "Decline",
      },
    },

    // add the async callback to create a video client
    // for incoming calls in the background on a push notification
    createStreamVideoClient: async () => {
      const userId = await AsyncStorage.getItem("@userId");
      const userName = await AsyncStorage.getItem("@userName");
      if (!userId) return undefined;

      // an example promise to fetch token from your server
      const tokenProvider = async (): Promise<string> =>
        yourServerAPI.getTokenForUser(userId).then((auth) => auth.token);

      const user: User = { id: userId, name: userName };
      return StreamVideoClient.getOrCreateInstance({
        apiKey: STREAM_API_KEY, // pass your stream api key
        user,
        tokenProvider,
      });
    },
  });
}

Always use StreamVideoClient.getOrCreateInstance(..) instead of new StreamVideoClient(..). Reusing the client instance preserves call accept/decline states changed while the app was in the background. The getOrCreateInstance method ensures the same user reuses the existing instance.

Set android.smallIcon for best results. Expo prebuild auto-generates a notification icon named notification_icon.png from the app icon. Override it via notification.icon in app.json or app.config.js. Custom icons should be 96x96 PNG grayscale with transparency.

Initialize SDK push notification methods

Call configuration methods outside the application cycle. This ensures configuration is available when the app opens from a push notification:

import "expo-router/entry";
import { setPushConfig } from "src/utils/setPushConfig";
import { setFirebaseListeners } from "src/utils/setFirebaseListeners";

setPushConfig(); // Set push config
setFirebaseListeners(); // Set the firebase listeners

Request notification permissions

Request permissions using react-native-permissions:

import { requestNotifications } from "react-native-permissions";

// This will request POST_NOTIFICATION runtime permission for Anroid 13+
await requestNotifications(["alert", "sound"]);

For a comprehensive guide on requesting all required permissions (camera, microphone, bluetooth, and notifications), see Manage Native Permissions.

Disabling push notifications

Disable push on user logout or user switch:

import { StreamVideoRN } from "@stream-io/video-react-native-sdk";

await StreamVideoRN.onPushLogout();

Optional: Android full-screen incoming call view on locked phone

Set ringingPushNotifications.showWhenLockedAndroid: true to add USE_FULL_SCREEN_INTENT permission and configure MainActivity.

For apps installed on phones running versions Android 13 or lower, the USE_FULL_SCREEN_INTENT permission is enabled by default.

For all apps being installed on Android 14 and above, the Google Play Store revokes the USE_FULL_SCREEN_INTENT for apps that do not have calling or alarm functionalities. Which means, while submitting your app to the play store, if you do declare that 'Making and receiving calls' is a 'core' functionality in your app, this permission is granted by default on Android 14 and above.

If the USE_FULL_SCREEN_INTENT permission is not granted, the notification will show up as an expanded heads up notification on the lock screen.

Incoming and Outgoing call UI in foreground

Show call UIs during ringing calls. See watching for calls for implementation.

Troubleshooting

See the Troubleshooting guide for common issues.