# React Native

Set up push notifications for non-ringing events (missed calls, livestream started, etc.) in your React Native app.

Non-ringing notifications are **not handled by the SDK**. The Stream backend sends push notifications directly to the device via Firebase (Android) or APNs (iOS). Obtaining the device token, displaying the notification, and handling taps is entirely your app's responsibility. You can use any notification library or custom native code.

>
> **Info:** The examples below use `@notifee/react-native` for notification display, `@react-native-firebase/messaging` for Android push handling, and `@react-native-community/push-notification-ios` for iOS push handling. These are just examples — you can use any notification library that fits your app.
>

## Add push provider credentials to Stream

Follow these guides to add push providers:

- **Android** - [Firebase Cloud Messaging](https://getstream.io/video/docs/react-native/advanced/incoming-calls/push-providers/firebase/)
- **iOS** - [Apple Push Notification Service (APNs)](https://getstream.io/video/docs/react-native/advanced/incoming-calls/push-providers/apn-voip/)

## Install Dependencies

```bash title=Terminal
yarn add @react-native-firebase/app \
  @react-native-firebase/messaging

npx pod-install
```

>
> **Tip:** If you already completed the [ringing call setup](https://getstream.io/video/docs/react-native/advanced/incoming-calls/ringing-setup/react-native/), Firebase is already installed and configured. You can skip this step and the Android-specific setup below.
>

## Android-specific setup

1. Create a Firebase project at [Firebase console](https://console.firebase.google.com/)
2. Add your Android app in **Project settings** > **Your apps**. Use your app's package name.
3. Download **google-services.json** to `/android/app/google-services.json`
4. Enable the `google-services` plugin:

```groovy title="/android/build.gradle" {4}
buildscript {
  dependencies {
    // ... other dependencies
    classpath 'com.google.gms:google-services:4.3.15'
  }
}
```

```groovy title="/android/build.gradle" {2}
apply plugin: 'com.android.application'
apply plugin: 'com.google.gms.google-services'
```

>
> **Note:** The **google-services.json** file contains unique and non-secret identifiers of your Firebase project. For more information, see [Understand Firebase Projects](https://firebase.google.com/docs/projects/learn-more#config-files-objects).
>

## iOS-specific setup

### Add background modes

In Xcode, open `Info.plist` and add `UIBackgroundModes`:

```xml
<key>UIBackgroundModes</key>
<array>
  <string>remote-notification</string>
</array>
```

### Enable push notifications capability

Enable Push Notifications in Xcode: `Project` > `Signing & Capabilities`.

## Step 1: Register the device token

The Stream backend needs your device's push token to deliver non-ringing notifications. You need to obtain the token and register it via `client.addDevice()`.

>
> **Tip:** If you have the [ringing call setup](https://getstream.io/video/docs/react-native/advanced/incoming-calls/ringing-setup/react-native/) configured, the SDK already registers the Firebase token on Android. In that case you only need the iOS token registration. If ringing is **not** set up, you need to register the token on both platforms.
>

**Android**

Use `@react-native-firebase/messaging` to obtain the FCM token and register it with Stream:

```tsx title="hooks/useRegisterNonRingingPushToken.android.ts"
import { useEffect, useRef } from "react";
import { useStreamVideoClient } from "@stream-io/video-react-native-sdk";
import messaging from "@react-native-firebase/messaging";

const PUSH_PROVIDER_NAME = "your-firebase-provider-name"; // must match Stream Dashboard

export const useRegisterNonRingingPushToken = () => {
  const client = useStreamVideoClient();
  const lastToken = useRef("");

  useEffect(() => {
    if (!client) return;

    const registerToken = async (token: string) => {
      if (token === lastToken.current) return;
      await client.addDevice(token, "firebase", PUSH_PROVIDER_NAME);
      lastToken.current = token;
    };

    messaging().getToken().then(registerToken);
    const unsubscribe = messaging().onTokenRefresh(registerToken);

    return () => {
      unsubscribe();
      if (lastToken.current) {
        client.removeDevice(lastToken.current);
      }
    };
  }, [client]);
};
```

>
> **Note:** If ringing is already set up, this step is handled by the SDK. You can make this file a no-op:
>
> ```ts title="hooks/useRegisterNonRingingPushToken.android.ts"
> // No-op — SDK registers the Firebase token as part of ringing setup.
> export const useRegisterNonRingingPushToken = () => {};
> ```
>

**iOS**

Use `@react-native-community/push-notification-ios` (or any library) to obtain the APN device token and register it with Stream:

```tsx title="hooks/useRegisterNonRingingPushToken.ts"
import { useEffect, useRef } from "react";
import { useStreamVideoClient } from "@stream-io/video-react-native-sdk";
import PushNotificationIOS from "@react-native-community/push-notification-ios";

const PUSH_PROVIDER_NAME = "your-apn-provider-name"; // must match Stream Dashboard

export const useRegisterNonRingingPushToken = () => {
  const client = useStreamVideoClient();
  const lastToken = useRef("");

  useEffect(() => {
    if (!client) return;

    PushNotificationIOS.addEventListener("register", (token) => {
      if (token !== lastToken.current) {
        client.addDevice(token, "apn", PUSH_PROVIDER_NAME);
        lastToken.current = token;
      }
    });

    PushNotificationIOS.requestPermissions();

    return () => {
      PushNotificationIOS.removeEventListener("register");
      if (lastToken.current) {
        client.removeDevice(lastToken.current);
      }
    };
  }, [client]);
};
```

Call this hook inside your `<StreamVideo>` provider so it has access to the client. Metro resolves the `.android.ts` / `.ts` files automatically per platform.

## Step 2: Handle incoming push and display notification

When a non-ringing push arrives, parse the payload and display a local notification. Create a `setNotificationListeners` method that wraps the platform-specific listeners and the shared display logic, then call it at app startup.

### Shared display logic

This utility can be reused across platforms:

```ts title="utils/nonRingingNotificationUtils.ts"
import notifee, { AndroidImportance } from "@notifee/react-native";

const NON_RINGING_CHANNEL_ID = "stream_non_ringing_calls";
const NON_RINGING_EVENTS = new Set([
  "call.live_started",
  "call.notification",
  "call.missed",
]);

export async function displayNonRingingNotification(
  streamData: Record<string, string>,
) {
  const type = streamData.type;
  if (!NON_RINGING_EVENTS.has(type)) return;

  const createdByName = streamData.created_by_display_name || "Someone";

  await notifee.displayNotification({
    title: type === "call.missed" ? "Missed call" : "Call notification",
    body:
      type === "call.missed"
        ? `You missed a call from ${createdByName}`
        : `${createdByName} is notifying you about a call`,
    data: { call_cid: streamData.call_cid, type, sender: "stream.video" },
    android: {
      channelId: NON_RINGING_CHANNEL_ID,
      importance: AndroidImportance.HIGH,
      pressAction: { id: "default", launchActivity: "default" },
    },
  });
}
```

### Platform-specific listeners

Use platform-specific file extensions (`.android.ts` / `.ts`) so Metro resolves the correct file per platform:

**Android**

In your Firebase message handlers, check for non-ringing Stream events and display a notification using the shared utility:

```ts title="utils/setNotificationListeners.android.ts"
import messaging from "@react-native-firebase/messaging";
import {
  isFirebaseStreamVideoMessage,
  firebaseDataHandler,
} from "@stream-io/video-react-native-sdk";
import { displayNonRingingNotification } from "./nonRingingNotificationUtils";

export const setNotificationListeners = () => {
  messaging().setBackgroundMessageHandler(async (msg) => {
    if (isFirebaseStreamVideoMessage(msg)) {
      await firebaseDataHandler(msg.data);
      if (msg.data?.type !== "call.ring" && !msg.notification) {
        await displayNonRingingNotification(msg.data as Record<string, string>);
      }
    }
  });

  messaging().onMessage(async (msg) => {
    if (isFirebaseStreamVideoMessage(msg)) {
      firebaseDataHandler(msg.data);
      if (msg.data?.type !== "call.ring" && !msg.notification) {
        await displayNonRingingNotification(msg.data as Record<string, string>);
      }
    }
  });
};
```

**iOS**

Stream sends APNs with an empty `aps.alert` for non-ringing events. Forward the notification from `AppDelegate` to JS using `@react-native-community/push-notification-ios`, then display using the same shared utility:

```ts title="utils/setNotificationListeners.ts"
import PushNotificationIOS from "@react-native-community/push-notification-ios";
import { displayNonRingingNotification } from "./nonRingingNotificationUtils";

export const setNotificationListeners = () => {
  PushNotificationIOS.addEventListener("notification", (notification) => {
    const stream = notification.getData()?.stream;
    if (stream?.sender === "stream.video" && stream?.type !== "call.ring") {
      displayNonRingingNotification(stream);
    }
    notification.finish(PushNotificationIOS.FetchResult.NoData);
  });
};
```

>
> **Note:** On iOS, the `PushNotificationIOS` notification event only fires when the app is in the **foreground**. For background/killed state, consider using a [Notification Service Extension](https://developer.apple.com/documentation/usernotifications/modifying_content_in_newly_delivered_notifications) to modify the empty `aps.alert` before iOS displays it.
>

## Step 3: Handle notification taps

When the user taps a non-ringing notification, navigate them to the relevant screen. The notification `data` contains `call_cid` (format: `type:id`) which identifies the call.

```ts title="utils/registerNonRingingNotifications.ts"
import notifee, { EventType } from "@notifee/react-native";

export const registerNonRingingNotificationHandler = () => {
  notifee.onForegroundEvent(({ type, detail }) => {
    if (type === EventType.PRESS && detail.notification?.data?.call_cid) {
      const callId = detail.notification.data.call_cid.split(":")[1];
      // Navigate to the call screen using your navigation method
    }
  });

  notifee.onBackgroundEvent(async ({ type, detail }) => {
    if (type === EventType.PRESS && detail.notification?.data?.call_cid) {
      const callId = detail.notification.data.call_cid.split(":")[1];
      // Navigate to the call screen
    }
  });
};
```

## Step 4: Initialize at app startup

Call your notification setup outside the React lifecycle so it's ready when the app opens from a push notification:

```js title="index.js"
import { AppRegistry } from "react-native";
import { setNotificationListeners } from "src/utils/setNotificationListeners";
import { registerNonRingingNotificationHandler } from "src/utils/registerNonRingingNotifications";
import App from "./App";

setNotificationListeners(); // Registers push listeners and notification handlers
registerNonRingingNotificationHandler(); // Registers tap handlers
AppRegistry.registerComponent("app", () => App);
```

## Disabling push — usually on logout

>
> **Note:** `StreamVideoRN.onPushLogout()` is only required if you have the [ringing call setup](https://getstream.io/video/docs/react-native/advanced/incoming-calls/ringing-setup/react-native/) enabled. It cleans up the ringing push token registered by the SDK. If you only use non-ringing notifications, remove the device token yourself via `client.removeDevice(token)` on logout.
>

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

await StreamVideoRN.onPushLogout();
```

## Troubleshooting

- **No notifications on iOS** — Verify the APN token is registered with Stream via `client.getDevices()`. Check that the push provider name matches the [Stream Dashboard](https://getstream.io/signin/?product=video).
- **Duplicate notifications on Android** — If the FCM message has a `notification` payload, Firebase auto-displays it. Check `msg.notification` and skip your display logic if present.
- **iOS foreground only** — `willPresent` delegate only fires in foreground. Background display requires a Notification Service Extension.

See the [Troubleshooting guide](https://getstream.io/video/docs/react-native/advanced/troubleshooting/) for more common issues.

---

For the most recent version of this documentation, visit [https://getstream.io/video/docs/react-native/advanced/incoming-calls/non-ringing-notifications-setup/react-native/](https://getstream.io/video/docs/react-native/advanced/incoming-calls/non-ringing-notifications-setup/react-native/).