diff --git a/packages/client/docusaurus/docs/javascript/02-guides/02-joining-creating-calls.mdx b/packages/client/docusaurus/docs/javascript/02-guides/02-joining-creating-calls.mdx index b1d00c078a..9bcaac220d 100644 --- a/packages/client/docusaurus/docs/javascript/02-guides/02-joining-creating-calls.mdx +++ b/packages/client/docusaurus/docs/javascript/02-guides/02-joining-creating-calls.mdx @@ -97,7 +97,7 @@ Ending a call requires a [special permission](../../guides/permissions-and-moder await call.endCall(); ``` -Only users with special permission can join an ended call. +Only users with a special permission (`OwnCapability.JOIN_ENDED_CALL`) can join an ended call. ### Load call diff --git a/packages/react-native-sdk/CHANGELOG.md b/packages/react-native-sdk/CHANGELOG.md index e4b29efa43..54a3097e99 100644 --- a/packages/react-native-sdk/CHANGELOG.md +++ b/packages/react-native-sdk/CHANGELOG.md @@ -2,6 +2,24 @@ This file was generated using [@jscutlery/semver](https://github.com/jscutlery/semver). +## [0.10.0](https://github.com/GetStream/stream-video-js/compare/@stream-io/video-react-native-sdk-0.9.7...@stream-io/video-react-native-sdk-0.10.0) (2024-08-08) + + +### ⚠ BREAKING CHANGES + +* **react-native:** make notifee to be optional (#1456) + +### Bug Fixes + +* **react-native:** make notifee to be optional ([#1456](https://github.com/GetStream/stream-video-js/issues/1456)) ([0b3f787](https://github.com/GetStream/stream-video-js/commit/0b3f7876c82a8873901bc1bc77a17f6f98825166)) + +### [0.9.7](https://github.com/GetStream/stream-video-js/compare/@stream-io/video-react-native-sdk-0.9.6...@stream-io/video-react-native-sdk-0.9.7) (2024-08-06) + + +### Bug Fixes + +* added workaround for android where video doesn't resume when resuming app from lock screen ([#1454](https://github.com/GetStream/stream-video-js/issues/1454)) ([b112506](https://github.com/GetStream/stream-video-js/commit/b1125069b24c3bbbf0191582ba27ff841a0cd9f8)) + ### [0.9.6](https://github.com/GetStream/stream-video-js/compare/@stream-io/video-react-native-sdk-0.9.5...@stream-io/video-react-native-sdk-0.9.6) (2024-07-31) ### Dependency Updates diff --git a/packages/react-native-sdk/docusaurus/docs/reactnative/01-setup/02-installation/01-react-native.mdx b/packages/react-native-sdk/docusaurus/docs/reactnative/01-setup/02-installation/01-react-native.mdx index 63163455ba..5976fd8ad9 100644 --- a/packages/react-native-sdk/docusaurus/docs/reactnative/01-setup/02-installation/01-react-native.mdx +++ b/packages/react-native-sdk/docusaurus/docs/reactnative/01-setup/02-installation/01-react-native.mdx @@ -24,7 +24,7 @@ Stream Video React Native SDK requires installing some peer dependencies to prov ```bash title=Terminal yarn add @stream-io/react-native-webrtc \ react-native-incall-manager react-native-svg \ - @react-native-community/netinfo @notifee/react-native + @react-native-community/netinfo npx pod-install ``` @@ -35,7 +35,6 @@ So what did we install precisely? - `react-native-incall-manager` handles media-routes/sensors/events during an audio/video call. - `react-native-svg` provides SVG support to React Native, SVRN's components and it's icons are reliant on this dependency. - `@react-native-community/netinfo` - is used to detect the device's connectivity state, type and quality. -- `@notifee/react-native` - is used to keep calls alive in the background on Android. ### Android Specific installation diff --git a/packages/react-native-sdk/docusaurus/docs/reactnative/01-setup/02-installation/02-expo.mdx b/packages/react-native-sdk/docusaurus/docs/reactnative/01-setup/02-installation/02-expo.mdx index 0d81f525c9..b328d16f0e 100644 --- a/packages/react-native-sdk/docusaurus/docs/reactnative/01-setup/02-installation/02-expo.mdx +++ b/packages/react-native-sdk/docusaurus/docs/reactnative/01-setup/02-installation/02-expo.mdx @@ -22,7 +22,6 @@ npx expo install @config-plugins/react-native-webrtc npx expo install react-native-incall-manager npx expo install react-native-svg npx expo install @react-native-community/netinfo -npx expo install @notifee/react-native ``` So what did we install precisely? @@ -33,7 +32,6 @@ So what did we install precisely? - `react-native-incall-manager` handles media-routes/sensors/events during an audio/video call. - `react-native-svg` provides SVG support to React Native, SVRN's components and it's icons are reliant on this dependency. - `@react-native-community/netinfo` - is used to detect the device's connectivity state, type and quality. -- `@notifee/react-native` - is used to keep calls alive in the background on Android. ### Android Specific installation diff --git a/packages/react-native-sdk/docusaurus/docs/reactnative/03-core/02-joining-creating-calls.mdx b/packages/react-native-sdk/docusaurus/docs/reactnative/03-core/02-joining-creating-calls.mdx index f27d6bf01c..d407c03315 100644 --- a/packages/react-native-sdk/docusaurus/docs/reactnative/03-core/02-joining-creating-calls.mdx +++ b/packages/react-native-sdk/docusaurus/docs/reactnative/03-core/02-joining-creating-calls.mdx @@ -77,7 +77,7 @@ Ending a call requires a [special permission](../../guides/permissions-and-moder await call.endCall(); ``` -Only users with special permission can join an ended call. +Only users with a special permission (`OwnCapability.JOIN_ENDED_CALL`) can join an ended call. ### Load call diff --git a/packages/react-native-sdk/docusaurus/docs/reactnative/03-core/07-keeping-call-alive.mdx b/packages/react-native-sdk/docusaurus/docs/reactnative/03-core/07-keeping-call-alive.mdx index a279809bbc..d11e5c3d1b 100644 --- a/packages/react-native-sdk/docusaurus/docs/reactnative/03-core/07-keeping-call-alive.mdx +++ b/packages/react-native-sdk/docusaurus/docs/reactnative/03-core/07-keeping-call-alive.mdx @@ -17,44 +17,32 @@ Head over to the documentation [here](../../advanced/pip/) on how to picture-in- ### Android 7 -There is no support for Picture-in-picture (PiP) mode below Android 8. Hence in those platforms, we use a [foreground service](https://developer.android.com/guide/components/foreground-services) to keep the call alive. The SDK will manage the foreground service. In Expo, the config plugin adds it up. But if Expo is not used, be sure to add the following: - -#### Add declarations in AndroidManifest +There is no support for Picture-in-picture (PiP) mode below Android 8. Hence in those platforms, we use a [foreground service](https://developer.android.com/guide/components/foreground-services) to keep the call alive. The SDK will automatically create and manage the foreground service. The only requirement is to install the `Notifee` library so that SDK can handle a foreground service. To install the [`Notifee`](https://github.com/invertase/notifee) library, run the following command in your terminal of choice: -In Expo, declarations in Android Manifest are automatically done by the expo config plugin of the SDK, so nothing needs to be added manually. +```bash title=Terminal +npx expo install @notifee/react-native +``` -Add the following in `AndroidManifest.xml`: - -```xml title="AndroidManifest.xml" - - - - - - +```bash title=Terminal +yarn add @notifee/react-native +npx pod-install ``` - -#### Optional: override the default configuration of the notifications +#### Optional: override the default configuration of the foreground service notifications You can also optionally override the default configuration of the notification used by the SDK. Below we give an example of that: ```ts import { StreamVideoRN } from '@stream-io/video-react-native-sdk'; -import { AndroidImportance } from '@notifee/react-native'; StreamVideoRN.updateConfig({ foregroundService: { 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 23fdb35bba..e659e46729 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 @@ -27,6 +27,7 @@ yarn add @react-native-firebase/app yarn add @react-native-firebase/messaging yarn add react-native-callkeep yarn add react-native-voip-push-notification +yarn add @notifee/react-native npx pod-install ``` 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 9590564a68..4106ca14e2 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 @@ -27,6 +27,7 @@ npx expo install @react-native-firebase/app npx expo install @react-native-firebase/messaging npx expo install react-native-voip-push-notification npx expo install react-native-callkeep +npx expo install @notifee/react-native ``` So what did we install precisely? 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 8f3a738b4a..3dd1467037 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 @@ -20,6 +20,7 @@ Please follow the below guides for adding appropriate push providers to Stream: yarn add @react-native-firebase/app yarn add @react-native-firebase/messaging yarn add @react-native-community/push-notification-ios +yarn add @notifee/react-native npx pod-install ``` 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 2bdab82b4a..e5d63f1a7d 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 @@ -19,6 +19,7 @@ Please follow the below guides for adding appropriate push providers to Stream: ```bash title=Terminal npx expo install expo-notifications npx expo install expo-task-manager +npx expo install @notifee/react-native ``` So what did we install precisely? diff --git a/packages/react-native-sdk/docusaurus/docs/reactnative/06-advanced/08-screensharing/02-setup/01-react-native.mdx b/packages/react-native-sdk/docusaurus/docs/reactnative/06-advanced/08-screensharing/02-setup/01-react-native.mdx index 610f3b13b4..713801fb86 100644 --- a/packages/react-native-sdk/docusaurus/docs/reactnative/06-advanced/08-screensharing/02-setup/01-react-native.mdx +++ b/packages/react-native-sdk/docusaurus/docs/reactnative/06-advanced/08-screensharing/02-setup/01-react-native.mdx @@ -19,7 +19,6 @@ To be able to use the foreground service, the permission must be declared in the - // highlight-next-line ``` diff --git a/packages/react-native-sdk/expo-config-plugin/__tests__/withAndroidManifest.test.ts b/packages/react-native-sdk/expo-config-plugin/__tests__/withAndroidManifest.test.ts index bda4849149..b02006bdc2 100644 --- a/packages/react-native-sdk/expo-config-plugin/__tests__/withAndroidManifest.test.ts +++ b/packages/react-native-sdk/expo-config-plugin/__tests__/withAndroidManifest.test.ts @@ -35,6 +35,9 @@ const getMainActivityOrThrow = AndroidConfig.Manifest.getMainActivityOrThrow; const sampleManifestPath = getFixturePath('AndroidManifest.xml'); const props: ConfigProps = { + ringingPushNotifications: { + disableVideoIos: false, + }, androidPictureInPicture: { enableAutomaticEnter: true, }, diff --git a/packages/react-native-sdk/expo-config-plugin/__tests__/withAndroidPermissions.test.ts b/packages/react-native-sdk/expo-config-plugin/__tests__/withAndroidPermissions.test.ts index 5301a3594e..9be3c33dca 100644 --- a/packages/react-native-sdk/expo-config-plugin/__tests__/withAndroidPermissions.test.ts +++ b/packages/react-native-sdk/expo-config-plugin/__tests__/withAndroidPermissions.test.ts @@ -14,6 +14,7 @@ describe('withStreamVideoReactNativeSDKAndroidPermissions', () => { }; const props: ConfigProps = { enableScreenshare: true, + ringingPushNotifications: { disableVideoIos: false }, }; const updatedConfig = withStreamVideoReactNativeSDKAndroidPermissions( diff --git a/packages/react-native-sdk/expo-config-plugin/src/withAndroidManifest.ts b/packages/react-native-sdk/expo-config-plugin/src/withAndroidManifest.ts index b6ced3b1f7..0ef7f9a60d 100644 --- a/packages/react-native-sdk/expo-config-plugin/src/withAndroidManifest.ts +++ b/packages/react-native-sdk/expo-config-plugin/src/withAndroidManifest.ts @@ -16,12 +16,11 @@ type ManifestService = Unpacked< >; function getNotifeeService() { - /* Example: + /* We add this service to the AndroidManifest.xml: + android:foregroundServiceType="dataSync" /> */ const foregroundServiceType = 'dataSync'; let head = prefixAndroidKeys({ @@ -40,10 +39,10 @@ const withStreamVideoReactNativeSDKManifest: ConfigPlugin = ( props ) => { return withAndroidManifest(configuration, (config) => { - try { - const androidManifest = config.modResults; - const mainApplication = getMainApplicationOrThrow(androidManifest); - /* Add the notifee Service */ + const androidManifest = config.modResults; + const mainApplication = getMainApplicationOrThrow(androidManifest); + if (props?.ringingPushNotifications) { + /* Add the notifee foreground Service */ let services = mainApplication.service ?? []; // we filter out the existing notifee service (if any) so that we can override it services = services.filter( @@ -52,39 +51,32 @@ const withStreamVideoReactNativeSDKManifest: ConfigPlugin = ( ); services.push(getNotifeeService()); mainApplication.service = services; + } - if (props?.androidPictureInPicture) { - const mainActivity = getMainActivityOrThrow(androidManifest); - ('keyboard|keyboardHidden|orientation|screenSize|uiMode'); - const currentConfigChangesArray = mainActivity.$[ - 'android:configChanges' - ] - ? mainActivity.$['android:configChanges'].split('|') - : []; - const neededConfigChangesArray = - 'screenSize|smallestScreenSize|screenLayout|orientation'.split('|'); - // Create a Set from the two arrays. - const set = new Set([ - ...currentConfigChangesArray, - ...neededConfigChangesArray, - ]); - const mergedConfigChanges = [...set]; - mainActivity.$['android:configChanges'] = mergedConfigChanges.join('|'); - mainActivity.$['android:supportsPictureInPicture'] = 'true'; - } + if (props?.androidPictureInPicture) { + const mainActivity = getMainActivityOrThrow(androidManifest); + ('keyboard|keyboardHidden|orientation|screenSize|uiMode'); + const currentConfigChangesArray = mainActivity.$['android:configChanges'] + ? mainActivity.$['android:configChanges'].split('|') + : []; + const neededConfigChangesArray = + 'screenSize|smallestScreenSize|screenLayout|orientation'.split('|'); + // Create a Set from the two arrays. + const set = new Set([ + ...currentConfigChangesArray, + ...neededConfigChangesArray, + ]); + const mergedConfigChanges = [...set]; + mainActivity.$['android:configChanges'] = mergedConfigChanges.join('|'); + mainActivity.$['android:supportsPictureInPicture'] = 'true'; + } - if (props?.ringingPushNotifications?.showWhenLockedAndroid) { - const mainActivity = getMainActivityOrThrow(androidManifest); - mainActivity.$['android:showWhenLocked'] = 'true'; - mainActivity.$['android:turnScreenOn'] = 'true'; - } - config.modResults = androidManifest; - } catch (error: any) { - console.log(error); - throw new Error( - 'Cannot setup StreamVideoReactNativeSDK because the AndroidManifest is malformed' - ); + if (props?.ringingPushNotifications?.showWhenLockedAndroid) { + const mainActivity = getMainActivityOrThrow(androidManifest); + mainActivity.$['android:showWhenLocked'] = 'true'; + mainActivity.$['android:turnScreenOn'] = 'true'; } + config.modResults = androidManifest; return config; }); }; diff --git a/packages/react-native-sdk/expo-config-plugin/src/withAndroidPermissions.ts b/packages/react-native-sdk/expo-config-plugin/src/withAndroidPermissions.ts index 4b7191997d..1f0b1fc7aa 100644 --- a/packages/react-native-sdk/expo-config-plugin/src/withAndroidPermissions.ts +++ b/packages/react-native-sdk/expo-config-plugin/src/withAndroidPermissions.ts @@ -4,22 +4,29 @@ import { ConfigProps } from './common/types'; const withStreamVideoReactNativeSDKAndroidPermissions: ConfigPlugin< ConfigProps > = (configuration, props) => { - const foregroundServicePermissions = [ - 'android.permission.FOREGROUND_SERVICE', - 'android.permission.FOREGROUND_SERVICE_DATA_SYNC', - ]; - if (props?.enableScreenshare) { - foregroundServicePermissions.push( - 'android.permission.FOREGROUND_SERVICE_MEDIA_PROJECTION' - ); - } - const config = AndroidConfig.Permissions.withPermissions(configuration, [ - 'android.permission.POST_NOTIFICATIONS', - ...foregroundServicePermissions, + const permissions = [ 'android.permission.BLUETOOTH', 'android.permission.BLUETOOTH_CONNECT', 'android.permission.BLUETOOTH_ADMIN', - ]); + ]; + if (props?.ringingPushNotifications || props?.enableScreenshare) { + permissions.push( + 'android.permission.POST_NOTIFICATIONS', + 'android.permission.FOREGROUND_SERVICE' + ); + if (props?.ringingPushNotifications) { + permissions.push('android.permission.FOREGROUND_SERVICE_DATA_SYNC'); + } + if (props?.enableScreenshare) { + permissions.push( + 'android.permission.FOREGROUND_SERVICE_MEDIA_PROJECTION' + ); + } + } + const config = AndroidConfig.Permissions.withPermissions( + configuration, + permissions + ); return config; }; diff --git a/packages/react-native-sdk/expo-config-plugin/src/withMainActivity.ts b/packages/react-native-sdk/expo-config-plugin/src/withMainActivity.ts index 365682d7a9..f22f6433fe 100644 --- a/packages/react-native-sdk/expo-config-plugin/src/withMainActivity.ts +++ b/packages/react-native-sdk/expo-config-plugin/src/withMainActivity.ts @@ -13,39 +13,33 @@ const withStreamVideoReactNativeSDKMainActivity: ConfigPlugin = ( return withMainActivity(configuration, (config) => { const isMainActivityJava = config.modResults.language === 'java'; - try { - config.modResults.contents = addImports( + config.modResults.contents = addImports( + config.modResults.contents, + [ + 'com.streamvideo.reactnative.StreamVideoReactNative', + 'android.os.Build', + 'android.util.Rational', + 'androidx.lifecycle.Lifecycle', + 'android.app.PictureInPictureParams', + 'com.oney.WebRTCModule.WebRTCModuleOptions', + ], + isMainActivityJava + ); + config.modResults.contents = addOnPictureInPictureModeChanged( + config.modResults.contents, + isMainActivityJava + ); + if (props?.androidPictureInPicture?.enableAutomaticEnter) { + config.modResults.contents = addOnUserLeaveHint( config.modResults.contents, - [ - 'com.streamvideo.reactnative.StreamVideoReactNative', - 'android.os.Build', - 'android.util.Rational', - 'androidx.lifecycle.Lifecycle', - 'android.app.PictureInPictureParams', - 'com.oney.WebRTCModule.WebRTCModuleOptions', - ], isMainActivityJava ); - config.modResults.contents = addOnPictureInPictureModeChanged( + } + if (props?.enableScreenshare) { + config.modResults.contents = addInsideOnCreate( config.modResults.contents, isMainActivityJava ); - if (props?.androidPictureInPicture?.enableAutomaticEnter) { - config.modResults.contents = addOnUserLeaveHint( - config.modResults.contents, - isMainActivityJava - ); - } - if (props?.enableScreenshare) { - config.modResults.contents = addInsideOnCreate( - config.modResults.contents, - isMainActivityJava - ); - } - } catch (error: any) { - throw new Error( - "Cannot add StreamVideoReactNativeSDK to the project's MainApplication because it's malformed." - ); } return config; diff --git a/packages/react-native-sdk/jest-setup.ts b/packages/react-native-sdk/jest-setup.ts index e9d83f4329..5fcb97a886 100644 --- a/packages/react-native-sdk/jest-setup.ts +++ b/packages/react-native-sdk/jest-setup.ts @@ -27,6 +27,10 @@ jest.mock('react-native/Libraries/Utilities/Platform', () => ({ OS: 'ios', select: jest.fn((selector) => selector.ios), Version: '16.2', + constants: { + osVersion: '16.2', + systemName: 'iOS', + }, })); // Mock the notifee module using the mock provided by @notifee/react-native itself diff --git a/packages/react-native-sdk/package.json b/packages/react-native-sdk/package.json index 5879af52aa..367aa7e438 100644 --- a/packages/react-native-sdk/package.json +++ b/packages/react-native-sdk/package.json @@ -1,6 +1,6 @@ { "name": "@stream-io/video-react-native-sdk", - "version": "0.9.6", + "version": "0.10.0", "packageManager": "yarn@3.2.4", "main": "dist/commonjs/index.js", "module": "dist/module/index.js", @@ -17,7 +17,7 @@ "build:expo-plugin": "rimraf expo-config-plugin/dist && tsc --project expo-config-plugin/tsconfig.json", "build": "yarn clean && yarn copy-version && bob build && yarn build:expo-plugin", "test:expo-plugin": "jest expo-config-plugin --coverage", - "test": "jest --coverage && yarn test:expo-plugin", + "test": "yarn copy-version && jest --coverage && yarn test:expo-plugin", "copy-version": "echo \"export const version = '$npm_package_version';\" > ./src/version.ts" }, "files": [ @@ -75,6 +75,9 @@ "react-native-voip-push-notification": ">=3.3.1" }, "peerDependenciesMeta": { + "@notifee/react-native": { + "optional": true + }, "@react-native-community/push-notification-ios": { "optional": true }, diff --git a/packages/react-native-sdk/src/hooks/useAndroidKeepCallAliveEffect.ts b/packages/react-native-sdk/src/hooks/useAndroidKeepCallAliveEffect.ts index 5d51e4fb6b..9cad00df62 100644 --- a/packages/react-native-sdk/src/hooks/useAndroidKeepCallAliveEffect.ts +++ b/packages/react-native-sdk/src/hooks/useAndroidKeepCallAliveEffect.ts @@ -1,15 +1,19 @@ import { useCall, useCallStateHooks } from '@stream-io/video-react-bindings'; import { useEffect, useRef } from 'react'; -import notifee, { AuthorizationStatus } from '@notifee/react-native'; import { StreamVideoRN } from '../utils'; import { Platform } from 'react-native'; import { CallingState, getLogger } from '@stream-io/video-client'; +import { getNotifeeLibNoThrowForKeepCallAlive } from '../utils/push/libs/notifee'; const isAndroid7OrBelow = Platform.OS === 'android' && Platform.Version < 26; +const notifeeLib = isAndroid7OrBelow + ? getNotifeeLibNoThrowForKeepCallAlive() + : undefined; + function setForegroundService() { if (!isAndroid7OrBelow) return; - notifee.registerForegroundService(() => { + notifeeLib?.default.registerForegroundService(() => { return new Promise(() => { const logger = getLogger(['setForegroundService method']); logger('info', 'Foreground service running for call in progress'); @@ -23,8 +27,11 @@ async function startForegroundService(call_cid: string) { const { title, body } = foregroundServiceConfig.android.notificationTexts; // request for notification permission and then start the foreground service - const settings = await notifee.getNotificationSettings(); - if (settings.authorizationStatus !== AuthorizationStatus.AUTHORIZED) { + if (!notifeeLib) return; + const settings = await notifeeLib.default.getNotificationSettings(); + if ( + settings.authorizationStatus !== notifeeLib.AuthorizationStatus.AUTHORIZED + ) { const logger = getLogger(['startForegroundService']); logger( 'info', @@ -32,7 +39,7 @@ async function startForegroundService(call_cid: string) { ); return; } - await notifee.displayNotification({ + await notifeeLib.default.displayNotification({ id: call_cid, title, body, @@ -69,6 +76,7 @@ export const useAndroidKeepCallAliveEffect = () => { const callingState = useCallCallingState(); useEffect((): (() => void) | undefined => { + if (!notifeeLib) return; if (Platform.OS === 'ios' || !activeCallCid) { return; } @@ -79,6 +87,7 @@ export const useAndroidKeepCallAliveEffect = () => { if (foregroundServiceStartedRef.current) { return; } + const notifee = notifeeLib.default; notifee.getDisplayedNotifications().then((displayedNotifications) => { const activeCallNotification = displayedNotifications.find( (notification) => notification.id === activeCallCid @@ -99,7 +108,7 @@ export const useAndroidKeepCallAliveEffect = () => { // cancel any notifee displayed notification when the call has transitioned out of ringing return () => { // cancels the non fg service notifications - notifee.cancelDisplayedNotification(activeCallCid); + notifeeLib.default.cancelDisplayedNotification(activeCallCid); }; } else if ( callingState === CallingState.IDLE || @@ -107,18 +116,20 @@ export const useAndroidKeepCallAliveEffect = () => { ) { if (foregroundServiceStartedRef.current) { // stop foreground service when the call is not active - notifee.stopForegroundService(); + notifeeLib.default.stopForegroundService(); foregroundServiceStartedRef.current = false; } else { - notifee.getDisplayedNotifications().then((displayedNotifications) => { - const activeCallNotification = displayedNotifications.find( - (notification) => notification.id === activeCallCid - ); - if (activeCallNotification) { - // this means that we have a incoming call notification shown as foreground service and we must stop it - notifee.stopForegroundService(); - } - }); + notifeeLib.default + .getDisplayedNotifications() + .then((displayedNotifications) => { + const activeCallNotification = displayedNotifications.find( + (notification) => notification.id === activeCallCid + ); + if (activeCallNotification) { + // this means that we have a incoming call notification shown as foreground service and we must stop it + notifeeLib.default.stopForegroundService(); + } + }); } } }, [activeCallCid, callingState]); @@ -127,7 +138,8 @@ export const useAndroidKeepCallAliveEffect = () => { return () => { // stop foreground service when this effect is unmounted if (foregroundServiceStartedRef.current) { - notifee.stopForegroundService(); + if (!notifeeLib) return; + notifeeLib.default.stopForegroundService(); foregroundServiceStartedRef.current = false; } }; diff --git a/packages/react-native-sdk/src/providers/StreamCall.tsx b/packages/react-native-sdk/src/providers/StreamCall.tsx index 29e103769c..64b58050a1 100644 --- a/packages/react-native-sdk/src/providers/StreamCall.tsx +++ b/packages/react-native-sdk/src/providers/StreamCall.tsx @@ -49,7 +49,18 @@ const AppStateListener = () => { // ref: https://www.reddit.com/r/reactnative/comments/15kib42/appstate_behavior_in_ios_when_swiping_down_to/ const subscription = AppState.addEventListener('change', (nextAppState) => { if (appState.current.match(/background/) && nextAppState === 'active') { - call?.camera?.resume(); + if ( + call?.camera?.state.status === 'enabled' && + Platform.OS === 'android' + ) { + // when device is locked and resumed, the status isnt made disabled but stays enabled + // as a workaround we stop the track and enable again if its already in enabled state + call?.camera?.disable(true).then(() => { + call?.camera?.enable(); + }); + } else { + call?.camera?.resume(); + } appState.current = nextAppState; } else if ( appState.current === 'active' && diff --git a/packages/react-native-sdk/src/utils/StreamVideoRN/types.ts b/packages/react-native-sdk/src/utils/StreamVideoRN/types.ts index 872be90b50..1323569591 100644 --- a/packages/react-native-sdk/src/utils/StreamVideoRN/types.ts +++ b/packages/react-native-sdk/src/utils/StreamVideoRN/types.ts @@ -1,5 +1,5 @@ import { StreamVideoClient } from '@stream-io/video-client'; -import { AndroidChannel } from '@notifee/react-native'; +import type { AndroidChannel } from '@notifee/react-native'; export type NonRingingPushEvent = 'call.live_started' | 'call.notification'; diff --git a/packages/react-native-sdk/src/utils/push/android.ts b/packages/react-native-sdk/src/utils/push/android.ts index 970a4e106a..827cfe0f50 100644 --- a/packages/react-native-sdk/src/utils/push/android.ts +++ b/packages/react-native-sdk/src/utils/push/android.ts @@ -1,8 +1,3 @@ -import notifee, { - EventType, - Event, - AndroidCategory, -} from '@notifee/react-native'; import { FirebaseMessagingTypes } from '@react-native-firebase/messaging'; import { Call, @@ -20,6 +15,8 @@ import { getFirebaseMessagingLibNoThrow, getExpoNotificationsLib, getExpoTaskManagerLib, + getNotifeeLibThrowIfNotInstalledForPush, + NotifeeLib, } from './libs'; import { pushAcceptedIncomingCallCId$, @@ -43,6 +40,14 @@ const DECLINE_CALL_ACTION_ID = 'decline'; type PushConfig = NonNullable; +type onBackgroundEventFunctionParams = Parameters< + NotifeeLib['default']['onBackgroundEvent'] +>[0]; + +type Event = Parameters[0]; + +// EventType = NotifeeLib['EventType']; + /** Setup Firebase push message handler **/ export function setupFirebaseHandlerAndroid(pushConfig: PushConfig) { if (Platform.OS !== 'android') { @@ -114,6 +119,8 @@ export function setupFirebaseHandlerAndroid(pushConfig: PushConfig) { } // 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); }); @@ -208,6 +215,9 @@ const firebaseMessagingOnMessageHandler = async ( AppState.currentState !== 'active'; const asForegroundService = canListenToWS(); + const notifeeLib = getNotifeeLibThrowIfNotInstalledForPush(); + const notifee = notifeeLib.default; + if (asForegroundService) { // Listen to call events from WS through fg service // note: this will replace the current empty fg service runner @@ -291,7 +301,7 @@ const firebaseMessagingOnMessageHandler = async ( }, }, ], - category: AndroidCategory.CALL, + category: notifeeLib.AndroidCategory.CALL, fullScreenAction: { id: 'stream_ringing_incoming_call', }, @@ -316,6 +326,8 @@ const firebaseMessagingOnMessageHandler = async ( return; } } else { + const notifeeLib = getNotifeeLibThrowIfNotInstalledForPush(); + const notifee = notifeeLib.default; // the other types are call.live_started and call.notification const callChannel = pushConfig.android.callChannel; const callNotificationTextGetters = @@ -376,18 +388,24 @@ const onNotifeeEvent = async ( pushAcceptedIncomingCallCId$.observed && pushRejectedIncomingCallCId$.observed; + const notifeeLib = getNotifeeLibThrowIfNotInstalledForPush(); + const notifee = notifeeLib.default; // Check if we need to decline the call const didPressDecline = - type === EventType.ACTION_PRESS && + type === notifeeLib.EventType.ACTION_PRESS && pressAction?.id === DECLINE_CALL_ACTION_ID; - const didDismiss = type === EventType.DISMISSED; + const didDismiss = type === notifeeLib.EventType.DISMISSED; const mustDecline = didPressDecline || didDismiss; // Check if we need to accept the call const mustAccept = - type === EventType.ACTION_PRESS && + type === notifeeLib.EventType.ACTION_PRESS && pressAction?.id === ACCEPT_CALL_ACTION_ID; - if (mustAccept || mustDecline || type === EventType.ACTION_PRESS) { + if ( + mustAccept || + mustDecline || + type === notifeeLib.EventType.ACTION_PRESS + ) { clearPushWSEventSubscriptions(); notifee.stopForegroundService(); } @@ -403,16 +421,17 @@ const onNotifeeEvent = async ( } await processCallFromPushInBackground(pushConfig, call_cid, 'decline'); } else { - if (type === EventType.PRESS) { + if (type === notifeeLib.EventType.PRESS) { pushTappedIncomingCallCId$.next(call_cid); // pressed state will be handled by the app with rxjs observers as the app will go to foreground always - } else if (isBackground && type === EventType.DELIVERED) { + } else if (isBackground && type === notifeeLib.EventType.DELIVERED) { pushAndroidBackgroundDeliveredIncomingCallCId$.next(call_cid); // background delivered state will be handled by the app with rxjs observers as processing needs to happen only when app is opened } } } else { - if (type === EventType.PRESS) { + const notifeeLib = getNotifeeLibThrowIfNotInstalledForPush(); + if (type === notifeeLib.EventType.PRESS) { pushTappedIncomingCallCId$.next(call_cid); pushConfig.onTapNonRingingCallNotification?.( call_cid, diff --git a/packages/react-native-sdk/src/utils/push/ios.ts b/packages/react-native-sdk/src/utils/push/ios.ts index 62d6c7303e..4fab00ab42 100644 --- a/packages/react-native-sdk/src/utils/push/ios.ts +++ b/packages/react-native-sdk/src/utils/push/ios.ts @@ -14,10 +14,13 @@ import { clearPushWSEventSubscriptions, processCallFromPushInBackground, } from './utils'; -import { getExpoNotificationsLib, getPushNotificationIosLib } from './libs'; +import { + getExpoNotificationsLib, + getNotifeeLibThrowIfNotInstalledForPush, + getPushNotificationIosLib, +} from './libs'; import { StreamVideoClient, getLogger } from '@stream-io/video-client'; import { setPushLogoutCallback } from '../internal/pushLogoutCallback'; -import notifee, { EventType } from '@notifee/react-native'; type PushConfig = NonNullable; @@ -96,8 +99,10 @@ export const setupRemoteNotificationsHandleriOS = (pushConfig: PushConfig) => { if (Platform.OS !== 'ios') { return; } - notifee.onForegroundEvent(({ type, detail }) => { - if (type === EventType.PRESS) { + const notifeeLib = getNotifeeLibThrowIfNotInstalledForPush(); + + notifeeLib.default.onForegroundEvent(({ type, detail }) => { + if (type === notifeeLib.EventType.PRESS) { const streamPayload = detail.notification?.data?.stream as | StreamPayload | undefined; 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 58daa12f5a..74ff5fa2a6 100644 --- a/packages/react-native-sdk/src/utils/push/libs/index.ts +++ b/packages/react-native-sdk/src/utils/push/libs/index.ts @@ -4,6 +4,7 @@ export * from './firebaseMessaging'; export * from './iosPushNotification'; export * from './voipPushNotification'; export * from './callkeep'; +export * from './notifee'; /* NOTE: must keep each libs in different files diff --git a/packages/react-native-sdk/src/utils/push/libs/notifee.ts b/packages/react-native-sdk/src/utils/push/libs/notifee.ts new file mode 100644 index 0000000000..b69453dce3 --- /dev/null +++ b/packages/react-native-sdk/src/utils/push/libs/notifee.ts @@ -0,0 +1,33 @@ +import { getLogger } from '../../..'; + +export type NotifeeLib = typeof import('@notifee/react-native'); + +let notifeeLib: NotifeeLib | undefined; + +try { + notifeeLib = require('@notifee/react-native'); +} catch (_e) {} + +const INSTALLATION_INSTRUCTION = + 'Please see https://notifee.app/react-native/docs/installation for installation instructions'; + +export function getNotifeeLibThrowIfNotInstalledForPush() { + if (!notifeeLib) { + throw Error( + '@notifee/react-native is not installed. It is required for implementing push notifications. ' + + INSTALLATION_INSTRUCTION + ); + } + return notifeeLib; +} + +export function getNotifeeLibNoThrowForKeepCallAlive() { + if (!notifeeLib) { + const logger = getLogger(['getNotifeeLibNoThrow']); + logger( + 'info', + `${'@notifee/react-native library not installed. It is required to keep call alive in the background for Android < 26. '}${INSTALLATION_INSTRUCTION}` + ); + } + return notifeeLib; +} diff --git a/packages/react-sdk/docusaurus/docs/React/02-guides/02-joining-creating-calls.mdx b/packages/react-sdk/docusaurus/docs/React/02-guides/02-joining-creating-calls.mdx index 0ba6cf703b..0d7a022b2f 100644 --- a/packages/react-sdk/docusaurus/docs/React/02-guides/02-joining-creating-calls.mdx +++ b/packages/react-sdk/docusaurus/docs/React/02-guides/02-joining-creating-calls.mdx @@ -97,7 +97,7 @@ Ending a call requires a [special permission](../../guides/permissions-and-moder await call.endCall(); ``` -Only users with special permission can join an ended call. +Only users with a special permission (`OwnCapability.JOIN_ENDED_CALL`) can join an ended call. ### Load call diff --git a/sample-apps/react-native/dogfood/Gemfile.lock b/sample-apps/react-native/dogfood/Gemfile.lock index bb4c250538..cc45e478f6 100644 --- a/sample-apps/react-native/dogfood/Gemfile.lock +++ b/sample-apps/react-native/dogfood/Gemfile.lock @@ -10,30 +10,29 @@ GEM i18n (>= 1.6, < 2) minitest (>= 5.1) tzinfo (~> 2.0) - addressable (2.8.6) - public_suffix (>= 2.0.2, < 6.0) + addressable (2.8.7) + public_suffix (>= 2.0.2, < 7.0) algoliasearch (1.27.5) httpclient (~> 2.8, >= 2.8.3) json (>= 1.5.1) - artifactory (3.0.15) + artifactory (3.0.17) ast (2.4.2) atomos (0.1.3) aws-eventstream (1.3.0) - aws-partitions (1.892.0) - aws-sdk-core (3.191.2) + aws-partitions (1.962.0) + aws-sdk-core (3.201.3) aws-eventstream (~> 1, >= 1.3.0) aws-partitions (~> 1, >= 1.651.0) aws-sigv4 (~> 1.8) - base64 jmespath (~> 1, >= 1.6.1) - aws-sdk-kms (1.77.0) - aws-sdk-core (~> 3, >= 3.191.0) - aws-sigv4 (~> 1.1) - aws-sdk-s3 (1.143.0) - aws-sdk-core (~> 3, >= 3.191.0) + aws-sdk-kms (1.88.0) + aws-sdk-core (~> 3, >= 3.201.0) + aws-sigv4 (~> 1.5) + aws-sdk-s3 (1.157.0) + aws-sdk-core (~> 3, >= 3.201.0) aws-sdk-kms (~> 1) - aws-sigv4 (~> 1.8) - aws-sigv4 (1.8.0) + aws-sigv4 (~> 1.5) + aws-sigv4 (1.9.1) aws-eventstream (~> 1, >= 1.0.2) babosa (1.0.4) base64 (0.2.0) @@ -89,7 +88,7 @@ GEM escape (0.0.4) ethon (0.16.0) ffi (>= 1.15.0) - excon (0.109.0) + excon (0.111.0) faraday (1.10.3) faraday-em_http (~> 1.0) faraday-em_synchrony (~> 1.0) @@ -111,22 +110,22 @@ GEM faraday-httpclient (1.0.1) faraday-multipart (1.0.4) multipart-post (~> 2) - faraday-net_http (1.0.1) + faraday-net_http (1.0.2) faraday-net_http_persistent (1.2.0) faraday-patron (1.0.0) faraday-rack (1.0.0) faraday-retry (1.0.3) faraday_middleware (1.2.0) faraday (~> 1.0) - fastimage (2.3.0) - fastlane (2.219.0) + fastimage (2.3.1) + fastlane (2.222.0) CFPropertyList (>= 2.3, < 4.0.0) addressable (>= 2.8, < 3.0.0) artifactory (~> 3.0) aws-sdk-s3 (~> 1.0) babosa (>= 1.0.3, < 2.0.0) bundler (>= 1.12.0, < 3.0.0) - colored + colored (~> 1.2) commander (~> 4.6) dotenv (>= 2.1.1, < 3.0.0) emoji_regex (>= 0.1, < 4.0) @@ -147,10 +146,10 @@ GEM mini_magick (>= 4.9.4, < 5.0.0) multipart-post (>= 2.0.0, < 3.0.0) naturally (~> 2.2) - optparse (>= 0.1.1) + optparse (>= 0.1.1, < 1.0.0) plist (>= 3.1.0, < 4.0.0) rubyzip (>= 2.0.0, < 3.0.0) - security (= 0.1.3) + security (= 0.1.5) simctl (~> 1.6.3) terminal-notifier (>= 2.0.0, < 3.0.0) terminal-table (~> 3) @@ -159,7 +158,7 @@ GEM word_wrap (~> 1.0.0) xcodeproj (>= 1.13.0, < 2.0.0) xcpretty (~> 0.3.0) - xcpretty-travis-formatter (>= 0.0.3) + xcpretty-travis-formatter (>= 0.0.3, < 2.0.0) fastlane-plugin-increment_version_code (0.4.3) fastlane-plugin-increment_version_name (0.0.10) fastlane-plugin-load_json (0.0.1) @@ -185,12 +184,12 @@ GEM google-apis-core (>= 0.11.0, < 2.a) google-apis-storage_v1 (0.31.0) google-apis-core (>= 0.11.0, < 2.a) - google-cloud-core (1.6.1) + google-cloud-core (1.7.1) google-cloud-env (>= 1.0, < 3.a) google-cloud-errors (~> 1.0) google-cloud-env (1.6.0) faraday (>= 0.17.3, < 3.0) - google-cloud-errors (1.3.1) + google-cloud-errors (1.4.0) google-cloud-storage (1.47.0) addressable (~> 2.8) digest-crc (~> 0.4) @@ -206,28 +205,28 @@ GEM os (>= 0.9, < 2.0) signet (>= 0.16, < 2.a) highline (2.0.3) - http-cookie (1.0.5) + http-cookie (1.0.6) domain_name (~> 0.5) httpclient (2.8.3) i18n (1.14.1) concurrent-ruby (~> 1.0) jmespath (1.6.2) - json (2.7.1) - jwt (2.8.1) + json (2.7.2) + jwt (2.8.2) base64 language_server-protocol (3.17.0.3) - mini_magick (4.12.0) + mini_magick (4.13.2) mini_mime (1.1.5) minitest (5.22.2) molinillo (0.8.0) multi_json (1.15.0) - multipart-post (2.4.0) + multipart-post (2.4.1) nanaimo (0.3.0) nap (1.1.0) naturally (2.2.1) netrc (0.11.0) nkf (0.2.0) - optparse (0.4.0) + optparse (0.5.0) os (1.1.4) parallel (1.24.0) parser (3.3.0.5) @@ -237,14 +236,15 @@ GEM public_suffix (4.0.7) racc (1.7.3) rainbow (3.1.1) - rake (13.1.0) + rake (13.2.1) regexp_parser (2.9.0) representable (3.2.0) declarative (< 0.1.0) trailblazer-option (>= 0.1.1, < 0.2.0) uber (< 0.2.0) retriable (3.1.2) - rexml (3.2.6) + rexml (3.3.4) + strscan rouge (2.0.7) rubocop (1.60.2) json (~> 2.3) @@ -268,7 +268,7 @@ GEM ruby-progressbar (1.13.0) ruby2_keywords (0.0.5) rubyzip (2.3.2) - security (0.1.3) + security (0.1.5) signet (0.19.0) addressable (~> 2.8) faraday (>= 0.17.5, < 3.a) @@ -277,6 +277,7 @@ GEM simctl (1.6.10) CFPropertyList naturally + strscan (3.1.0) terminal-notifier (2.0.0) terminal-table (3.0.2) unicode-display_width (>= 1.1.1, < 3) @@ -292,13 +293,13 @@ GEM uber (0.1.0) unicode-display_width (2.5.0) word_wrap (1.0.0) - xcodeproj (1.24.0) + xcodeproj (1.25.0) CFPropertyList (>= 2.3.3, < 4.0) atomos (~> 0.1.3) claide (>= 1.0.2, < 2.0) colored2 (~> 3.1) nanaimo (~> 0.3.0) - rexml (~> 3.2.4) + rexml (>= 3.3.2, < 4.0) xcpretty (0.3.0) rouge (~> 2.0.7) xcpretty-travis-formatter (1.0.1) diff --git a/sample-apps/react-native/dogfood/ios/Podfile b/sample-apps/react-native/dogfood/ios/Podfile index a85e8e6907..128caae326 100644 --- a/sample-apps/react-native/dogfood/ios/Podfile +++ b/sample-apps/react-native/dogfood/ios/Podfile @@ -39,13 +39,19 @@ end target 'StreamReactNativeVideoSDKSample' do config = use_native_modules! + # Beginning with firebase-ios-sdk v9+ (react-native-firebase v15+) we must tell CocoaPods to use frameworks. + # https://rnfirebase.io/#altering-cocoapods-to-use-frameworks + # Note: must disable flipper due to enabling static linkage + use_frameworks! :linkage => :static + $RNFirebaseAsStaticFramework = true + use_react_native!( :path => config[:reactNativePath], # Enables Flipper. # # Note that if you have use_frameworks! enabled, Flipper will not work and # you should disable the next line. - :flipper_configuration => flipper_config, + # :flipper_configuration => flipper_config, # An absolute path to your application root. :app_path => "#{Pod::Config.instance.installation_root}/.." ) diff --git a/sample-apps/react-native/dogfood/package.json b/sample-apps/react-native/dogfood/package.json index 789fbd83a3..33f8cbc87d 100644 --- a/sample-apps/react-native/dogfood/package.json +++ b/sample-apps/react-native/dogfood/package.json @@ -1,6 +1,6 @@ { "name": "@stream-io/video-react-native-dogfood", - "version": "4.0.6", + "version": "4.0.8", "private": true, "scripts": { "android": "react-native run-android", diff --git a/sample-apps/react-native/dogfood/src/navigators/Call.tsx b/sample-apps/react-native/dogfood/src/navigators/Call.tsx index 4632fce84b..71792147f2 100644 --- a/sample-apps/react-native/dogfood/src/navigators/Call.tsx +++ b/sample-apps/react-native/dogfood/src/navigators/Call.tsx @@ -21,7 +21,9 @@ import { useOrientation } from '../hooks/useOrientation'; const CallStack = createNativeStackNavigator(); const Calls = () => { - const calls = useCalls(); + const calls = useCalls().filter( + (c) => c.state.callingState === CallingState.RINGING, + ); const { top } = useSafeAreaInsets(); const orientation = useOrientation(); 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 150609031d..bd7636e839 100644 --- a/sample-apps/react-native/expo-video-sample/app/ringing.tsx +++ b/sample-apps/react-native/expo-video-sample/app/ringing.tsx @@ -11,7 +11,9 @@ import { SafeAreaView } from 'react-native-safe-area-context'; import { router } from 'expo-router'; export default function JoinRingingCallScreen() { - const calls = useCalls().filter((c) => c.ringing); + const calls = useCalls().filter( + (c) => c.state.callingState === CallingState.RINGING, + ); useEffect(() => { if (calls.length > 1) { diff --git a/yarn.lock b/yarn.lock index 87883156b1..398c4015ac 100644 --- a/yarn.lock +++ b/yarn.lock @@ -7326,6 +7326,8 @@ __metadata: react-native-svg: ">=13.6.0" react-native-voip-push-notification: ">=3.3.1" peerDependenciesMeta: + "@notifee/react-native": + optional: true "@react-native-community/push-notification-ios": optional: true "@react-native-firebase/app":