diff --git a/packages/react-native-sdk/docusaurus/docs/reactnative/04-ui-components/call/ringing-call-content.mdx b/packages/react-native-sdk/docusaurus/docs/reactnative/04-ui-components/call/ringing-call-content.mdx index be92bd4ba9..fcb63b1c18 100644 --- a/packages/react-native-sdk/docusaurus/docs/reactnative/04-ui-components/call/ringing-call-content.mdx +++ b/packages/react-native-sdk/docusaurus/docs/reactnative/04-ui-components/call/ringing-call-content.mdx @@ -49,15 +49,21 @@ To use the `RingingCallContent` you can do the following: ```tsx {11} import { + StreamCall, useCalls, RingingCallContent, } from '@stream-io/video-react-native-sdk'; const Call = () => { - const calls = useCalls(); + // filter for ringing calls + const calls = useCalls().filter( + (c) => c.state.callingState === CallingState.RINGING, + ); + const call = calls[0]; + if (!call) return null; return ( - + ); diff --git a/packages/react-native-sdk/docusaurus/docs/reactnative/06-advanced/03-ringing.mdx b/packages/react-native-sdk/docusaurus/docs/reactnative/06-advanced/03-ringing.mdx index 0192a3c50b..6d0334974e 100644 --- a/packages/react-native-sdk/docusaurus/docs/reactnative/06-advanced/03-ringing.mdx +++ b/packages/react-native-sdk/docusaurus/docs/reactnative/06-advanced/03-ringing.mdx @@ -44,50 +44,65 @@ The caller will automatically join the call once the first callee accepts the ca The call will automatically stop if every callee rejects the call. :::note -When ring is true, a push notification will be sent to the members, provided their app have the required setup. +When ring is true, a **push notification** will be sent to the members, provided their app have the required setup. For more details around push notifications, please check [this page](../../advanced/push-notifications/overview). ::: ## Watch for incoming and outgoing calls -The easiest way to watch for incoming and outgoing calls is to use the `useCalls` hook. +The easiest way to watch for incoming and outgoing calls is to use the `useCalls` hook and the [`RingingCallContent`](../../ui-components/call/ringing-call-content) component. -```tsx -import { useCalls, CallingState } from '@stream-io/video-react-native-sdk'; +**Important**: Make sure that the ringing calls are watched in the root component of your app. This makes sure that in whichever screen the user is in, or if the app was opened from a push notification it is shown. Below is an example of how to watch for ringing calls in the root component of your App. -export const MyCallUI = () => { - const calls = useCalls(); +```ts +import { SafeAreaView, StyleSheet } from 'react-native'; +import { + StreamCall, + StreamVideo, + useCalls, + RingingCallContent, + StreamVideoClient, + User, +} from '@stream-io/video-react-native-sdk'; - // handle incoming ring calls - const incomingCalls = calls.filter( - (call) => - call.isCreatedByMe === false && - call.state.callingState === CallingState.RINGING, +const user: User = { + id: 'sara', +}; +const apiKey = ''; +const tokenProvider = () => Promise.resolve(''); +const client = StreamVideoClient.getOrCreateInstance({ apiKey, tokenProvider, user }); + +const RingingCalls = () => { + // filter for ringing calls + const calls = useCalls().filter( + (c) => c.state.callingState === CallingState.RINGING, ); - - const [incomingCall] = incomingCalls; - if (incomingCall) { - // render the incoming call UI - return ; - } - - // handle outgoing ring calls - const outgoingCalls = calls.filter( - (call) => - call.isCreatedByMe === true && - call.state.callingState === CallingState.RINGING, + const call = calls[0]; + if (!call) return null; + + return ( + + + + + + ); +} + +const App = () => { + return ( + + + + ); - - const [outgoingCall] = outgoingCalls; - if (outgoingCall) { - // render the outgoing call UI - return ; - } - - return null; }; + +export default App; ``` +In the above example, the component `RingingCalls` renders over the rest of the App whenever there is a incoming or outgoing call. Alternatively you can use a Modal view or Dialog to show there is a ringing call over the rest of your app. + ## Canceling a call A caller can cancel an outgoing call until the first callee accepts the call. Canceling a call will stop the signaling flow. diff --git a/packages/react-native-sdk/docusaurus/docs/reactnative/06-advanced/04-push-notifications/03-ringing-setup/01-react-native.mdx b/packages/react-native-sdk/docusaurus/docs/reactnative/06-advanced/04-push-notifications/03-ringing-setup/01-react-native.mdx index ef629e8509..ef29b9a05b 100644 --- a/packages/react-native-sdk/docusaurus/docs/reactnative/06-advanced/04-push-notifications/03-ringing-setup/01-react-native.mdx +++ b/packages/react-native-sdk/docusaurus/docs/reactnative/06-advanced/04-push-notifications/03-ringing-setup/01-react-native.mdx @@ -38,65 +38,6 @@ So what did we install precisely? - `react-native-voip-push-notification` for handling incoming [PushKit](https://developer.apple.com/documentation/pushkit) notifications on iOS. - `react-native-callkeep` for reporting incoming calls to iOS [CallKit](https://developer.apple.com/documentation/callkit). -## Android-specific setup - -1. To create a Firebase project, go to the [Firebase console](https://console.firebase.google.com/) and click on **Add project**. - -2. 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.** - -3. After registering the app, download the **google-services.json** file and place it inside of your project at the following location: `/android/app/google-services.json.` - -4. To allow Firebase on Android to use the credentials, the `google-services` plugin must be enabled on the project. This requires modification to two files in the Android directory. Add the highlighted lines in the relevant files: - -```groovy title="/android/build.gradle" -buildscript { - dependencies { - // ... other dependencies - // highlight-next-line - classpath 'com.google.gms:google-services:4.3.15' - } -} -``` - -```groovy title="/android/build.gradle" -apply plugin: 'com.android.application' -// highlight-next-line -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). - -::: - -### Add declarations in AndroidManifest - -Add the following in `AndroidManifest.xml`: - -```xml title="AndroidManifest.xml" - - - - - -``` - -### 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 using [`react-native-permissions`](https://github.com/zoontek/react-native-permissions) library: - - -```js -import { requestNotifications } from 'react-native-permissions'; - -await requestNotifications(['alert', 'sound']); -``` - ## iOS-specific setup ### Disable Firebase installation @@ -239,56 +180,150 @@ The final method to add is the one that gets invoked when there is a VoIP push n } ``` -## Setup the push config for the SDK +## Android-specific setup + +1. To create a Firebase project, go to the [Firebase console](https://console.firebase.google.com/) and click on **Add project**. + +2. 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.** + +3. After registering the app, download the **google-services.json** file and place it inside of your project at the following location: `/android/app/google-services.json.` + +4. To allow Firebase on Android to use the credentials, the `google-services` plugin must be enabled on the project. This requires modification to two files in the Android directory. Add the highlighted lines in the relevant files: + +```groovy title="/android/build.gradle" +buildscript { + dependencies { + // ... other dependencies + // highlight-next-line + classpath 'com.google.gms:google-services:4.3.15' + } +} +``` + +```groovy title="/android/build.gradle" +apply plugin: 'com.android.application' +// highlight-next-line +apply plugin: 'com.google.gms.google-services' +``` -The SDK automatically processes the incoming 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, +:::note -### Add the ability to statically navigate to screens in your app +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). -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 adding the ability to [navigate without the navigation property in the react-navigation library](https://reactnavigation.org/docs/navigating-without-navigation-prop/). +::: -The following is an example implementation of a utility file that has helpers to statically navigate in the app: +### Add declarations in AndroidManifest -```ts title="src/utils/staticNavigation.ts" -import { createNavigationContainerRef } from '@react-navigation/native'; +Add the following in `AndroidManifest.xml`: -import { RootStackParamList } from '../navigation/types'; +```xml title="AndroidManifest.xml" + + + -export const navigationRef = createNavigationContainerRef(); + +``` -/** - * This is used to run the navigation logic from root level even before the navigation is ready - */ -export const staticNavigate = ( - ...navigationArgs: Parameters -) => { - // note the use of setInterval, it is responsible for constantly checking if requirements are met and then navigating - // highlight-start - const intervalId = setInterval(async () => { - // run only when the navigation is ready and add any other requirements (like authentication) - if (navigationRef.isReady() && GlobalState.hasAuthentication) { - clearInterval(intervalId); - navigationRef.navigate(...navigationArgs); +### 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 using [`react-native-permissions`](https://github.com/zoontek/react-native-permissions) library: + + +```js +import { requestNotifications } from 'react-native-permissions'; + +await requestNotifications(['alert', 'sound']); +``` + + +### Add Firebase message handlers +To process the incoming push notifications, the SDK provides the utility functions that you must add to your existing or new Firebase notification listeners. Below is the snippet of how to add the firebase listeners: + +```ts title="src/utils/setFirebaseListeners.ts" +import messaging from '@react-native-firebase/messaging'; +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) } - }, 300); - // highlight-end + }); + + // 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) + } + }); }; ``` -When doing this it is _very important_ to set the `navigationRef` in your navigation container as shown below: +**The Firebase message handlers** +* The `onMessage` handler should not be added if you do not want notifications to show up when the app is in the foreground. When the app is in foreground, you would automatically see the incoming call screen. +* 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. -```ts -import { navigationRef } from './src/utils/staticNavigationUtils'; +**The Notifee event handlers** +* The `onForegroundEvent` handler should not be added if you did not add foreground notifications above. +* 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. + +:::infoNOTE +If you had disabled the installation 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: -// highlight-next-line - - -; +``` +setFirebaseListeners.android.ts +setFirebaseListeners.ts ``` -### Setup the push config +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: -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, +```ts title="setFirebaseListeners.ts" +export const setFirebaseListeners = () => { + // do nothing +}; +``` + +This is to ensure that `@react-native-firebase/messaging` is only imported on the Android platform. +::: + +## Setup the push notifications configuration for the SDK + +The SDK automatically processes the incoming push notifications once the setup above is done if the push notifications configuration has been set using `StreamVideoRN.setPushConfig`. Below is an example of how this method can be called, ```ts title="src/utils/setPushConfig.ts" import { @@ -298,7 +333,6 @@ import { import { AndroidImportance } from '@notifee/react-native'; import AsyncStorage from '@react-native-async-storage/async-storage'; import { STREAM_API_KEY } from '../../constants'; -import { staticNavigate } from './staticNavigationUtils'; export function setPushConfig() { StreamVideoRN.setPushConfig({ @@ -329,15 +363,6 @@ export function setPushConfig() { getBody: (_createdUserName: string) => 'Tap to answer the call', }, }, - // add the callback to be executed a call is accepted, used for navigation - navigateAcceptCall: () => { - staticNavigate({ name: 'ActiveCallScreen', params: undefined }); - }, - // add the callback to be executed when a notification is tapped, - // but the user did not press accept or decline, used for navigation - navigateToIncomingCall: () => { - staticNavigate({ name: 'IncomingCallScreen', params: undefined }); - }, // add the async callback to create a video client // for incoming calls in the background on a push notification createStreamVideoClient: async () => { @@ -359,16 +384,26 @@ export function setPushConfig() { } ``` -Now, call the method outside of your application cycle. That is, alongside your `AppRegistry.registerComponent()` method call at the entry point of your application code. 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 config as soon as the JS bridge is initialized. Following is an example, +## Call the created methods outside of the application lifecycle + +Call the methods we have created outside of your application cycle. That is, alongside your `AppRegistry.registerComponent()` method call at the entry point of your application code. 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, ```js title="index.js" import { AppRegistry } from 'react-native'; +// highlight-next-line import { setPushConfig } from 'src/utils/setPushConfig'; +// highlight-next-line +import { setFirebaseListeners } from 'src/utils/setFirebaseListeners'; import App from './App'; // Set push config // highlight-next-line setPushConfig(); +// Set the firebase listeners +// highlight-next-line +setFirebaseListeners(); AppRegistry.registerComponent('app', () => App); ``` @@ -408,8 +443,16 @@ For all apps being installed on Android 14 and above, the Google Play Store revo 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. ::: +## Show the incoming and outgoing call UI when app is on the foreground + +The last part of the setup for ringing calls is to show the incoming and outgoing call UIs in the app whenever there is a ringing call. If this was not implemented before, please headover to [this page](../../../ringing-calls/#watch-for-incoming-and-outgoing-calls) of our documentation to implement that. + ## 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](https://dashboard.getstream.io/) 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](https://getstream.io/contact/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. diff --git a/packages/react-native-sdk/docusaurus/docs/reactnative/06-advanced/04-push-notifications/03-ringing-setup/02-expo.mdx b/packages/react-native-sdk/docusaurus/docs/reactnative/06-advanced/04-push-notifications/03-ringing-setup/02-expo.mdx index c04853beb9..8d301dc6b5 100644 --- a/packages/react-native-sdk/docusaurus/docs/reactnative/06-advanced/04-push-notifications/03-ringing-setup/02-expo.mdx +++ b/packages/react-native-sdk/docusaurus/docs/reactnative/06-advanced/04-push-notifications/03-ringing-setup/02-expo.mdx @@ -148,37 +148,88 @@ React Native Firebase Messaging automatically registers the device with APNs to } ``` -## Setup the push config for the SDK +## Add Firebase message handlers +To process the incoming push notifications, the SDK provides the utility functions that you must add to your existing or new Firebase notification listeners. Below is the snippet of how to add the firebase listeners: -The SDK automatically processes the incoming 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, +```ts title="src/utils/setFirebaseListeners.ts" +import messaging from '@react-native-firebase/messaging'; +import { + isFirebaseStreamVideoMessage, + firebaseDataHandler, + onAndroidNotifeeEvent, + isNotifeeStreamVideoEvent, +} from '@stream-io/video-react-native-sdk'; -### Add the ability to statically navigate to screens in your app +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) + } + }); -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](https://docs.expo.dev/routing/navigating-pages/#imperative-navigation). + // 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) + } + }); -The following is an example implementation of a utility file that has helpers to statically navigate in the app: + // 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) + } + }); +}; +``` -```ts title="src/utils/staticNavigation.ts" -import { User } from '@stream-io/video-react-native-sdk'; -import { router } from 'expo-router'; +**The Firebase message handlers** +* The `onMessage` handler should not be added if you do not want notifications to show up when the app is in the foreground. When the app is in foreground, you would automatically see the incoming call screen. +* 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. -/** - * This is used to run the navigation logic from root level - */ -export const staticNavigateToRingingCall = () => { - const intervalId = setInterval(async () => { - // add any requirements here (like authentication) - if (GlobalState.hasAuthentication) { - clearInterval(intervalId); - router.push('/ringing'); - } - }, 300); +**The Notifee event handlers** +* The `onForegroundEvent` handler should not be added if you did not add foreground notifications above. +* 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. + +:::infoNOTE +If you had 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: + +```ts title="setFirebaseListeners.ts" +export const setFirebaseListeners = () => { + // do nothing }; ``` +::: -### Setup the push config +## Setup the push notifications configuration for the SDK -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, +The SDK automatically processes the incoming push notifications once the setup above is done if the push notifications configuration has been set using `StreamVideoRN.setPushConfig`. Below is an example of how this method can be called, ```ts title="src/utils/setPushConfig.ts" import { @@ -188,7 +239,6 @@ import { import { AndroidImportance } from '@notifee/react-native'; import AsyncStorage from '@react-native-async-storage/async-storage'; import { STREAM_API_KEY } from '../../constants'; -import { staticNavigateToRingingCall } from './staticNavigationUtils'; export function setPushConfig() { StreamVideoRN.setPushConfig({ @@ -221,15 +271,6 @@ export function setPushConfig() { getBody: (_createdUserName: string) => 'Tap to answer the call', }, }, - // add the callback to be executed a call is accepted, used for navigation - navigateAcceptCall: () => { - staticNavigateToRingingCall(); - }, - // add the callback to be executed when a notification is tapped, - // but the user did not press accept or decline, used for navigation - navigateToIncomingCall: () => { - staticNavigateToRingingCall(); - }, // add the async callback to create a video client // for incoming calls in the background on a push notification createStreamVideoClient: async () => { @@ -251,18 +292,30 @@ export function setPushConfig() { } ``` -Now, call the method outside of your application cycle. That is inside `index.js`. 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 config as soon as the JS bridge is initialized. Following is an example, +## 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, ```js title="index.js" import 'expo-router/entry'; -import { setPushConfig } from './utils/setPushConfig'; +// highlight-next-line +import { setPushConfig } from 'src/utils/setPushConfig'; +// highlight-next-line +import { setFirebaseListeners } from 'src/utils/setFirebaseListeners'; +// Set push config +// highlight-next-line setPushConfig(); +// Set the firebase listeners +// highlight-next-line +setFirebaseListeners(); ``` -## Request for notification permissions +## Request for notification permissions in Android -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: +At an appropriate place in your app, request for notification permissions from the user on Android. Below is a small example of how to request permissions in Expo: ```js import {PermissionsAndroid} from 'react-native'; @@ -291,8 +344,16 @@ For all apps being installed on Android 14 and above, the Google Play Store revo 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. ::: +## Show the incoming and outgoing call UI when app is on the foreground + +The last part of the setup for ringing calls is to show the incoming and outgoing call UIs in the app whenever there is a ringing call. If this was not implemented before, please headover to [this page](../../../ringing-calls/#watch-for-incoming-and-outgoing-calls) of our documentation to implement that. + ## 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](https://dashboard.getstream.io/) 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](https://getstream.io/contact/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. \ No newline at end of file diff --git a/packages/react-native-sdk/docusaurus/docs/reactnative/06-advanced/04-push-notifications/04-other-than-ringing-setup/01-react-native.mdx b/packages/react-native-sdk/docusaurus/docs/reactnative/06-advanced/04-push-notifications/04-other-than-ringing-setup/01-react-native.mdx index 92f8cb4bf7..d0ea800118 100644 --- a/packages/react-native-sdk/docusaurus/docs/reactnative/06-advanced/04-push-notifications/04-other-than-ringing-setup/01-react-native.mdx +++ b/packages/react-native-sdk/docusaurus/docs/reactnative/06-advanced/04-push-notifications/04-other-than-ringing-setup/01-react-native.mdx @@ -98,6 +98,10 @@ In Xcode: Open `Info.plist` file and add the following in `UIBackgroundModes`. B ``` +### Enable push notifications capability + +Enable the Push Notifications capability in the Xcode `Project` > `Signing & Capabilities` pane. + ### Update `AppDelegate.h` At the top of the file, add: @@ -182,6 +186,129 @@ Then add the following lines to `didFinishLaunchingWithOptions`: ### Enable push notifications To receive push notifications, enable the Push Notifications capability in the Xcode `Project` > `Signing & Capabilities` pane. + +## 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 + +When Firebase a push message, it must be processed first. For this we expose handler function from the SDK which reads the message and displays it using the `@notifee/react-native` library. Below is the snippet to add message handlers: + +```ts title="src/utils/setPushMessageHandlers.ts" +import messaging from '@react-native-firebase/messaging'; +import { + isFirebaseStreamVideoMessage, + firebaseDataHandler, +} from '@stream-io/video-react-native-sdk'; + +export const setFirebaseListeners = () => { + // 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) + } + }); +}; +``` + +**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. + +:::infoNOTE +If you had disabled the installation 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: + +```ts title="setPushMessageListeners.ts" +export const setFirebaseListeners = () => { + // do nothing +}; +``` + +This is to ensure that `@react-native-firebase/messaging` is only imported on the Android platform. +::: + +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 onPress listeners + +Below is the snippet of how to add the notification onPress listeners for Android using `@notifee/react-native` library: + +```ts title="src/utils/setNotifeeListeners.ts" +import { + isNotifeeStreamVideoEvent, + onAndroidNotifeeEvent, +} 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 (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** + +Below is the snippet of how to add the notification onPress listeners for iOS using `@react-native-community/push-notification-ios` library. Add the following `useEffect` in the root component of your App, this is most likely in `App.tsx`. + +```ts +import PushNotificationIOS from '@react-native-community/push-notification-ios'; +import { + isPushNotificationiOSStreamVideoEvent, + onPushNotificationiOSStreamVideoEvent, +} from '@stream-io/video-react-native-sdk'; + +useEffect(() => { + PushNotificationIOS.addEventListener('notification', (notification) => { + if (isPushNotificationiOSStreamVideoEvent(notification)) { + onPushNotificationiOSStreamVideoEvent(notification); + } else { + // any other APN notifications + } + }); + return () => { + PushNotificationIOS.removeEventListener('notification'); + }; +}, []); +``` + ## Setup the push config for the SDK The SDK automatically processes the incoming 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, @@ -307,11 +434,20 @@ export function setPushConfig() { } ``` -Now, call the method outside of your application cycle. That is, alongside your `AppRegistry.registerComponent()` method call at the entry point of your application code. 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 config as soon as the JS bridge is initialized. Following is an example, +## Call the created methods outside of the application lifecycle + +Call the methods we have created outside of your application cycle. That is, alongside your `AppRegistry.registerComponent()` method call at the entry point of your application code. 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 config as soon as the JS bridge is initialized. Following is an example, + +Following is an example, ```js title="index.js" import { AppRegistry } from 'react-native'; +// highlight-next-line import { setPushConfig } from 'src/utils/setPushConfig'; +// highlight-next-line +import { setNotifeeListeners } from 'src/utils/setNotifeeListeners'; +// highlight-next-line +import { setFirebaseListeners } from 'src/utils/setFirebaseListeners'; import App from './App'; // Set push config @@ -346,4 +482,8 @@ await StreamVideoRN.onPushLogout(); - 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](https://dashboard.getstream.io/) 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](https://getstream.io/contact/support/). \ No newline at end of file +- If you are still having trouble with Push Notifications, please submit a ticket to us at [support](https://getstream.io/contact/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. \ No newline at end of file diff --git a/packages/react-native-sdk/docusaurus/docs/reactnative/06-advanced/04-push-notifications/04-other-than-ringing-setup/02-expo.mdx b/packages/react-native-sdk/docusaurus/docs/reactnative/06-advanced/04-push-notifications/04-other-than-ringing-setup/02-expo.mdx index 037db7a945..6c84e04f3e 100644 --- a/packages/react-native-sdk/docusaurus/docs/reactnative/06-advanced/04-push-notifications/04-other-than-ringing-setup/02-expo.mdx +++ b/packages/react-native-sdk/docusaurus/docs/reactnative/06-advanced/04-push-notifications/04-other-than-ringing-setup/02-expo.mdx @@ -71,6 +71,205 @@ In **app.json**, in the `plugins` field, add true to the `enableNonRingingPushNo If Expo EAS build is not used, please do `npx expo prebuild --clean` to generate the native directories again after adding the config plugins. +## 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. + +```bash title=Terminal +yarn add @react-native-firebase/app +yarn add @react-native-firebase/messaging +``` + +Below is the snippet to add message handlers: + +```ts title="src/utils/setPushMessageHandlers.ts" +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: + +```js title="/firebase.json" +{ + "react-native": { + "messaging_ios_auto_register_for_remote_messages": false + } +} +``` + + + +First we have to install the `expo-task-manager` library. + +```bash title=Terminal +npx expo install expo-task-manager +``` + +Below is the snippet to add message handlers: + +```ts title="src/utils/setPushMessageHandlers.ts" +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: + +```ts title="src/utils/setNotifeeListeners.ts" +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`. + +```ts +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 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, @@ -188,13 +387,27 @@ export function setPushConfig() { } ``` -Now, call the method outside of your application cycle. That is inside `index.js`. 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 config as soon as the JS bridge is initialized. Following is an example, +## 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, ```js title="index.js" import 'expo-router/entry'; -import { setPushConfig } from './utils/setPushConfig'; - +// highlight-next-line +import { setPushConfig } from 'src/utils/setPushConfig'; +// highlight-next-line +import { setNotifeeListeners } from 'src/utils/setNotifeeListeners'; +// highlight-next-line +import { setPushMessageListeners } from 'src/utils/setPushMessageListeners'; + +// highlight-next-line setPushConfig(); +// highlight-next-line +setNotifeeListeners(); +// highlight-next-line +setPushMessageListeners(); ``` ## Request for notification permissions @@ -220,4 +433,8 @@ await StreamVideoRN.onPushLogout(); - 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](https://dashboard.getstream.io/) 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](https://getstream.io/contact/support/). \ No newline at end of file +- If you are still having trouble with Push Notifications, please submit a ticket to us at [support](https://getstream.io/contact/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. \ No newline at end of file diff --git a/packages/react-native-sdk/package.json b/packages/react-native-sdk/package.json index 300636a458..58170b2f5e 100644 --- a/packages/react-native-sdk/package.json +++ b/packages/react-native-sdk/package.json @@ -65,7 +65,6 @@ "expo": ">=47.0.0", "expo-build-properties": "*", "expo-notifications": "*", - "expo-task-manager": "*", "react": ">=17.0.0", "react-native": ">=0.67.0", "react-native-callkeep": ">=4.3.11", @@ -100,9 +99,6 @@ "expo-notifications": { "optional": true }, - "expo-task-manager": { - "optional": true - }, "react-native-callkeep": { "optional": true }, @@ -122,8 +118,8 @@ "@notifee/react-native": "7.8.0", "@react-native-community/netinfo": "9.3.9", "@react-native-community/push-notification-ios": "1.11.0", - "@react-native-firebase/app": "17.5.0", - "@react-native-firebase/messaging": "17.5.0", + "@react-native-firebase/app": "19.2.2", + "@react-native-firebase/messaging": "19.2.2", "@react-native/eslint-config": "^0.74.84", "@stream-io/react-native-webrtc": "118.1.0", "@stream-io/video-filters-react-native": "workspace:^", @@ -139,8 +135,7 @@ "expo": "50.0.19", "expo-build-properties": "^0.12.3", "expo-modules-core": "1.12.16", - "expo-notifications": "~0.27.8", - "expo-task-manager": "~11.7.3", + "expo-notifications": "~0.28.18", "jest": "^29.7.0", "react-native": "0.71.8", "react-native-builder-bob": "^0.23.2", diff --git a/packages/react-native-sdk/src/hooks/push/useIosCallKeepEventsSetupEffect.ts b/packages/react-native-sdk/src/hooks/push/useIosCallKeepEventsSetupEffect.ts index 88e2527837..3781630505 100644 --- a/packages/react-native-sdk/src/hooks/push/useIosCallKeepEventsSetupEffect.ts +++ b/packages/react-native-sdk/src/hooks/push/useIosCallKeepEventsSetupEffect.ts @@ -2,16 +2,23 @@ import { useEffect } from 'react'; import { voipCallkeepCallOnForegroundMap$, voipPushNotificationCallCId$, -} from '../../utils/push/rxSubjects'; +} from '../../utils/push/internal/rxSubjects'; import { RxUtils } from '@stream-io/video-client'; -import { - iosCallkeepAcceptCall, - iosCallkeepRejectCall, -} from '../../utils/push/ios'; import { getCallKeepLib } from '../../utils/push/libs'; -import { StreamVideoRN } from '../../utils'; +import { StreamVideoRN } from '../../utils/StreamVideoRN'; +import type { StreamVideoConfig } from '../../utils/StreamVideoRN/types'; +import { + clearPushWSEventSubscriptions, + processCallFromPushInBackground, +} from '../../utils/push/internal/utils'; +import { + pushAcceptedIncomingCallCId$, + voipCallkeepAcceptedCallOnNativeDialerMap$, +} from '../../utils/push/internal/rxSubjects'; import { Platform } from 'react-native'; +type PushConfig = NonNullable; + /** * This hook is used to listen to callkeep events and do the necessary actions */ @@ -62,3 +69,52 @@ export const useIosCallKeepEventsSetupEffect = () => { }; }, []); }; + +const iosCallkeepAcceptCall = ( + call_cid: string | undefined, + callUUIDFromCallkeep: string +) => { + if (!shouldProcessCallFromCallkeep(call_cid, callUUIDFromCallkeep)) { + return; + } + clearPushWSEventSubscriptions(); + // to call end callkeep later if ended in app and not through callkeep + voipCallkeepAcceptedCallOnNativeDialerMap$.next({ + uuid: callUUIDFromCallkeep, + cid: call_cid, + }); + // to process the call in the app + pushAcceptedIncomingCallCId$.next(call_cid); + // no need to keep these references anymore + voipCallkeepCallOnForegroundMap$.next(undefined); +}; + +const iosCallkeepRejectCall = async ( + call_cid: string | undefined, + callUUIDFromCallkeep: string, + pushConfig: PushConfig +) => { + if (!shouldProcessCallFromCallkeep(call_cid, callUUIDFromCallkeep)) { + return; + } + clearPushWSEventSubscriptions(); + // no need to keep these references anymore + voipCallkeepAcceptedCallOnNativeDialerMap$.next(undefined); + voipCallkeepCallOnForegroundMap$.next(undefined); + voipPushNotificationCallCId$.next(undefined); + await processCallFromPushInBackground(pushConfig, call_cid, 'decline'); +}; + +/** + * Helper function to determine if the answer/end call event from callkeep must be processed + * Just checks if we have a valid call_cid and acts as a type guard for call_cid + */ +const shouldProcessCallFromCallkeep = ( + call_cid: string | undefined, + callUUIDFromCallkeep: string +): call_cid is string => { + if (!call_cid || !callUUIDFromCallkeep) { + return false; + } + return true; +}; diff --git a/packages/react-native-sdk/src/hooks/push/useIosCallkeepWithCallingStateEffect.ts b/packages/react-native-sdk/src/hooks/push/useIosCallkeepWithCallingStateEffect.ts index 6e48e1678e..ccdf7a6312 100644 --- a/packages/react-native-sdk/src/hooks/push/useIosCallkeepWithCallingStateEffect.ts +++ b/packages/react-native-sdk/src/hooks/push/useIosCallkeepWithCallingStateEffect.ts @@ -7,7 +7,7 @@ import { getCallKeepLib } from '../../utils/push/libs'; import { voipCallkeepAcceptedCallOnNativeDialerMap$, voipCallkeepCallOnForegroundMap$, -} from '../../utils/push/rxSubjects'; +} from '../../utils/push/internal/rxSubjects'; const isNonActiveCallingState = (callingState: CallingState) => { return ( diff --git a/packages/react-native-sdk/src/hooks/push/useIosVoipPushEventsSetupEffect.ts b/packages/react-native-sdk/src/hooks/push/useIosVoipPushEventsSetupEffect.ts index c2516cb412..098223241d 100644 --- a/packages/react-native-sdk/src/hooks/push/useIosVoipPushEventsSetupEffect.ts +++ b/packages/react-native-sdk/src/hooks/push/useIosVoipPushEventsSetupEffect.ts @@ -12,11 +12,11 @@ import { NativeModules } from 'react-native'; import { canAddPushWSSubscriptionsRef, shouldCallBeEnded, -} from '../../utils/push/utils'; +} from '../../utils/push/internal/utils'; import { pushUnsubscriptionCallbacks$, voipPushNotificationCallCId$, -} from '../../utils/push/rxSubjects'; +} from '../../utils/push/internal/rxSubjects'; import { RxUtils, getLogger } from '@stream-io/video-client'; let lastVoipToken = { token: '', userId: '' }; diff --git a/packages/react-native-sdk/src/hooks/push/useProcessPushCallEffect.ts b/packages/react-native-sdk/src/hooks/push/useProcessPushCallEffect.ts index 2ba1cfef5a..a26e0c1fc8 100644 --- a/packages/react-native-sdk/src/hooks/push/useProcessPushCallEffect.ts +++ b/packages/react-native-sdk/src/hooks/push/useProcessPushCallEffect.ts @@ -3,7 +3,7 @@ import { pushAndroidBackgroundDeliveredIncomingCallCId$, pushRejectedIncomingCallCId$, pushTappedIncomingCallCId$, -} from '../../utils/push/rxSubjects'; +} from '../../utils/push/internal/rxSubjects'; import { useEffect } from 'react'; import { StreamVideoRN } from '../../utils'; import { @@ -12,7 +12,7 @@ import { } from '@stream-io/video-react-bindings'; import { BehaviorSubject } from 'rxjs'; import { filter } from 'rxjs/operators'; -import { processCallFromPush } from '../../utils/push/utils'; +import { processCallFromPush } from '../../utils/push/internal/utils'; import { StreamVideoClient } from '@stream-io/video-client'; import type { StreamVideoConfig } from '../../utils/StreamVideoRN/types'; @@ -92,11 +92,6 @@ const createCallSubscription = ( .pipe(filter(cidIsNotUndefined)) .subscribe(async (callCId) => { await processCallFromPush(client, callCId, action, pushConfig); - if (action === 'accept') { - pushConfig.navigateAcceptCall(); - } else if (action === 'pressed' || action === 'backgroundDelivered') { - pushConfig.navigateToIncomingCall(); - } behaviourSubjectWithCallCid.next(undefined); // remove the current call id to avoid processing again }); }; diff --git a/packages/react-native-sdk/src/hooks/push/useProcessPushNonRingingCallEffect.ts b/packages/react-native-sdk/src/hooks/push/useProcessPushNonRingingCallEffect.ts index 7f8a5d3b11..3f220a79f5 100644 --- a/packages/react-native-sdk/src/hooks/push/useProcessPushNonRingingCallEffect.ts +++ b/packages/react-native-sdk/src/hooks/push/useProcessPushNonRingingCallEffect.ts @@ -1,4 +1,4 @@ -import { pushNonRingingCallData$ } from '../../utils/push/rxSubjects'; +import { pushNonRingingCallData$ } from '../../utils/push/internal/rxSubjects'; import { useEffect } from 'react'; import { StreamVideoRN } from '../../utils'; import { @@ -6,7 +6,7 @@ import { useStreamVideoClient, } from '@stream-io/video-react-bindings'; import { filter } from 'rxjs/operators'; -import { processNonIncomingCallFromPush } from '../../utils/push/utils'; +import { processNonIncomingCallFromPush } from '../../utils/push/internal/utils'; /** * This hook is used to process the non ringing call data via push notifications using the relevant rxjs subject @@ -16,7 +16,7 @@ import { processNonIncomingCallFromPush } from '../../utils/push/utils'; export const useProcessPushNonRingingCallEffect = () => { const client = useStreamVideoClient(); const connectedUserId = useConnectedUser()?.id; - // The Effect to join/reject call automatically when incoming call was received and processed from push notification + // The Effect to automatically add the non ringing call to our low level client state useEffect(() => { const pushConfig = StreamVideoRN.getConfig().push; if (!pushConfig || !client || !connectedUserId) { diff --git a/packages/react-native-sdk/src/hooks/useAndroidKeepCallAliveEffect.ts b/packages/react-native-sdk/src/hooks/useAndroidKeepCallAliveEffect.ts index 970a0c6a2d..87295ae57f 100644 --- a/packages/react-native-sdk/src/hooks/useAndroidKeepCallAliveEffect.ts +++ b/packages/react-native-sdk/src/hooks/useAndroidKeepCallAliveEffect.ts @@ -37,6 +37,7 @@ async function startForegroundService(call_cid: string) { ); return; } + // channel id is not required for notifee as its only used on android 7 and below here await notifeeLib.default.displayNotification({ id: call_cid, title, diff --git a/packages/react-native-sdk/src/providers/StreamCall.tsx b/packages/react-native-sdk/src/providers/StreamCall.tsx index b5072f5f55..ddb684b9ea 100644 --- a/packages/react-native-sdk/src/providers/StreamCall.tsx +++ b/packages/react-native-sdk/src/providers/StreamCall.tsx @@ -5,7 +5,7 @@ import { useIosCallkeepWithCallingStateEffect } from '../hooks/push/useIosCallke import { canAddPushWSSubscriptionsRef, clearPushWSEventSubscriptions, -} from '../utils/push/utils'; +} from '../utils/push/internal/utils'; import { useAndroidKeepCallAliveEffect } from '../hooks/useAndroidKeepCallAliveEffect'; import { AppState, NativeModules, Platform } from 'react-native'; import { shouldDisableIOSLocalVideoOnBackgroundRef } from '../utils/internal/shouldDisableIOSLocalVideoOnBackground'; diff --git a/packages/react-native-sdk/src/utils/StreamVideoRN/index.ts b/packages/react-native-sdk/src/utils/StreamVideoRN/index.ts index f115c37d25..8fd1469ef1 100644 --- a/packages/react-native-sdk/src/utils/StreamVideoRN/index.ts +++ b/packages/react-native-sdk/src/utils/StreamVideoRN/index.ts @@ -1,7 +1,5 @@ -import { setupFirebaseHandlerAndroid } from '../push/android'; import { StreamVideoConfig } from './types'; import pushLogoutCallbacks from '../internal/pushLogoutCallback'; -import { setupRemoteNotificationsHandleriOS } from '../push/ios'; import newNotificationCallbacks, { NewCallNotificationCallback, } from '../internal/newNotificationCallbacks'; @@ -66,11 +64,18 @@ export class StreamVideoRN { // Ignoring this config as push config was already set return; } + if ( + __DEV__ && + (pushConfig.navigateAcceptCall || pushConfig.navigateToIncomingCall) + ) { + throw new Error( + `Support for navigateAcceptCall or navigateToIncomingCall in pushConfig has been removed. + Please watch for incoming and outgoing calls in the root component of your app. + Please see https://getstream.io/video/docs/react-native/advanced/ringing-calls/#watch-for-incoming-and-outgoing-calls for more information.` + ); + } + this.config.push = pushConfig; - // After getting the config we should setup callkeep events, firebase handler asap to handle incoming calls from a dead state - setupFirebaseHandlerAndroid(pushConfig); - // setup ios handler for non-voip push notifications asap - setupRemoteNotificationsHandleriOS(pushConfig); } static getConfig() { diff --git a/packages/react-native-sdk/src/utils/StreamVideoRN/types.ts b/packages/react-native-sdk/src/utils/StreamVideoRN/types.ts index 6a45788974..2bc890a223 100644 --- a/packages/react-native-sdk/src/utils/StreamVideoRN/types.ts +++ b/packages/react-native-sdk/src/utils/StreamVideoRN/types.ts @@ -107,10 +107,12 @@ export type StreamVideoConfig = { * } */ createStreamVideoClient: () => Promise; - /** The callback that is called when a call is accepted, used for navigation */ - navigateAcceptCall: () => void; - /** The callback that is called when a push notification is tapped but user did not press accept or decline, used for navigation */ - navigateToIncomingCall: () => void; + /** @deprecated This method will be removed in the future. Please watch for incoming and outgoing calls in the root component of your app. + Please see https://getstream.io/video/docs/react-native/advanced/ringing-calls/#watch-for-incoming-and-outgoing-calls for more information */ + navigateAcceptCall?: () => void; + /** @deprecated This method will be removed in the future. Please watch for incoming and outgoing calls in the root component of your app. + Please see https://getstream.io/video/docs/react-native/advanced/ringing-calls/#watch-for-incoming-and-outgoing-calls for more information */ + navigateToIncomingCall?: () => void; /** Callback that is called when a non ringing push notification was tapped */ onTapNonRingingCallNotification?: ( call_cid: string, diff --git a/packages/react-native-sdk/src/utils/index.ts b/packages/react-native-sdk/src/utils/index.ts index d39fb87e1d..9f15b7f81c 100644 --- a/packages/react-native-sdk/src/utils/index.ts +++ b/packages/react-native-sdk/src/utils/index.ts @@ -38,5 +38,6 @@ export const getInitialsOfName = (name: string) => { return initials; }; +export * from './push/index'; export * from './enterPiPAndroid'; export * from './StreamVideoRN'; diff --git a/packages/react-native-sdk/src/utils/push/android.ts b/packages/react-native-sdk/src/utils/push/android.ts index 5a8da03960..b7b05663a0 100644 --- a/packages/react-native-sdk/src/utils/push/android.ts +++ b/packages/react-native-sdk/src/utils/push/android.ts @@ -1,4 +1,3 @@ -import { FirebaseMessagingTypes } from '@react-native-firebase/messaging'; import { Call, RxUtils, @@ -12,11 +11,10 @@ import type { } from '../StreamVideoRN/types'; import { getFirebaseMessagingLib, - getFirebaseMessagingLibNoThrow, getExpoNotificationsLib, - getExpoTaskManagerLib, getNotifeeLibThrowIfNotInstalledForPush, NotifeeLib, + FirebaseMessagingTypes, } from './libs'; import { pushAcceptedIncomingCallCId$, @@ -25,15 +23,16 @@ import { pushNonRingingCallData$, pushUnsubscriptionCallbacks$, pushAndroidBackgroundDeliveredIncomingCallCId$, -} from './rxSubjects'; +} from './internal/rxSubjects'; import { canAddPushWSSubscriptionsRef, clearPushWSEventSubscriptions, processCallFromPushInBackground, shouldCallBeEnded, -} from './utils'; +} from './internal/utils'; import { setPushLogoutCallback } from '../internal/pushLogoutCallback'; import { getAndroidDefaultRingtoneUrl } from '../getAndroidDefaultRingtoneUrl'; +import { StreamVideoRN } from '../StreamVideoRN'; const ACCEPT_CALL_ACTION_ID = 'accept'; const DECLINE_CALL_ACTION_ID = 'decline'; @@ -48,89 +47,6 @@ type Event = Parameters[0]; let lastFirebaseToken = { token: '', userId: '' }; -// EventType = NotifeeLib['EventType']; - -/** Setup Firebase push message handler **/ -export function setupFirebaseHandlerAndroid(pushConfig: PushConfig) { - if (Platform.OS !== 'android') { - return; - } - if (pushConfig.isExpo) { - const messaging = getFirebaseMessagingLibNoThrow(true); - if (messaging) { - // handles on app killed state in expo, expo-notifications cannot handle that - messaging().setBackgroundMessageHandler( - async (msg) => - await firebaseMessagingOnMessageHandler(msg.data, pushConfig) - ); - messaging().onMessage((msg) => - firebaseMessagingOnMessageHandler(msg.data, pushConfig) - ); // this is to listen to foreground messages, which we dont need for now - } else { - const Notifications = getExpoNotificationsLib(); - const TaskManager = getExpoTaskManagerLib(); - const BACKGROUND_NOTIFICATION_TASK = - 'STREAM-VIDEO-SDK-INTERNAL-BACKGROUND-NOTIFICATION-TASK'; - - TaskManager.defineTask( - BACKGROUND_NOTIFICATION_TASK, - ({ data, error }) => { - if (error) { - return; - } - // @ts-ignore - const dataToProcess = data.notification?.data; - firebaseMessagingOnMessageHandler(dataToProcess, pushConfig); - } - ); - // background handler (does not handle on app killed state) - Notifications.registerTaskAsync(BACKGROUND_NOTIFICATION_TASK); - // foreground handler - Notifications.setNotificationHandler({ - handleNotification: async (notification) => { - // @ts-ignore - const trigger = notification?.request?.trigger; - if (trigger.type === 'push') { - const data = trigger?.remoteMessage?.data; - if (data?.sender === 'stream.video') { - await firebaseMessagingOnMessageHandler(data, pushConfig); - return { - shouldShowAlert: false, - shouldPlaySound: false, - shouldSetBadge: false, - }; - } - } - return { - shouldShowAlert: true, - shouldPlaySound: false, - shouldSetBadge: false, - }; - }, - }); - } - } else { - const messaging = getFirebaseMessagingLib(); - messaging().setBackgroundMessageHandler( - async (msg) => - await firebaseMessagingOnMessageHandler(msg.data, pushConfig) - ); - messaging().onMessage((msg) => - firebaseMessagingOnMessageHandler(msg.data, pushConfig) - ); // this is to listen to foreground messages, which we dont need for now - } - - // the notification tap handlers are always registered with notifee for both expo and non-expo in android - const notifeeLib = getNotifeeLibThrowIfNotInstalledForPush(); - const notifee = notifeeLib.default; - notifee.onBackgroundEvent(async (event) => { - await onNotifeeEvent(event, pushConfig, true); - }); - notifee.onForegroundEvent((event) => { - onNotifeeEvent(event, pushConfig, false); - }); -} - /** Send token to stream, create notification channel, */ export async function initAndroidPushToken( client: StreamVideoClient, @@ -184,10 +100,14 @@ export async function initAndroidPushToken( } } -const firebaseMessagingOnMessageHandler = async ( - data: FirebaseMessagingTypes.RemoteMessage['data'], - pushConfig: PushConfig +/** + * Creates notification from the push message data. + * For Ringing and Non-Ringing calls. + */ +export const firebaseDataHandler = async ( + data: FirebaseMessagingTypes.RemoteMessage['data'] ) => { + if (Platform.OS !== 'android') return; /* Example data from firebase "message": { "data": { @@ -203,7 +123,8 @@ const firebaseMessagingOnMessageHandler = async ( // other stuff } */ - if (!data || data.sender !== 'stream.video') { + const pushConfig = StreamVideoRN.getConfig().push; + if (!pushConfig || !data || data.sender !== 'stream.video') { return; } @@ -377,16 +298,25 @@ const firebaseMessagingOnMessageHandler = async ( } }; -const onNotifeeEvent = async ( - event: Event, - pushConfig: PushConfig, - isBackground: boolean -) => { +export const onAndroidNotifeeEvent = async ({ + event, + isBackground, +}: { + event: Event; + isBackground: boolean; +}) => { + if (Platform.OS !== 'android') return; const { type, detail } = event; const { notification, pressAction } = detail; const notificationId = notification?.id; const data = notification?.data; - if (!data || !notificationId || data.sender !== 'stream.video') { + const pushConfig = StreamVideoRN.getConfig().push; + if ( + !pushConfig || + !data || + !notificationId || + data.sender !== 'stream.video' + ) { return; } @@ -443,7 +373,6 @@ const onNotifeeEvent = async ( } else { const notifeeLib = getNotifeeLibThrowIfNotInstalledForPush(); if (type === notifeeLib.EventType.PRESS) { - pushTappedIncomingCallCId$.next(call_cid); pushConfig.onTapNonRingingCallNotification?.( call_cid, data.type as NonRingingPushEvent diff --git a/packages/react-native-sdk/src/utils/push/index.ts b/packages/react-native-sdk/src/utils/push/index.ts new file mode 100644 index 0000000000..f73002b97f --- /dev/null +++ b/packages/react-native-sdk/src/utils/push/index.ts @@ -0,0 +1,3 @@ +export * from './android'; +export * from './ios'; +export * from './utils'; diff --git a/packages/react-native-sdk/src/utils/push/rxSubjects.ts b/packages/react-native-sdk/src/utils/push/internal/rxSubjects.ts similarity index 97% rename from packages/react-native-sdk/src/utils/push/rxSubjects.ts rename to packages/react-native-sdk/src/utils/push/internal/rxSubjects.ts index a1186c291f..cbbe61da15 100644 --- a/packages/react-native-sdk/src/utils/push/rxSubjects.ts +++ b/packages/react-native-sdk/src/utils/push/internal/rxSubjects.ts @@ -1,5 +1,5 @@ import { BehaviorSubject } from 'rxjs'; -import { NonRingingPushEvent } from '../StreamVideoRN/types'; +import { NonRingingPushEvent } from '../../StreamVideoRN/types'; /** * This rxjs subject is used to store the call cid of the accepted incoming call from push notification diff --git a/packages/react-native-sdk/src/utils/push/internal/utils.ts b/packages/react-native-sdk/src/utils/push/internal/utils.ts new file mode 100644 index 0000000000..39b782c105 --- /dev/null +++ b/packages/react-native-sdk/src/utils/push/internal/utils.ts @@ -0,0 +1,178 @@ +import { + Call, + RxUtils, + StreamVideoClient, + getLogger, +} from '@stream-io/video-client'; +import type { + NonRingingPushEvent, + StreamVideoConfig, +} from '../../StreamVideoRN/types'; +import { onNewCallNotification } from '../../internal/newNotificationCallbacks'; +import { pushUnsubscriptionCallbacks$ } from './rxSubjects'; + +type PushConfig = NonNullable; + +type CanAddPushWSSubscriptionsRef = { current: boolean }; + +/** + * This function is used to check if the call should be ended based on the push notification + * Useful for callkeep management to end the call if necessary (with reportEndCallWithUUID) + */ +export const shouldCallBeEnded = ( + callFromPush: Call, + created_by_id: string | undefined, + receiver_id: string | undefined +) => { + /* callkeep reasons for ending a call + FAILED: 1, + REMOTE_ENDED: 2, + UNANSWERED: 3, + ANSWERED_ELSEWHERE: 4, + DECLINED_ELSEWHERE: 5, + MISSED: 6 + */ + const callSession = callFromPush.state.session; + const rejected_by = callSession?.rejected_by; + const accepted_by = callSession?.accepted_by; + let mustEndCall = false; + let callkeepReason = 0; + if (created_by_id && rejected_by) { + if (rejected_by[created_by_id]) { + // call was cancelled by the caller + mustEndCall = true; + callkeepReason = 2; + } + } else if (receiver_id && rejected_by) { + if (rejected_by[receiver_id]) { + // call was rejected by the receiver in some other device + mustEndCall = true; + callkeepReason = 5; + } + } else if (receiver_id && accepted_by) { + if (accepted_by[receiver_id]) { + // call was accepted by the receiver in some other device + mustEndCall = true; + callkeepReason = 4; + } + } + return { mustEndCall, callkeepReason }; +}; + +/* An action for the notification or callkeep and app does not have JS context setup yet, so we need to do two steps: + 1. we need to create a new client and connect the user to decline the call + 2. this is because the app is in background state and we don't have a client to get the call and do an action +*/ +export const processCallFromPushInBackground = async ( + pushConfig: PushConfig, + call_cid: string, + action: Parameters[2] +) => { + let videoClient: StreamVideoClient | undefined; + + try { + videoClient = await pushConfig.createStreamVideoClient(); + if (!videoClient) { + return; + } + } catch (e) { + const logger = getLogger(['processCallFromPushInBackground']); + logger('error', 'failed to create video client', e); + return; + } + await processCallFromPush(videoClient, call_cid, action, pushConfig); +}; + +/** + * This function is used process the call from push notifications due to incoming call + * It does the following steps: + * 1. Get the call from the client if present or create a new call + * 2. Fetch the latest state of the call from the server if its not already in ringing state + * 3. Join or leave the call based on the user's action. + */ +export const processCallFromPush = async ( + client: StreamVideoClient, + call_cid: string, + action: 'accept' | 'decline' | 'pressed' | 'backgroundDelivered', + pushConfig: PushConfig +) => { + let callFromPush: Call; + try { + callFromPush = await client.onRingingCall(call_cid); + } catch (e) { + const logger = getLogger(['processCallFromPush']); + logger('error', 'failed to fetch call from push notification', e); + return; + } + // note: when action was pressed or delivered, we dont need to do anything as the only thing is to do is to get the call which adds it to the client + try { + if (action === 'accept') { + if (pushConfig.publishOptions) { + callFromPush.updatePublishOptions(pushConfig.publishOptions); + } + await callFromPush.join(); + } else if (action === 'decline') { + await callFromPush.leave({ reject: true }); + } + } catch (e) { + const logger = getLogger(['processCallFromPush']); + logger( + 'error', + `failed to process ${action} call from push notification`, + e + ); + } +}; + +/** + * This function is used process the call from push notifications due to non ringing calls + * It does the following steps: + * 1. Get the call from the client if present or create a new call + * 2. Fetch the latest state of the call from the server + * 3. Call all the callbacks to inform the app about the call + */ +export const processNonIncomingCallFromPush = async ( + client: StreamVideoClient, + call_cid: string, + nonRingingNotificationType: NonRingingPushEvent +) => { + let callFromPush: Call; + try { + const _callFromPush = client.state.calls.find((c) => c.cid === call_cid); + if (_callFromPush) { + callFromPush = _callFromPush; + } else { + // if not it means that WS is not alive when receiving the push notifications and we need to fetch the call + const [callType, callId] = call_cid.split(':'); + callFromPush = client.call(callType as string, callId as string); + await callFromPush.get(); + } + } catch (e) { + const logger = getLogger(['processNonIncomingCallFromPush']); + logger('error', 'failed to fetch call from push notification', e); + return; + } + onNewCallNotification(callFromPush, nonRingingNotificationType); +}; + +/** + * This function is used to clear all the push related WS subscriptions + * note: events are subscribed in push for accept/decline through WS + */ +export const clearPushWSEventSubscriptions = () => { + const unsubscriptionCallbacks = RxUtils.getCurrentValue( + pushUnsubscriptionCallbacks$ + ); + if (unsubscriptionCallbacks) { + unsubscriptionCallbacks.forEach((cb) => cb()); + } + pushUnsubscriptionCallbacks$.next(undefined); +}; + +/** + * This ref is used to check if the push WS subscriptions can be added + * It is used to avoid adding the push WS subscriptions when the client is connected to WS in the foreground + */ +export const canAddPushWSSubscriptionsRef: CanAddPushWSSubscriptionsRef = { + current: true, +}; diff --git a/packages/react-native-sdk/src/utils/push/ios.ts b/packages/react-native-sdk/src/utils/push/ios.ts index 4f4af70e77..1d30609355 100644 --- a/packages/react-native-sdk/src/utils/push/ios.ts +++ b/packages/react-native-sdk/src/utils/push/ios.ts @@ -1,41 +1,24 @@ import { Platform } from 'react-native'; -import type { - NonRingingPushEvent, - StreamVideoConfig, -} from '../StreamVideoRN/types'; -import { - pushAcceptedIncomingCallCId$, - voipPushNotificationCallCId$, - voipCallkeepCallOnForegroundMap$, - voipCallkeepAcceptedCallOnNativeDialerMap$, - pushNonRingingCallData$, -} from './rxSubjects'; -import { - clearPushWSEventSubscriptions, - processCallFromPushInBackground, -} from './utils'; +import type { StreamVideoConfig } from '../StreamVideoRN/types'; +import { pushNonRingingCallData$ } from './internal/rxSubjects'; import { + ExpoNotification, getExpoNotificationsLib, - getNotifeeLibThrowIfNotInstalledForPush, getPushNotificationIosLib, + PushNotificationiOSType, } from './libs'; import { StreamVideoClient, getLogger } from '@stream-io/video-client'; import { setPushLogoutCallback } from '../internal/pushLogoutCallback'; +import { EventType, Event } from '@notifee/react-native'; +import { StreamVideoRN } from '../StreamVideoRN'; +import { StreamPushPayload } from './utils'; type PushConfig = NonNullable; -type StreamPayload = - | { - call_cid: string; - type: 'call.ring' | NonRingingPushEvent; - sender: string; - } - | undefined; - let lastApnToken = { token: '', userId: '' }; function processNonRingingNotificationStreamPayload( - streamPayload: StreamPayload + streamPayload: StreamPushPayload ) { if ( streamPayload?.sender === 'stream.video' && @@ -48,87 +31,56 @@ function processNonRingingNotificationStreamPayload( } } -export const iosCallkeepAcceptCall = ( - call_cid: string | undefined, - callUUIDFromCallkeep: string -) => { - if (!shouldProcessCallFromCallkeep(call_cid, callUUIDFromCallkeep)) { - return; - } - clearPushWSEventSubscriptions(); - // to call end callkeep later if ended in app and not through callkeep - voipCallkeepAcceptedCallOnNativeDialerMap$.next({ - uuid: callUUIDFromCallkeep, - cid: call_cid, - }); - // to process the call in the app - pushAcceptedIncomingCallCId$.next(call_cid); - // no need to keep these references anymore - voipCallkeepCallOnForegroundMap$.next(undefined); -}; - -export const iosCallkeepRejectCall = async ( - call_cid: string | undefined, - callUUIDFromCallkeep: string, - pushConfig: PushConfig -) => { - if (!shouldProcessCallFromCallkeep(call_cid, callUUIDFromCallkeep)) { - return; +export const oniOSExpoNotificationEvent = (event: ExpoNotification) => { + const pushConfig = StreamVideoRN.getConfig().push; + if (pushConfig) { + if (event.request.trigger.type === 'push') { + const streamPayload = event.request.trigger.payload + ?.stream as StreamPushPayload; + processNonRingingNotificationStreamPayload(streamPayload); + } } - clearPushWSEventSubscriptions(); - // no need to keep these references anymore - voipCallkeepAcceptedCallOnNativeDialerMap$.next(undefined); - voipCallkeepCallOnForegroundMap$.next(undefined); - voipPushNotificationCallCId$.next(undefined); - await processCallFromPushInBackground(pushConfig, call_cid, 'decline'); }; -/** - * Helper function to determine if the answer/end call event from callkeep must be processed - * Just checks if we have a valid call_cid and acts as a type guard for call_cid - */ -const shouldProcessCallFromCallkeep = ( - call_cid: string | undefined, - callUUIDFromCallkeep: string -): call_cid is string => { - if (!call_cid || !callUUIDFromCallkeep) { - return false; +export const oniOSNotifeeEvent = ({ + event, +}: { + event: Event; + isBackground: boolean; +}) => { + if (Platform.OS !== 'ios') return; + const pushConfig = StreamVideoRN.getConfig().push; + const { type, detail } = event; + if (pushConfig && type === EventType.PRESS) { + const streamPayload = detail.notification?.data?.stream as + | StreamPushPayload + | undefined; + const result = processNonRingingNotificationStreamPayload(streamPayload); + if (result) { + pushConfig.onTapNonRingingCallNotification?.(result.cid, result.type); + } } - return true; }; -export const setupRemoteNotificationsHandleriOS = (pushConfig: PushConfig) => { - if (Platform.OS !== 'ios') { +export function onPushNotificationiOSStreamVideoEvent( + notification: PushNotificationiOSType +) { + const pushNotificationIosLib = getPushNotificationIosLib(); + const data = notification.getData(); + const streamPayload = data?.stream as StreamPushPayload; + const isClicked = data.userInteraction === 1; + const pushConfig = StreamVideoRN.getConfig().push; + if (!streamPayload || !isClicked || !pushConfig) { + notification.finish(pushNotificationIosLib.FetchResult.NoData); return; } - const notifeeLib = getNotifeeLibThrowIfNotInstalledForPush(); - - notifeeLib.default.onForegroundEvent(({ type, detail }) => { - if (type === notifeeLib.EventType.PRESS) { - const streamPayload = detail.notification?.data?.stream as - | StreamPayload - | undefined; - const result = processNonRingingNotificationStreamPayload(streamPayload); - if (result) { - pushConfig.onTapNonRingingCallNotification?.(result.cid, result.type); - } - } - }); - if (pushConfig.isExpo) { - const Notifications = getExpoNotificationsLib(); - - // foreground handler (just to show the notifications on foreground) - Notifications.setNotificationHandler({ - handleNotification: async () => { - return { - shouldShowAlert: true, - shouldPlaySound: true, - shouldSetBadge: false, - }; - }, - }); + // listen to foreground notifications + const result = processNonRingingNotificationStreamPayload(streamPayload); + if (result) { + pushConfig.onTapNonRingingCallNotification?.(result.cid, result.type); } -}; + notification.finish(pushNotificationIosLib.FetchResult.NoData); +} /** Send token to stream */ export async function initIosNonVoipToken( @@ -171,34 +123,16 @@ export async function initIosNonVoipToken( setDeviceToken(devicePushToken.data); } ); - const subscriptionForReceive = - expoNotificationsLib.addNotificationReceivedListener((event) => { - // listen to foreground notifications - if (event.request.trigger.type === 'push') { - const streamPayload = event.request.trigger.payload - ?.stream as StreamPayload; - processNonRingingNotificationStreamPayload(streamPayload); - } - }); setUnsubscribeListener(() => { subscription.remove(); - subscriptionForReceive.remove(); }); } else { const pushNotificationIosLib = getPushNotificationIosLib(); pushNotificationIosLib.addEventListener('register', (token) => { setDeviceToken(token); }); - pushNotificationIosLib.addEventListener('notification', (notification) => { - const data = notification.getData(); - const streamPayload = data?.stream as StreamPayload; - // listen to foreground notifications - processNonRingingNotificationStreamPayload(streamPayload); - notification.finish(pushNotificationIosLib.FetchResult.NoData); - }); setUnsubscribeListener(() => { pushNotificationIosLib.removeEventListener('register'); - pushNotificationIosLib.removeEventListener('notification'); }); } } diff --git a/packages/react-native-sdk/src/utils/push/libs/expoNotifications.ts b/packages/react-native-sdk/src/utils/push/libs/expoNotifications.ts index d62caad67f..72d2a14aa1 100644 --- a/packages/react-native-sdk/src/utils/push/libs/expoNotifications.ts +++ b/packages/react-native-sdk/src/utils/push/libs/expoNotifications.ts @@ -1,5 +1,9 @@ export type ExpoNotificationsLib = typeof import('expo-notifications'); +import type { Notification } from 'expo-notifications'; + +export type ExpoNotification = Notification; + let expoNotificationsLib: ExpoNotificationsLib | undefined; try { diff --git a/packages/react-native-sdk/src/utils/push/libs/expoTaskManager.ts b/packages/react-native-sdk/src/utils/push/libs/expoTaskManager.ts deleted file mode 100644 index 137a0aaf62..0000000000 --- a/packages/react-native-sdk/src/utils/push/libs/expoTaskManager.ts +++ /dev/null @@ -1,16 +0,0 @@ -export type ExpoTaskManagerLib = typeof import('expo-task-manager'); - -let expoTaskManagerLib: ExpoTaskManagerLib | undefined; - -try { - expoTaskManagerLib = require('expo-task-manager'); -} catch (_e) {} - -export function getExpoTaskManagerLib(): ExpoTaskManagerLib { - if (!expoTaskManagerLib) { - throw Error( - 'expo-task-manager library is not installed. Please see https://docs.expo.dev/versions/latest/sdk/task-manager/ for installation instructions' - ); - } - return expoTaskManagerLib; -} diff --git a/packages/react-native-sdk/src/utils/push/libs/index.ts b/packages/react-native-sdk/src/utils/push/libs/index.ts index 74ff5fa2a6..988e511fe9 100644 --- a/packages/react-native-sdk/src/utils/push/libs/index.ts +++ b/packages/react-native-sdk/src/utils/push/libs/index.ts @@ -1,5 +1,4 @@ export * from './expoNotifications'; -export * from './expoTaskManager'; export * from './firebaseMessaging'; export * from './iosPushNotification'; export * from './voipPushNotification'; diff --git a/packages/react-native-sdk/src/utils/push/libs/iosPushNotification.ts b/packages/react-native-sdk/src/utils/push/libs/iosPushNotification.ts index ff8bc44c87..17c3199e53 100644 --- a/packages/react-native-sdk/src/utils/push/libs/iosPushNotification.ts +++ b/packages/react-native-sdk/src/utils/push/libs/iosPushNotification.ts @@ -3,6 +3,10 @@ export type PushNotificationIosLib = let pushNotificationIosLib: PushNotificationIosLib | undefined; +import type { PushNotification } from '@react-native-community/push-notification-ios'; + +export type PushNotificationiOSType = PushNotification; + try { pushNotificationIosLib = require('@react-native-community/push-notification-ios').default; diff --git a/packages/react-native-sdk/src/utils/push/utils.ts b/packages/react-native-sdk/src/utils/push/utils.ts index 1d6448847a..ded014fb2b 100644 --- a/packages/react-native-sdk/src/utils/push/utils.ts +++ b/packages/react-native-sdk/src/utils/push/utils.ts @@ -1,178 +1,47 @@ -import { - Call, - getLogger, - RxUtils, - StreamVideoClient, -} from '@stream-io/video-client'; -import type { - NonRingingPushEvent, - StreamVideoConfig, -} from '../StreamVideoRN/types'; -import { onNewCallNotification } from '../internal/newNotificationCallbacks'; -import { pushUnsubscriptionCallbacks$ } from './rxSubjects'; - -type PushConfig = NonNullable; - -type CanAddPushWSSubscriptionsRef = { current: boolean }; - -/** - * This function is used to check if the call should be ended based on the push notification - * Useful for callkeep management to end the call if necessary (with reportEndCallWithUUID) - */ -export const shouldCallBeEnded = ( - callFromPush: Call, - created_by_id: string | undefined, - receiver_id: string | undefined -) => { - /* callkeep reasons for ending a call - FAILED: 1, - REMOTE_ENDED: 2, - UNANSWERED: 3, - ANSWERED_ELSEWHERE: 4, - DECLINED_ELSEWHERE: 5, - MISSED: 6 - */ - const callSession = callFromPush.state.session; - const rejected_by = callSession?.rejected_by; - const accepted_by = callSession?.accepted_by; - let mustEndCall = false; - let callkeepReason = 0; - if (created_by_id && rejected_by) { - if (rejected_by[created_by_id]) { - // call was cancelled by the caller - mustEndCall = true; - callkeepReason = 2; - } - } else if (receiver_id && rejected_by) { - if (rejected_by[receiver_id]) { - // call was rejected by the receiver in some other device - mustEndCall = true; - callkeepReason = 5; - } - } else if (receiver_id && accepted_by) { - if (accepted_by[receiver_id]) { - // call was accepted by the receiver in some other device - mustEndCall = true; - callkeepReason = 4; +import { Event } from '@notifee/react-native'; +import { FirebaseMessagingTypes } from './libs/firebaseMessaging'; +import { ExpoNotification } from './libs/expoNotifications'; +import { NonRingingPushEvent } from '../StreamVideoRN/types'; +import { PushNotificationiOSType } from './libs/iosPushNotification'; + +export type StreamPushPayload = + | { + call_cid: string; + type: 'call.ring' | NonRingingPushEvent; + sender: string; } - } - return { mustEndCall, callkeepReason }; -}; - -/* An action for the notification or callkeep and app does not have JS context setup yet, so we need to do two steps: - 1. we need to create a new client and connect the user to decline the call - 2. this is because the app is in background state and we don't have a client to get the call and do an action -*/ -export const processCallFromPushInBackground = async ( - pushConfig: PushConfig, - call_cid: string, - action: Parameters[2] -) => { - let videoClient: StreamVideoClient | undefined; - - try { - videoClient = await pushConfig.createStreamVideoClient(); - if (!videoClient) { - return; - } - } catch (e) { - const logger = getLogger(['processCallFromPushInBackground']); - logger('error', 'failed to create video client', e); - return; - } - await processCallFromPush(videoClient, call_cid, action, pushConfig); -}; - -/** - * This function is used process the call from push notifications due to incoming call - * It does the following steps: - * 1. Get the call from the client if present or create a new call - * 2. Fetch the latest state of the call from the server if its not already in ringing state - * 3. Join or leave the call based on the user's action. - */ -export const processCallFromPush = async ( - client: StreamVideoClient, - call_cid: string, - action: 'accept' | 'decline' | 'pressed' | 'backgroundDelivered', - pushConfig: PushConfig -) => { - let callFromPush: Call; - try { - callFromPush = await client.onRingingCall(call_cid); - } catch (e) { - const logger = getLogger(['processCallFromPush']); - logger('error', 'failed to fetch call from push notification', e); - return; - } - // note: when action was pressed or delivered, we dont need to do anything as the only thing is to do is to get the call which adds it to the client - try { - if (action === 'accept') { - if (pushConfig.publishOptions) { - callFromPush.updatePublishOptions(pushConfig.publishOptions); - } - await callFromPush.join(); - } else if (action === 'decline') { - await callFromPush.leave({ reject: true }); - } - } catch (e) { - const logger = getLogger(['processCallFromPush']); - logger( - 'error', - `failed to process ${action} call from push notification`, - e + | undefined; + +export function isFirebaseStreamVideoMessage( + message: FirebaseMessagingTypes.RemoteMessage +) { + return message.data?.sender === 'stream.video'; +} + +export function isNotifeeStreamVideoEvent(event: Event) { + const { detail } = event; + const { notification } = detail; + return notification?.data?.sender === 'stream.video'; +} + +export function isExpoNotificationStreamVideoEvent(event: ExpoNotification) { + if (event.request.trigger.type === 'push') { + // iOS + const streamPayload = event.request.trigger.payload + ?.stream as StreamPushPayload; + // Android + const remoteMessageData = event.request.trigger.remoteMessage?.data; + return ( + streamPayload?.sender === 'stream.video' || + remoteMessageData?.sender === 'stream.video' ); } -}; - -/** - * This function is used process the call from push notifications due to non ringing calls - * It does the following steps: - * 1. Get the call from the client if present or create a new call - * 2. Fetch the latest state of the call from the server if its not already in ringing state - * 3. Call all the callbacks to inform the app about the call - */ -export const processNonIncomingCallFromPush = async ( - client: StreamVideoClient, - call_cid: string, - nonRingingNotificationType: NonRingingPushEvent -) => { - let callFromPush: Call; - try { - const _callFromPush = client.state.calls.find((c) => c.cid === call_cid); - if (_callFromPush) { - callFromPush = _callFromPush; - } else { - // if not it means that WS is not alive when receiving the push notifications and we need to fetch the call - const [callType, callId] = call_cid.split(':'); - callFromPush = client.call(callType as string, callId as string); - await callFromPush.get(); - } - } catch (e) { - const logger = getLogger(['processNonIncomingCallFromPush']); - logger('error', 'failed to fetch call from push notification', e); - return; - } - onNewCallNotification(callFromPush, nonRingingNotificationType); -}; - -/** - * This function is used to clear all the push related WS subscriptions - * note: events are subscribed in push for accept/decline through WS - */ -export const clearPushWSEventSubscriptions = () => { - const unsubscriptionCallbacks = RxUtils.getCurrentValue( - pushUnsubscriptionCallbacks$ - ); - if (unsubscriptionCallbacks) { - unsubscriptionCallbacks.forEach((cb) => cb()); - } - pushUnsubscriptionCallbacks$.next(undefined); -}; - -/** - * This ref is used to check if the push WS subscriptions can be added - * It is used to avoid adding the push WS subscriptions when the client is connected to WS in the foreground - */ -export const canAddPushWSSubscriptionsRef: CanAddPushWSSubscriptionsRef = { - current: true, -}; +} + +export function isPushNotificationiOSStreamVideoEvent( + notification: PushNotificationiOSType +) { + const data = notification.getData(); + const streamPayload = data?.stream as StreamPushPayload; + return streamPayload?.sender === 'stream.video'; +} diff --git a/sample-apps/react-native/dogfood/App.tsx b/sample-apps/react-native/dogfood/App.tsx index 885b5409e0..6d5a3c295a 100755 --- a/sample-apps/react-native/dogfood/App.tsx +++ b/sample-apps/react-native/dogfood/App.tsx @@ -32,6 +32,11 @@ import { GestureHandlerRootView } from 'react-native-gesture-handler'; import { LogBox, StyleSheet } from 'react-native'; import { LiveStream } from './src/navigators/Livestream'; import { REACT_NATIVE_DOGFOOD_APP_ENVIRONMENT } from '@env'; +import PushNotificationIOS from '@react-native-community/push-notification-ios'; +import { + isPushNotificationiOSStreamVideoEvent, + onPushNotificationiOSStreamVideoEvent, +} from '@stream-io/video-react-native-sdk'; // only enable warning and error logs from webrtc library Logger.enable(`${Logger.ROOT_PREFIX}:(WARN|ERROR)`); @@ -57,6 +62,17 @@ const StackNavigator = () => { useProntoLinkEffect(); useSyncPermissions(); + useEffect(() => { + PushNotificationIOS.addEventListener('notification', (notification) => { + if (isPushNotificationiOSStreamVideoEvent(notification)) { + onPushNotificationiOSStreamVideoEvent(notification); + } + }); + return () => { + PushNotificationIOS.removeEventListener('notification'); + }; + }, []); + let mode; switch (appMode) { case 'Meeting': diff --git a/sample-apps/react-native/dogfood/ios/Podfile.lock b/sample-apps/react-native/dogfood/ios/Podfile.lock index 0d405e0962..2e15589063 100644 --- a/sample-apps/react-native/dogfood/ios/Podfile.lock +++ b/sample-apps/react-native/dogfood/ios/Podfile.lock @@ -32,9 +32,6 @@ PODS: - MMKV (1.3.9): - MMKVCore (~> 1.3.9) - MMKVCore (1.3.9) - - PromisesObjC (2.4.0) - - PromisesSwift (2.4.0): - - PromisesObjC (= 2.4.0) - RCT-Folly (2022.05.16.00): - boost - DoubleConversion @@ -917,11 +914,14 @@ PODS: - React-Core - react-native-safe-area-context (4.10.5): - React-Core - - react-native-video (6.0.0-beta.5): + - react-native-video (6.7.0): + - glog + - RCT-Folly (= 2022.05.16.00) - React-Core - - react-native-video/Video (= 6.0.0-beta.5) - - react-native-video/Video (6.0.0-beta.5): - - PromisesSwift + - react-native-video/Video (= 6.7.0) + - react-native-video/Video (6.7.0): + - glog + - RCT-Folly (= 2022.05.16.00) - React-Core - React-nativeconfig (0.73.4) - React-NativeModulesApple (0.73.4): @@ -1148,7 +1148,7 @@ PODS: - stream-react-native-webrtc (125.0.0-rc.1): - React-Core - WebRTC-SDK (~> 125.6422.05) - - stream-video-react-native (1.2.12): + - stream-video-react-native (1.2.14): - glog - RCT-Folly (= 2022.05.16.00) - React-Core @@ -1244,8 +1244,6 @@ SPEC REPOS: - libevent - MMKV - MMKVCore - - PromisesObjC - - PromisesSwift - SocketRocket - TOCropViewController - WebRTC-SDK @@ -1412,8 +1410,6 @@ SPEC CHECKSUMS: libevent: 4049cae6c81cdb3654a443be001fb9bdceff7913 MMKV: 817ba1eea17421547e01e087285606eb270a8dcb MMKVCore: af055b00e27d88cd92fad301c5fecd1ff9b26dd9 - PromisesObjC: f5707f49cb48b9636751c5b2e7d227e43fba9f47 - PromisesSwift: 9d77319bbe72ebf6d872900551f7eeba9bce2851 RCT-Folly: 7169b2b1c44399c76a47b5deaaba715eeeb476c0 RCTRequired: ab7f915c15569f04a49669e573e6e319a53f9faa RCTTypeSafety: 63b97ced7b766865057e7154db0e81ce4ee6cf1e @@ -1440,7 +1436,7 @@ SPEC CHECKSUMS: react-native-mmkv: 7da5e18e55c04a9af9a7e0ab9792a1e8d33765a1 react-native-netinfo: 299dad906cdbf3b67bcc6f693c807f98bdd127cc react-native-safe-area-context: a240ad4b683349e48b1d51fed1611138d1bdad97 - react-native-video: df7d8a4c8568ed4a31b28e6cd2bfa4a98b186e36 + react-native-video: d3f78ea2b7ed6bf8f0b8d16a9d3bbe19f73bcbca React-nativeconfig: ca8b90c736cf3be019cb332ca42d93dd95b32e05 React-NativeModulesApple: 1fdffcce7772e274baeab33a1900f45feba86cbd React-perflogger: 8a1e1af5733004bdd91258dcefbde21e0d1faccd @@ -1479,7 +1475,7 @@ SPEC CHECKSUMS: SocketRocket: f32cd54efbe0f095c4d7594881e52619cfe80b17 stream-io-video-filters-react-native: 8c345e6adf5164646696d45f9962c4199b2fe3b9 stream-react-native-webrtc: 976646e6e3643d331fc4da20e4be04ea16af295b - stream-video-react-native: f6aa195dbaf9f72945ad2d16d709dc1e40ceee2b + stream-video-react-native: 99eb88b52009b0687c3e9d8600e924ba789490dd TOCropViewController: 80b8985ad794298fb69d3341de183f33d1853654 WebRTC-SDK: 1990a1a595bd0b59c17485ce13ff17f575732c12 Yoga: 1b901a6d6eeba4e8a2e8f308f708691cdb5db312 diff --git a/sample-apps/react-native/dogfood/package.json b/sample-apps/react-native/dogfood/package.json index 15fc21e98a..92d568e141 100644 --- a/sample-apps/react-native/dogfood/package.json +++ b/sample-apps/react-native/dogfood/package.json @@ -24,8 +24,8 @@ "@react-native-clipboard/clipboard": "^1.11.1", "@react-native-community/netinfo": "11.3.0", "@react-native-community/push-notification-ios": "^1.11.0", - "@react-native-firebase/app": "18.8.0", - "@react-native-firebase/messaging": "18.8.0", + "@react-native-firebase/app": "19.2.2", + "@react-native-firebase/messaging": "19.2.2", "@react-native-google-signin/google-signin": "^11.0.0", "@react-navigation/native": "^6.1.10", "@react-navigation/native-stack": "^6.9.18", @@ -45,11 +45,11 @@ "react-native-incall-manager": "4.2.0", "react-native-mmkv": "2.8.0", "react-native-permissions": "^4.1.1", - "react-native-reanimated": "^3.7.0", - "react-native-safe-area-context": "^4.4.1", - "react-native-screens": "^3.29.0", + "react-native-reanimated": "~3.10.1", + "react-native-safe-area-context": "4.10.5", + "react-native-screens": "3.31.1", "react-native-svg": "^13.9.0", - "react-native-video": "^6.0.0-beta.5", + "react-native-video": "^6.7.0", "react-native-voip-push-notification": "~3.3.1", "rxjs": "~7.8.1", "stream-chat-react-native": "5.27.0" diff --git a/sample-apps/react-native/dogfood/src/components/ActiveCall.tsx b/sample-apps/react-native/dogfood/src/components/ActiveCall.tsx index 07ec207fbe..d739b28a83 100644 --- a/sample-apps/react-native/dogfood/src/components/ActiveCall.tsx +++ b/sample-apps/react-native/dogfood/src/components/ActiveCall.tsx @@ -4,6 +4,7 @@ import { CallContent, CallControlProps, } from '@stream-io/video-react-native-sdk'; + import { ActivityIndicator, StyleSheet } from 'react-native'; import { SafeAreaView } from 'react-native-safe-area-context'; import { ParticipantsInfoList } from './ParticipantsInfoList'; diff --git a/sample-apps/react-native/dogfood/src/navigators/Call.tsx b/sample-apps/react-native/dogfood/src/navigators/Call.tsx index 71792147f2..0365229e97 100644 --- a/sample-apps/react-native/dogfood/src/navigators/Call.tsx +++ b/sample-apps/react-native/dogfood/src/navigators/Call.tsx @@ -21,9 +21,7 @@ import { useOrientation } from '../hooks/useOrientation'; const CallStack = createNativeStackNavigator(); const Calls = () => { - const calls = useCalls().filter( - (c) => c.state.callingState === CallingState.RINGING, - ); + const calls = useCalls().filter((c) => c.ringing); const { top } = useSafeAreaInsets(); const orientation = useOrientation(); diff --git a/sample-apps/react-native/dogfood/src/utils/setFirebaseListeners.android.ts b/sample-apps/react-native/dogfood/src/utils/setFirebaseListeners.android.ts new file mode 100644 index 0000000000..4f539d709c --- /dev/null +++ b/sample-apps/react-native/dogfood/src/utils/setFirebaseListeners.android.ts @@ -0,0 +1,24 @@ +import messaging from '@react-native-firebase/messaging'; +import { + isFirebaseStreamVideoMessage, + firebaseDataHandler, +} from '@stream-io/video-react-native-sdk'; + +export const setFirebaseListeners = () => { + // Set up the background message handler for + // 1. incoming call notifications + // 2. non-ringing notifications + messaging().setBackgroundMessageHandler(async (msg) => { + if (isFirebaseStreamVideoMessage(msg)) { + await firebaseDataHandler(msg.data); + } + }); + // Set up the foreground message handler for + // 1. incoming call notifications + // 2. non-ringing notifications + messaging().onMessage((msg) => { + if (isFirebaseStreamVideoMessage(msg)) { + firebaseDataHandler(msg.data); + } + }); +}; diff --git a/sample-apps/react-native/dogfood/src/utils/setFirebaseListeners.ts b/sample-apps/react-native/dogfood/src/utils/setFirebaseListeners.ts new file mode 100644 index 0000000000..9a452a91ed --- /dev/null +++ b/sample-apps/react-native/dogfood/src/utils/setFirebaseListeners.ts @@ -0,0 +1,3 @@ +export const setFirebaseListeners = () => { + // do nothing on iOS +}; diff --git a/sample-apps/react-native/dogfood/src/utils/setPushConfig.ts b/sample-apps/react-native/dogfood/src/utils/setPushConfig.ts index ff6d1404fa..df628c0b5e 100644 --- a/sample-apps/react-native/dogfood/src/utils/setPushConfig.ts +++ b/sample-apps/react-native/dogfood/src/utils/setPushConfig.ts @@ -1,12 +1,18 @@ import { StreamVideoClient, StreamVideoRN, + onAndroidNotifeeEvent, + isNotifeeStreamVideoEvent, + oniOSNotifeeEvent, } from '@stream-io/video-react-native-sdk'; import { AndroidImportance } from '@notifee/react-native'; import { staticNavigate } from './staticNavigationUtils'; import { mmkvStorage } from '../contexts/createStoreContext'; import { createToken } from '../modules/helpers/createToken'; import { prontoCallId$ } from '../hooks/useProntoLinkEffect'; +import { Platform } from 'react-native'; +import notifee from '@notifee/react-native'; +import { setFirebaseListeners } from './setFirebaseListeners'; export function setPushConfig() { StreamVideoRN.setPushConfig({ @@ -45,12 +51,6 @@ export function setPushConfig() { }, }, createStreamVideoClient, - navigateAcceptCall: () => { - staticNavigate({ name: 'Call', params: undefined }); - }, - navigateToIncomingCall: () => { - staticNavigate({ name: 'Call', params: undefined }); - }, onTapNonRingingCallNotification: (call_cid) => { const [callType, callId] = call_cid.split(':'); if (callType === 'default') { @@ -63,6 +63,31 @@ export function setPushConfig() { } }, }); + + setFirebaseListeners(); + if (Platform.OS === 'android') { + // on press handlers of background notifications + notifee.onBackgroundEvent(async (event) => { + if (isNotifeeStreamVideoEvent(event)) { + await onAndroidNotifeeEvent({ event, isBackground: true }); + } + }); + // on press handlers of foreground notifications + notifee.onForegroundEvent((event) => { + if (isNotifeeStreamVideoEvent(event)) { + onAndroidNotifeeEvent({ event, isBackground: false }); + } + }); + } + if (Platform.OS === 'ios') { + // on press handlers of foreground notifications for iOS + // note: used only for non-ringing notifications + notifee.onForegroundEvent((event) => { + if (isNotifeeStreamVideoEvent(event)) { + oniOSNotifeeEvent({ event, isBackground: false }); + } + }); + } } /** diff --git a/sample-apps/react-native/expo-video-sample/app/index.tsx b/sample-apps/react-native/expo-video-sample/app/index.tsx index 8728a3ed34..f76381bef2 100644 --- a/sample-apps/react-native/expo-video-sample/app/index.tsx +++ b/sample-apps/react-native/expo-video-sample/app/index.tsx @@ -6,15 +6,19 @@ import { ScrollView, KeyboardAvoidingView, } from 'react-native'; -import { NavigationHeader } from '../components/NavigationHeader'; +import { + isExpoNotificationStreamVideoEvent, + oniOSExpoNotificationEvent, +} from '@stream-io/video-react-native-sdk'; import { SafeAreaView } from 'react-native-safe-area-context'; +import * as Notifications from 'expo-notifications'; +import { NavigationHeader } from '../components/NavigationHeader'; import CreateMeetingCall from '../components/CreateMeetingCall'; import CreateRingingCall from '../components/CreateRingingCall'; -import * as Notifications from 'expo-notifications'; export default function CreateCallScreen() { useEffect(() => { - const run = async () => { + const requestPermissions = async () => { await Notifications.requestPermissionsAsync(); if (Platform.OS === 'android') { if (Platform.Version > 30) { @@ -24,7 +28,22 @@ export default function CreateCallScreen() { } } }; - run(); + requestPermissions(); + + if (Platform.OS === 'ios') { + // This listener is fired whenever a notification is received while the app is foregrounded. + // here the notification payload is processed and the call is added to the low level client state + const subscription = Notifications.addNotificationReceivedListener( + (notification) => { + if (isExpoNotificationStreamVideoEvent(notification)) { + oniOSExpoNotificationEvent(notification); + } + }, + ); + return () => { + subscription.remove(); + }; + } }, []); return ( diff --git a/sample-apps/react-native/expo-video-sample/app/ringing.tsx b/sample-apps/react-native/expo-video-sample/app/ringing.tsx index bd7636e839..150609031d 100644 --- a/sample-apps/react-native/expo-video-sample/app/ringing.tsx +++ b/sample-apps/react-native/expo-video-sample/app/ringing.tsx @@ -11,9 +11,7 @@ import { SafeAreaView } from 'react-native-safe-area-context'; import { router } from 'expo-router'; export default function JoinRingingCallScreen() { - const calls = useCalls().filter( - (c) => c.state.callingState === CallingState.RINGING, - ); + const calls = useCalls().filter((c) => c.ringing); useEffect(() => { if (calls.length > 1) { diff --git a/sample-apps/react-native/expo-video-sample/package.json b/sample-apps/react-native/expo-video-sample/package.json index 17420dea97..b5466318eb 100644 --- a/sample-apps/react-native/expo-video-sample/package.json +++ b/sample-apps/react-native/expo-video-sample/package.json @@ -26,7 +26,6 @@ "expo-router": "~3.5.23", "expo-splash-screen": "~0.27.6", "expo-status-bar": "~1.12.1", - "expo-task-manager": "~11.8.2", "react": "18.2.0", "react-native": "0.74.5", "react-native-callkeep": "4.3.12", diff --git a/sample-apps/react-native/expo-video-sample/utils/setFirebaseListeners.android.ts b/sample-apps/react-native/expo-video-sample/utils/setFirebaseListeners.android.ts new file mode 100644 index 0000000000..4f539d709c --- /dev/null +++ b/sample-apps/react-native/expo-video-sample/utils/setFirebaseListeners.android.ts @@ -0,0 +1,24 @@ +import messaging from '@react-native-firebase/messaging'; +import { + isFirebaseStreamVideoMessage, + firebaseDataHandler, +} from '@stream-io/video-react-native-sdk'; + +export const setFirebaseListeners = () => { + // Set up the background message handler for + // 1. incoming call notifications + // 2. non-ringing notifications + messaging().setBackgroundMessageHandler(async (msg) => { + if (isFirebaseStreamVideoMessage(msg)) { + await firebaseDataHandler(msg.data); + } + }); + // Set up the foreground message handler for + // 1. incoming call notifications + // 2. non-ringing notifications + messaging().onMessage((msg) => { + if (isFirebaseStreamVideoMessage(msg)) { + firebaseDataHandler(msg.data); + } + }); +}; diff --git a/sample-apps/react-native/expo-video-sample/utils/setFirebaseListeners.ts b/sample-apps/react-native/expo-video-sample/utils/setFirebaseListeners.ts new file mode 100644 index 0000000000..9a452a91ed --- /dev/null +++ b/sample-apps/react-native/expo-video-sample/utils/setFirebaseListeners.ts @@ -0,0 +1,3 @@ +export const setFirebaseListeners = () => { + // do nothing on iOS +}; diff --git a/sample-apps/react-native/expo-video-sample/utils/setPushConfig.ts b/sample-apps/react-native/expo-video-sample/utils/setPushConfig.ts index edb8515a61..af3c5b1e78 100644 --- a/sample-apps/react-native/expo-video-sample/utils/setPushConfig.ts +++ b/sample-apps/react-native/expo-video-sample/utils/setPushConfig.ts @@ -1,14 +1,17 @@ import { StreamVideoClient, StreamVideoRN, + oniOSNotifeeEvent, + isNotifeeStreamVideoEvent, + onAndroidNotifeeEvent, } from '@stream-io/video-react-native-sdk'; -import { AndroidImportance } from '@notifee/react-native'; +import { Platform } from 'react-native'; +import notifee, { AndroidImportance } from '@notifee/react-native'; +import * as Notifications from 'expo-notifications'; import AsyncStorage from '@react-native-async-storage/async-storage'; -import { - staticNavigateToNonRingingCall, - staticNavigateToRingingCall, -} from './staticNavigationUtils'; +import { staticNavigateToNonRingingCall } from './staticNavigationUtils'; import { createToken } from './createToken'; +import { setFirebaseListeners } from './setFirebaseListeners'; export function setPushConfig() { StreamVideoRN.setPushConfig({ @@ -48,16 +51,47 @@ export function setPushConfig() { }, }, createStreamVideoClient, - navigateAcceptCall: () => { - staticNavigateToRingingCall(); - }, - navigateToIncomingCall: () => { - staticNavigateToRingingCall(); - }, onTapNonRingingCallNotification: (_cid, _type) => { staticNavigateToNonRingingCall(); }, }); + + setFirebaseListeners(); + + if (Platform.OS === 'android') { + // on press handlers of background notifications + notifee.onBackgroundEvent(async (event) => { + if (isNotifeeStreamVideoEvent(event)) { + await onAndroidNotifeeEvent({ event, isBackground: true }); + } + }); + // on press handlers of foreground notifications + notifee.onForegroundEvent((event) => { + if (isNotifeeStreamVideoEvent(event)) { + onAndroidNotifeeEvent({ event, isBackground: false }); + } + }); + } + + if (Platform.OS === 'ios') { + // show notification on foreground + // https://docs.expo.dev/push-notifications/receiving-notifications/#foreground-notification-behavior + Notifications.setNotificationHandler({ + handleNotification: async () => ({ + shouldShowAlert: true, + shouldPlaySound: false, + shouldSetBadge: false, + }), + }); + + // on press handlers of foreground notifications for iOS + // note: used only for non-ringing notifications + notifee.onForegroundEvent((event) => { + if (isNotifeeStreamVideoEvent(event)) { + oniOSNotifeeEvent({ event, isBackground: false }); + } + }); + } } /** diff --git a/sample-apps/react-native/expo-video-sample/utils/staticNavigationUtils.ts b/sample-apps/react-native/expo-video-sample/utils/staticNavigationUtils.ts index 51cc909cd4..3778841de7 100644 --- a/sample-apps/react-native/expo-video-sample/utils/staticNavigationUtils.ts +++ b/sample-apps/react-native/expo-video-sample/utils/staticNavigationUtils.ts @@ -5,19 +5,6 @@ export class StaticNavigationService { static authenticationInfo: User | undefined = undefined; } -/** - * This is used to run the navigation logic from root level even before the navigation is ready - */ -export const staticNavigateToRingingCall = () => { - const intervalId = setInterval(async () => { - // run only when the navigation is ready and the user is authenticated - if (StaticNavigationService.authenticationInfo) { - clearInterval(intervalId); - router.push('/ringing'); - } - }, 300); -}; - export const staticNavigateToNonRingingCall = () => { const intervalId = setInterval(async () => { // run only when the navigation is ready and the user is authenticated diff --git a/yarn.lock b/yarn.lock index b5cfadc655..24a9ccb04c 100644 --- a/yarn.lock +++ b/yarn.lock @@ -5822,40 +5822,6 @@ __metadata: languageName: node linkType: hard -"@react-native-firebase/app@npm:17.5.0": - version: 17.5.0 - resolution: "@react-native-firebase/app@npm:17.5.0" - dependencies: - opencollective-postinstall: ^2.0.1 - superstruct: ^0.6.2 - peerDependencies: - expo: ">=47.0.0" - react: "*" - react-native: "*" - peerDependenciesMeta: - expo: - optional: true - checksum: 7d86f43a063561971edf328f7a2a1928660beb9617cbc3c5ba73ab65f359e0cadbc5e276e5654cfa0ef2d25c29f3f98587ebfd58f193be51f472facff3c3024f - languageName: node - linkType: hard - -"@react-native-firebase/app@npm:18.8.0": - version: 18.8.0 - resolution: "@react-native-firebase/app@npm:18.8.0" - dependencies: - opencollective-postinstall: ^2.0.3 - superstruct: ^0.6.2 - peerDependencies: - expo: ">=47.0.0" - react: "*" - react-native: "*" - peerDependenciesMeta: - expo: - optional: true - checksum: 10b4e5aec0dd18a690bc2ba07fe95f0d414f2904574dda930efd050b113629469d2c01e1d849b129ce575e84bbf89b53ffbeee55c51716c4a78e36be463c1fff - languageName: node - linkType: hard - "@react-native-firebase/app@npm:19.2.2": version: 19.2.2 resolution: "@react-native-firebase/app@npm:19.2.2" @@ -5873,28 +5839,6 @@ __metadata: languageName: node linkType: hard -"@react-native-firebase/messaging@npm:17.5.0": - version: 17.5.0 - resolution: "@react-native-firebase/messaging@npm:17.5.0" - peerDependencies: - "@react-native-firebase/app": 17.5.0 - checksum: 296dad0335d18e262569c4edc6b62d009fc15887b3a9801f7d49a64eb091cd86548b940cbd1e993d649fb7e14282b1e910b221e701adb8213305580db578411a - languageName: node - linkType: hard - -"@react-native-firebase/messaging@npm:18.8.0": - version: 18.8.0 - resolution: "@react-native-firebase/messaging@npm:18.8.0" - peerDependencies: - "@react-native-firebase/app": 18.8.0 - expo: ">=47.0.0" - peerDependenciesMeta: - expo: - optional: true - checksum: 4a6e744579954310557a56c843049283e8478185609d78409ad3949dbfe0e01007b6fb724496308dec9131266a55d068a0828587476df54ddfc3adc8a1013d29 - languageName: node - linkType: hard - "@react-native-firebase/messaging@npm:19.2.2": version: 19.2.2 resolution: "@react-native-firebase/messaging@npm:19.2.2" @@ -7664,7 +7608,6 @@ __metadata: expo-router: ~3.5.23 expo-splash-screen: ~0.27.6 expo-status-bar: ~1.12.1 - expo-task-manager: ~11.8.2 react: 18.2.0 react-native: 0.74.5 react-native-callkeep: 4.3.12 @@ -7976,8 +7919,8 @@ __metadata: "@react-native-clipboard/clipboard": ^1.11.1 "@react-native-community/netinfo": 11.3.0 "@react-native-community/push-notification-ios": ^1.11.0 - "@react-native-firebase/app": 18.8.0 - "@react-native-firebase/messaging": 18.8.0 + "@react-native-firebase/app": 19.2.2 + "@react-native-firebase/messaging": 19.2.2 "@react-native-google-signin/google-signin": ^11.0.0 "@react-native/babel-preset": 0.73.21 "@react-native/eslint-config": 0.74.84 @@ -8015,11 +7958,11 @@ __metadata: react-native-incall-manager: 4.2.0 react-native-mmkv: 2.8.0 react-native-permissions: ^4.1.1 - react-native-reanimated: ^3.7.0 - react-native-safe-area-context: ^4.4.1 - react-native-screens: ^3.29.0 + react-native-reanimated: ~3.10.1 + react-native-safe-area-context: 4.10.5 + react-native-screens: 3.31.1 react-native-svg: ^13.9.0 - react-native-video: ^6.0.0-beta.5 + react-native-video: ^6.7.0 react-native-voip-push-notification: ~3.3.1 react-test-renderer: 18.2.0 rxjs: ~7.8.1 @@ -8037,8 +7980,8 @@ __metadata: "@notifee/react-native": 7.8.0 "@react-native-community/netinfo": 9.3.9 "@react-native-community/push-notification-ios": 1.11.0 - "@react-native-firebase/app": 17.5.0 - "@react-native-firebase/messaging": 17.5.0 + "@react-native-firebase/app": 19.2.2 + "@react-native-firebase/messaging": 19.2.2 "@react-native/eslint-config": ^0.74.84 "@stream-io/react-native-webrtc": 118.1.0 "@stream-io/video-client": "workspace:*" @@ -8056,8 +7999,7 @@ __metadata: expo: 50.0.19 expo-build-properties: ^0.12.3 expo-modules-core: 1.12.16 - expo-notifications: ~0.27.8 - expo-task-manager: ~11.7.3 + expo-notifications: ~0.28.18 intl-pluralrules: 2.0.1 jest: ^29.7.0 lodash.merge: ^4.6.2 @@ -8088,7 +8030,6 @@ __metadata: expo: ">=47.0.0" expo-build-properties: "*" expo-notifications: "*" - expo-task-manager: "*" react: ">=17.0.0" react-native: ">=0.67.0" react-native-callkeep: ">=4.3.11" @@ -8114,8 +8055,6 @@ __metadata: optional: true expo-notifications: optional: true - expo-task-manager: - optional: true react-native-callkeep: optional: true react-native-gesture-handler: @@ -14204,15 +14143,6 @@ __metadata: languageName: node linkType: hard -"expo-application@npm:~5.8.0": - version: 5.8.4 - resolution: "expo-application@npm:5.8.4" - peerDependencies: - expo: "*" - checksum: 80b0055e76c01a3c7a9068accc5016c4fb999fbbe037191b5d6a6a34e72246a919bfb46b0ecafbf84b9595092cf87b36c61ff5e4b5e99516b9293769eba2181f - languageName: node - linkType: hard - "expo-application@npm:~5.9.0": version: 5.9.1 resolution: "expo-application@npm:5.9.1" @@ -14412,24 +14342,6 @@ __metadata: languageName: node linkType: hard -"expo-notifications@npm:~0.27.8": - version: 0.27.8 - resolution: "expo-notifications@npm:0.27.8" - dependencies: - "@expo/image-utils": ^0.4.0 - "@ide/backoff": ^1.0.0 - abort-controller: ^3.0.0 - assert: ^2.0.0 - badgin: ^1.1.5 - expo-application: ~5.8.0 - expo-constants: ~15.4.0 - fs-extra: ^9.1.0 - peerDependencies: - expo: "*" - checksum: b6582598c99883e71ddef2a958e77efb0c4254b6e5fa375b885971dbc042db099c264b9f8ae2ad82b7039327fa768cf85dcda98b462912eb4d87b2ed52a4fb28 - languageName: node - linkType: hard - "expo-notifications@npm:~0.28.18": version: 0.28.18 resolution: "expo-notifications@npm:0.28.18" @@ -14510,28 +14422,6 @@ __metadata: languageName: node linkType: hard -"expo-task-manager@npm:~11.7.3": - version: 11.7.3 - resolution: "expo-task-manager@npm:11.7.3" - dependencies: - unimodules-app-loader: ~4.5.0 - peerDependencies: - expo: "*" - checksum: 340425019bc6ad48c9cecc4f5dd4fdbf1388e14f06f525f546fc421abe2cc8eaee76b067f94156a784d677ef43970fe279f4661eee3d9c77e85aaa14dfe8fa6b - languageName: node - linkType: hard - -"expo-task-manager@npm:~11.8.2": - version: 11.8.2 - resolution: "expo-task-manager@npm:11.8.2" - dependencies: - unimodules-app-loader: ~4.6.0 - peerDependencies: - expo: "*" - checksum: ebc0ebb23ce357cfe8be1c19bf781551ea71b40aac69b85814558cf9233b36596241c7b905512bcc174f148aa7d66cd67d2c755747ee16d1359264fc6376ac81 - languageName: node - linkType: hard - "expo@npm:50.0.19": version: 50.0.19 resolution: "expo@npm:50.0.19" @@ -21312,7 +21202,7 @@ __metadata: languageName: node linkType: hard -"opencollective-postinstall@npm:^2.0.1, opencollective-postinstall@npm:^2.0.3": +"opencollective-postinstall@npm:^2.0.3": version: 2.0.3 resolution: "opencollective-postinstall@npm:2.0.3" bin: @@ -22894,7 +22784,7 @@ __metadata: languageName: node linkType: hard -"react-native-reanimated@npm:^3.7.0, react-native-reanimated@npm:~3.10.1": +"react-native-reanimated@npm:~3.10.1": version: 3.10.1 resolution: "react-native-reanimated@npm:3.10.1" dependencies: @@ -22914,7 +22804,7 @@ __metadata: languageName: node linkType: hard -"react-native-safe-area-context@npm:4.10.5, react-native-safe-area-context@npm:^4.4.1": +"react-native-safe-area-context@npm:4.10.5": version: 4.10.5 resolution: "react-native-safe-area-context@npm:4.10.5" peerDependencies: @@ -22924,7 +22814,7 @@ __metadata: languageName: node linkType: hard -"react-native-screens@npm:3.31.1, react-native-screens@npm:^3.29.0": +"react-native-screens@npm:3.31.1": version: 3.31.1 resolution: "react-native-screens@npm:3.31.1" dependencies: @@ -22974,13 +22864,13 @@ __metadata: languageName: node linkType: hard -"react-native-video@npm:^6.0.0-beta.5": - version: 6.0.0-beta.5 - resolution: "react-native-video@npm:6.0.0-beta.5" +"react-native-video@npm:^6.7.0": + version: 6.7.0 + resolution: "react-native-video@npm:6.7.0" peerDependencies: react: "*" react-native: "*" - checksum: 7c9828f6704ee4d6bdde05e7b0ec69c292e09359cf763e1138761a1d577151a6d325219f4fc63c3b63a49a5ff630e8e2f4625d29c61921bee117f9842fa7848e + checksum: ad8b321ef5ac078fededcafa63d7a21fada7c13ecaf34cd795058868ef386791347ba3e5b6f320aa7ab6bf95bcb0b3958cfca540a0675c8c57fdeaf2e828b1cf languageName: node linkType: hard @@ -26358,20 +26248,6 @@ __metadata: languageName: node linkType: hard -"unimodules-app-loader@npm:~4.5.0": - version: 4.5.1 - resolution: "unimodules-app-loader@npm:4.5.1" - checksum: 3f2f353b6eb632ba45c5d5214d6037231ae8442938796fe5fb5b7a5d4b0eff05b6d35cd58d085777e4b1e32a675a99d315de12a4140e55994a0239f909f4ba6d - languageName: node - linkType: hard - -"unimodules-app-loader@npm:~4.6.0": - version: 4.6.0 - resolution: "unimodules-app-loader@npm:4.6.0" - checksum: 20897153b02f5436d51090ae30863950abec6a0438e460f4e5561bd155273c2a6eea626d1135fc8b07f5a57c1969e24f89b4ae4d5df198266ee88719db10444d - languageName: node - linkType: hard - "unique-filename@npm:^1.1.1": version: 1.1.1 resolution: "unique-filename@npm:1.1.1"