@stream-io/video-react-native-sdk 0.3.18 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (77) hide show
  1. package/CHANGELOG.md +11 -0
  2. package/dist/commonjs/components/Call/CallControls/AcceptCallButton.js +0 -5
  3. package/dist/commonjs/components/Call/CallControls/AcceptCallButton.js.map +1 -1
  4. package/dist/commonjs/components/Call/CallControls/RejectCallButton.js +0 -5
  5. package/dist/commonjs/components/Call/CallControls/RejectCallButton.js.map +1 -1
  6. package/dist/commonjs/hooks/push/useIosVoipPushEventsSetupEffect.js +58 -7
  7. package/dist/commonjs/hooks/push/useIosVoipPushEventsSetupEffect.js.map +1 -1
  8. package/dist/commonjs/hooks/useAndroidKeepCallAliveEffect.js +15 -6
  9. package/dist/commonjs/hooks/useAndroidKeepCallAliveEffect.js.map +1 -1
  10. package/dist/commonjs/providers/MediaStreamManagement.js +25 -34
  11. package/dist/commonjs/providers/MediaStreamManagement.js.map +1 -1
  12. package/dist/commonjs/providers/StreamCall.js +22 -5
  13. package/dist/commonjs/providers/StreamCall.js.map +1 -1
  14. package/dist/commonjs/utils/push/android.js +60 -1
  15. package/dist/commonjs/utils/push/android.js.map +1 -1
  16. package/dist/commonjs/utils/push/ios.js +2 -0
  17. package/dist/commonjs/utils/push/ios.js.map +1 -1
  18. package/dist/commonjs/utils/push/rxSubjects.js +6 -1
  19. package/dist/commonjs/utils/push/rxSubjects.js.map +1 -1
  20. package/dist/commonjs/utils/push/utils.js +68 -1
  21. package/dist/commonjs/utils/push/utils.js.map +1 -1
  22. package/dist/commonjs/version.js +1 -1
  23. package/dist/commonjs/version.js.map +1 -1
  24. package/dist/module/components/Call/CallControls/AcceptCallButton.js +0 -5
  25. package/dist/module/components/Call/CallControls/AcceptCallButton.js.map +1 -1
  26. package/dist/module/components/Call/CallControls/RejectCallButton.js +0 -5
  27. package/dist/module/components/Call/CallControls/RejectCallButton.js.map +1 -1
  28. package/dist/module/hooks/push/useIosVoipPushEventsSetupEffect.js +61 -9
  29. package/dist/module/hooks/push/useIosVoipPushEventsSetupEffect.js.map +1 -1
  30. package/dist/module/hooks/useAndroidKeepCallAliveEffect.js +16 -7
  31. package/dist/module/hooks/useAndroidKeepCallAliveEffect.js.map +1 -1
  32. package/dist/module/providers/MediaStreamManagement.js +25 -34
  33. package/dist/module/providers/MediaStreamManagement.js.map +1 -1
  34. package/dist/module/providers/StreamCall.js +19 -3
  35. package/dist/module/providers/StreamCall.js.map +1 -1
  36. package/dist/module/utils/push/android.js +63 -4
  37. package/dist/module/utils/push/android.js.map +1 -1
  38. package/dist/module/utils/push/ios.js +3 -1
  39. package/dist/module/utils/push/ios.js.map +1 -1
  40. package/dist/module/utils/push/rxSubjects.js +5 -0
  41. package/dist/module/utils/push/rxSubjects.js.map +1 -1
  42. package/dist/module/utils/push/utils.js +65 -0
  43. package/dist/module/utils/push/utils.js.map +1 -1
  44. package/dist/module/version.js +1 -1
  45. package/dist/module/version.js.map +1 -1
  46. package/dist/typescript/components/Call/CallControls/AcceptCallButton.d.ts.map +1 -1
  47. package/dist/typescript/components/Call/CallControls/RejectCallButton.d.ts.map +1 -1
  48. package/dist/typescript/hooks/push/useIosVoipPushEventsSetupEffect.d.ts.map +1 -1
  49. package/dist/typescript/hooks/useAndroidKeepCallAliveEffect.d.ts +1 -0
  50. package/dist/typescript/hooks/useAndroidKeepCallAliveEffect.d.ts.map +1 -1
  51. package/dist/typescript/providers/MediaStreamManagement.d.ts.map +1 -1
  52. package/dist/typescript/providers/StreamCall.d.ts +3 -1
  53. package/dist/typescript/providers/StreamCall.d.ts.map +1 -1
  54. package/dist/typescript/utils/push/android.d.ts.map +1 -1
  55. package/dist/typescript/utils/push/ios.d.ts.map +1 -1
  56. package/dist/typescript/utils/push/rxSubjects.d.ts +6 -0
  57. package/dist/typescript/utils/push/rxSubjects.d.ts.map +1 -1
  58. package/dist/typescript/utils/push/utils.d.ts +22 -1
  59. package/dist/typescript/utils/push/utils.d.ts.map +1 -1
  60. package/dist/typescript/version.d.ts +1 -1
  61. package/dist/typescript/version.d.ts.map +1 -1
  62. package/expo-config-plugin/dist/withAndroidPermissions.js +1 -0
  63. package/expo-config-plugin/dist/withPushAppDelegate.js +7 -1
  64. package/ios/StreamVideoReactNative.h +3 -1
  65. package/ios/StreamVideoReactNative.m +24 -1
  66. package/package.json +1 -1
  67. package/src/components/Call/CallControls/AcceptCallButton.tsx +0 -5
  68. package/src/components/Call/CallControls/RejectCallButton.tsx +0 -5
  69. package/src/hooks/push/useIosVoipPushEventsSetupEffect.ts +76 -8
  70. package/src/hooks/useAndroidKeepCallAliveEffect.ts +18 -7
  71. package/src/providers/MediaStreamManagement.tsx +30 -39
  72. package/src/providers/StreamCall.tsx +25 -3
  73. package/src/utils/push/android.ts +82 -4
  74. package/src/utils/push/ios.ts +6 -1
  75. package/src/utils/push/rxSubjects.ts +10 -0
  76. package/src/utils/push/utils.ts +70 -1
  77. package/src/version.ts +1 -1
@@ -1,6 +1,17 @@
1
- import { StreamVideoClient } from '@stream-io/video-client';
1
+ import { Call, StreamVideoClient } from '@stream-io/video-client';
2
2
  import type { NonRingingPushEvent, StreamVideoConfig } from '../StreamVideoRN/types';
3
3
  type PushConfig = NonNullable<StreamVideoConfig['push']>;
4
+ type CanAddPushWSSubscriptionsRef = {
5
+ current: boolean;
6
+ };
7
+ /**
8
+ * This function is used to check if the call should be ended based on the push notification
9
+ * Useful for callkeep management to end the call if necessary (with reportEndCallWithUUID)
10
+ */
11
+ export declare const shouldCallBeEnded: (callFromPush: Call, created_by_id: string | undefined, receiver_id: string | undefined) => {
12
+ mustEndCall: boolean;
13
+ callkeepReason: number;
14
+ };
4
15
  export declare const processCallFromPushInBackground: (pushConfig: PushConfig, call_cid: string, action: Parameters<typeof processCallFromPush>[2]) => Promise<void>;
5
16
  /**
6
17
  * This function is used process the call from push notifications due to incoming call
@@ -18,5 +29,15 @@ export declare const processCallFromPush: (client: StreamVideoClient, call_cid:
18
29
  * 3. Call all the callbacks to inform the app about the call
19
30
  */
20
31
  export declare const processNonIncomingCallFromPush: (client: StreamVideoClient, call_cid: string, nonRingingNotificationType: NonRingingPushEvent) => Promise<void>;
32
+ /**
33
+ * This function is used to clear all the push related WS subscriptions
34
+ * note: events are subscribed in push for accept/decline through WS
35
+ */
36
+ export declare const clearPushWSEventSubscriptions: () => void;
37
+ /**
38
+ * This ref is used to check if the push WS subscriptions can be added
39
+ * It is used to avoid adding the push WS subscriptions when the client is connected to WS in the foreground
40
+ */
41
+ export declare const canAddPushWSSubscriptionsRef: CanAddPushWSSubscriptionsRef;
21
42
  export {};
22
43
  //# sourceMappingURL=utils.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"utils.d.ts","sourceRoot":"","sources":["../../../../src/utils/push/utils.ts"],"names":[],"mappings":"AAAA,OAAO,EAAQ,iBAAiB,EAAE,MAAM,yBAAyB,CAAC;AAClE,OAAO,KAAK,EACV,mBAAmB,EACnB,iBAAiB,EAClB,MAAM,wBAAwB,CAAC;AAGhC,KAAK,UAAU,GAAG,WAAW,CAAC,iBAAiB,CAAC,MAAM,CAAC,CAAC,CAAC;AAMzD,eAAO,MAAM,+BAA+B,eAC9B,UAAU,YACZ,MAAM,UACR,WAAW,0BAA0B,CAAC,CAAC,CAAC,CAAC,kBAclD,CAAC;AAEF;;;;;;GAMG;AACH,eAAO,MAAM,mBAAmB,WACtB,iBAAiB,YACf,MAAM,UACR,QAAQ,GAAG,SAAS,GAAG,SAAS,kBAmBzC,CAAC;AAEF;;;;;;GAMG;AACH,eAAO,MAAM,8BAA8B,WACjC,iBAAiB,YACf,MAAM,8BACY,mBAAmB,kBAkBhD,CAAC"}
1
+ {"version":3,"file":"utils.d.ts","sourceRoot":"","sources":["../../../../src/utils/push/utils.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,IAAI,EAAW,iBAAiB,EAAE,MAAM,yBAAyB,CAAC;AAC3E,OAAO,KAAK,EACV,mBAAmB,EACnB,iBAAiB,EAClB,MAAM,wBAAwB,CAAC;AAIhC,KAAK,UAAU,GAAG,WAAW,CAAC,iBAAiB,CAAC,MAAM,CAAC,CAAC,CAAC;AAEzD,KAAK,4BAA4B,GAAG;IAAE,OAAO,EAAE,OAAO,CAAA;CAAE,CAAC;AAEzD;;;GAGG;AACH,eAAO,MAAM,iBAAiB,iBACd,IAAI,iBACH,MAAM,GAAG,SAAS,eACpB,MAAM,GAAG,SAAS;;;CAmChC,CAAC;AAMF,eAAO,MAAM,+BAA+B,eAC9B,UAAU,YACZ,MAAM,UACR,WAAW,0BAA0B,CAAC,CAAC,CAAC,CAAC,kBAclD,CAAC;AAEF;;;;;;GAMG;AACH,eAAO,MAAM,mBAAmB,WACtB,iBAAiB,YACf,MAAM,UACR,QAAQ,GAAG,SAAS,GAAG,SAAS,kBAmBzC,CAAC;AAEF;;;;;;GAMG;AACH,eAAO,MAAM,8BAA8B,WACjC,iBAAiB,YACf,MAAM,8BACY,mBAAmB,kBAkBhD,CAAC;AAEF;;;GAGG;AACH,eAAO,MAAM,6BAA6B,YAQzC,CAAC;AAEF;;;GAGG;AACH,eAAO,MAAM,4BAA4B,EAAE,4BAE1C,CAAC"}
@@ -1,2 +1,2 @@
1
- export declare const version = "0.3.18";
1
+ export declare const version = "0.4.0";
2
2
  //# sourceMappingURL=version.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"version.d.ts","sourceRoot":"","sources":["../../src/version.ts"],"names":[],"mappings":"AAAA,eAAO,MAAM,OAAO,WAAW,CAAC"}
1
+ {"version":3,"file":"version.d.ts","sourceRoot":"","sources":["../../src/version.ts"],"names":[],"mappings":"AAAA,eAAO,MAAM,OAAO,UAAU,CAAC"}
@@ -6,6 +6,7 @@ const withStreamVideoReactNativeSDKAndroidPermissions = (config) => {
6
6
  'android.permission.POST_NOTIFICATIONS',
7
7
  'android.permission.FOREGROUND_SERVICE',
8
8
  'android.permission.FOREGROUND_SERVICE_MICROPHONE',
9
+ 'android.permission.FOREGROUND_SERVICE_MEDIA_PROJECTION',
9
10
  'android.permission.BLUETOOTH',
10
11
  'android.permission.BLUETOOTH_CONNECT',
11
12
  'android.permission.BLUETOOTH_ADMIN',
@@ -22,6 +22,7 @@ const withPushAppDelegate = (configuration, props) => {
22
22
  '"RNCallKeep.h"',
23
23
  '<PushKit/PushKit.h>',
24
24
  '"RNVoipPushNotificationManager.h"',
25
+ '"StreamVideoReactNative.h"',
25
26
  ]);
26
27
  config.modResults.contents = addDidFinishLaunchingWithOptions(config.modResults.contents, props.ringingPushNotifications);
27
28
  config.modResults.contents = addDidUpdatePushCredentials(config.modResults.contents);
@@ -86,6 +87,11 @@ function addDidReceiveIncomingPushCallback(contents) {
86
87
  NSDictionary *stream = payload.dictionaryPayload[@"stream"];
87
88
  NSString *uuid = [[NSUUID UUID] UUIDString];
88
89
  NSString *createdCallerName = stream[@"created_by_display_name"];
90
+ NSString *cid = stream[@"call_cid"];
91
+
92
+ [StreamVideoReactNative registerIncomingCall:cid uuid:uuid];
93
+
94
+ [RNVoipPushNotificationManager addCompletionHandler:uuid completionHandler:completion];
89
95
 
90
96
  // display the incoming call notification
91
97
  [RNCallKeep reportNewIncomingCall: uuid
@@ -99,7 +105,7 @@ function addDidReceiveIncomingPushCallback(contents) {
99
105
  supportsUngrouping: YES
100
106
  fromPushKit: YES
101
107
  payload: stream
102
- withCompletionHandler: completion];
108
+ withCompletionHandler: nil];
103
109
  `;
104
110
  if (!contents.includes('[RNVoipPushNotificationManager didReceiveIncomingPushWithPayload')) {
105
111
  const codeblock = (0, codeMod_1.findObjcFunctionCodeBlock)(contents, DID_RECEIVE_INCOMING_PUSH);
@@ -4,6 +4,8 @@
4
4
 
5
5
  - (void)screenShareEventReceived:(NSString *)event;
6
6
 
7
+ + (void)registerIncomingCall:(NSString *)cid uuid:(NSString *)uuid;
8
+
7
9
  + (void)setup DEPRECATED_MSG_ATTRIBUTE("No need to use setup() anymore");
8
10
 
9
- @end
11
+ @end
@@ -7,6 +7,7 @@
7
7
  // Do not change these consts, it is what is used react-native-webrtc
8
8
  NSNotificationName const kBroadcastStartedNotification = @"iOS_BroadcastStarted";
9
9
  NSNotificationName const kBroadcastStoppedNotification = @"iOS_BroadcastStopped";
10
+ NSMutableDictionary *dictionary;
10
11
 
11
12
  void broadcastNotificationCallback(CFNotificationCenterRef center,
12
13
  void *observer,
@@ -92,9 +93,31 @@ RCT_EXPORT_MODULE();
92
93
  }
93
94
  }
94
95
 
96
+ +(void)registerIncomingCall:(NSString *)cid uuid:(NSString *)uuid {
97
+ if (dictionary == nil) {
98
+ dictionary = [NSMutableDictionary dictionary];
99
+ }
100
+ dictionary[cid] = uuid;
101
+ }
102
+
103
+ RCT_EXPORT_METHOD(getIncomingCallUUid:(NSString *)cid
104
+ resolver:(RCTPromiseResolveBlock)resolve
105
+ rejecter:(RCTPromiseRejectBlock)reject)
106
+ {
107
+ if (dictionary == nil) {
108
+ reject(@"access_failure", @"no incoming call dictionary found", nil);
109
+ }
110
+ NSString *uuid = dictionary[cid];
111
+ if (uuid) {
112
+ resolve(uuid);
113
+ } else {
114
+ reject(@"access_failure", @"requested incoming call found", nil);
115
+ }
116
+
117
+ }
118
+
95
119
  -(NSArray<NSString *> *)supportedEvents {
96
120
  return @[@"StreamVideoReactNative_Ios_Screenshare_Event"];
97
121
  }
98
122
 
99
-
100
123
  @end
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@stream-io/video-react-native-sdk",
3
- "version": "0.3.18",
3
+ "version": "0.4.0",
4
4
  "packageManager": "yarn@3.2.4",
5
5
  "main": "dist/commonjs/index.js",
6
6
  "module": "dist/module/index.js",
@@ -3,8 +3,6 @@ import React from 'react';
3
3
  import { CallControlsButton } from './CallControlsButton';
4
4
  import { Phone } from '../../../icons';
5
5
  import { useTheme } from '../../../contexts/ThemeContext';
6
- import { Platform } from 'react-native';
7
- import notifee from '@notifee/react-native';
8
6
 
9
7
  /**
10
8
  * The props for the Accept Call button.
@@ -45,9 +43,6 @@ export const AcceptCallButton = ({
45
43
  return;
46
44
  }
47
45
  try {
48
- if (Platform.OS === 'android' && call?.cid) {
49
- notifee.cancelDisplayedNotification(call?.cid);
50
- }
51
46
  await call?.join();
52
47
  if (onAcceptCallHandler) {
53
48
  onAcceptCallHandler();
@@ -4,8 +4,6 @@ import { CallControlsButton } from './CallControlsButton';
4
4
  import { PhoneDown } from '../../../icons';
5
5
  import { CallingState } from '@stream-io/video-client';
6
6
  import { useTheme } from '../../../contexts/ThemeContext';
7
- import { Platform } from 'react-native';
8
- import notifee from '@notifee/react-native';
9
7
 
10
8
  /**
11
9
  * The props for the Reject Call button.
@@ -52,9 +50,6 @@ export const RejectCallButton = ({
52
50
  if (callingState === CallingState.LEFT) {
53
51
  return;
54
52
  }
55
- if (Platform.OS === 'android' && call?.cid) {
56
- notifee.cancelDisplayedNotification(call?.cid);
57
- }
58
53
  await call?.leave({ reject: true });
59
54
  if (onRejectCallHandler) {
60
55
  onRejectCallHandler();
@@ -1,11 +1,23 @@
1
1
  import { useEffect } from 'react';
2
- import { getVoipPushNotificationLib } from '../../utils/push/libs';
2
+ import {
3
+ getCallKeepLib,
4
+ getVoipPushNotificationLib,
5
+ } from '../../utils/push/libs';
3
6
 
4
- import { Platform } from 'react-native';
7
+ import { AppState, Platform } from 'react-native';
5
8
  import { StreamVideoRN } from '../../utils';
6
9
  import { useStreamVideoClient } from '@stream-io/video-react-bindings';
7
- import { voipPushNotificationCallCId$ } from '../../utils/push/rxSubjects';
8
10
  import { setPushLogoutCallback } from '../../utils/internal/pushLogoutCallback';
11
+ import { NativeModules } from 'react-native';
12
+ import {
13
+ canAddPushWSSubscriptionsRef,
14
+ shouldCallBeEnded,
15
+ } from '../../utils/push/utils';
16
+ import {
17
+ pushUnsubscriptionCallbacks$,
18
+ voipPushNotificationCallCId$,
19
+ } from '../../utils/push/rxSubjects';
20
+ import { RxUtils } from '@stream-io/video-client';
9
21
 
10
22
  let lastVoipToken: string | undefined = '';
11
23
 
@@ -91,7 +103,7 @@ export const useIosVoipPushEventsSetupEffect = () => {
91
103
  }, []);
92
104
  };
93
105
 
94
- const onNotificationReceived = (notification: any) => {
106
+ const onNotificationReceived = async (notification: any) => {
95
107
  /* --- Example payload ---
96
108
  {
97
109
  "aps": {
@@ -121,9 +133,65 @@ const onNotificationReceived = (notification: any) => {
121
133
  return;
122
134
  }
123
135
  const call_cid = notification?.stream?.call_cid;
124
- if (call_cid) {
125
- // send the info to this subject, it is listened by callkeep events
126
- // callkeep events will then accept/reject the call
127
- voipPushNotificationCallCId$.next(call_cid);
136
+ const pushConfig = StreamVideoRN.getConfig().push;
137
+ if (!call_cid || Platform.OS !== 'ios' || !pushConfig) {
138
+ return;
139
+ }
140
+ const client = await pushConfig.createStreamVideoClient();
141
+ if (!client) {
142
+ return;
143
+ }
144
+ const callFromPush = await client.onRingingCall(call_cid);
145
+ let uuid = '';
146
+ try {
147
+ uuid = await NativeModules?.StreamVideoReactNative?.getIncomingCallUUid(
148
+ call_cid,
149
+ );
150
+ } catch (error) {
151
+ console.log('Error in getting call uuid', error);
152
+ }
153
+ if (!uuid) {
154
+ return;
155
+ }
156
+ const created_by_id = notification?.stream?.created_by_id;
157
+ const receiver_id = notification?.stream?.receiver_id;
158
+ function closeCallIfNecessary() {
159
+ const { mustEndCall, callkeepReason } = shouldCallBeEnded(
160
+ callFromPush,
161
+ created_by_id,
162
+ receiver_id,
163
+ );
164
+ if (mustEndCall) {
165
+ const callkeep = getCallKeepLib();
166
+ callkeep.reportEndCallWithUUID(uuid, callkeepReason);
167
+ return true;
168
+ }
169
+ return false;
170
+ }
171
+ const closed = closeCallIfNecessary();
172
+ const canListenToWS = () =>
173
+ canAddPushWSSubscriptionsRef.current && AppState.currentState !== 'active';
174
+ if (!closed && canListenToWS()) {
175
+ const unsubscribe = callFromPush.on('all', () => {
176
+ if (!canListenToWS()) {
177
+ unsubscribe();
178
+ return;
179
+ }
180
+ const _closed = closeCallIfNecessary();
181
+ if (_closed) {
182
+ unsubscribe();
183
+ }
184
+ });
185
+ const unsubscriptionCallbacks =
186
+ RxUtils.getCurrentValue(pushUnsubscriptionCallbacks$) ?? [];
187
+ pushUnsubscriptionCallbacks$.next([
188
+ ...unsubscriptionCallbacks,
189
+ unsubscribe,
190
+ ]);
128
191
  }
192
+ // send the info to this subject, it is listened by callkeep events
193
+ // callkeep events will then accept/reject the call
194
+ voipPushNotificationCallCId$.next(call_cid);
195
+ const voipPushNotification = getVoipPushNotificationLib();
196
+ voipPushNotification.onVoipNotificationCompleted(uuid);
129
197
  };
@@ -1,4 +1,4 @@
1
- import { useCallStateHooks } from '@stream-io/video-react-bindings';
1
+ import { useCall, useCallStateHooks } from '@stream-io/video-react-bindings';
2
2
  import { useEffect, useRef } from 'react';
3
3
  import notifee, { AuthorizationStatus } from '@notifee/react-native';
4
4
  import { StreamVideoRN } from '../utils';
@@ -63,6 +63,7 @@ let isSetForegroundServiceRan = false;
63
63
  * This hook is used to keep the call alive in the background for Android.
64
64
  * It starts a foreground service to keep the call alive as soon as the call is joined
65
65
  * and stops the foreground Service when the call is left.
66
+ * Additonally: also responsible for cancelling any notifee displayed notification when the call has transitioned out of ringing
66
67
  */
67
68
  export const useAndroidKeepCallAliveEffect = () => {
68
69
  if (!isSetForegroundServiceRan && Platform.OS === 'android') {
@@ -71,6 +72,7 @@ export const useAndroidKeepCallAliveEffect = () => {
71
72
  }
72
73
  const foregroundServiceStartedRef = useRef(false);
73
74
 
75
+ const activeCallCid = useCall()?.cid;
74
76
  const { useCallCallingState } = useCallStateHooks();
75
77
  const callingState = useCallCallingState();
76
78
 
@@ -90,16 +92,25 @@ export const useAndroidKeepCallAliveEffect = () => {
90
92
  foregroundServiceStartedRef.current = true;
91
93
  };
92
94
  run();
95
+ } else if (callingState === CallingState.RINGING) {
96
+ // cancel any notifee displayed notification when the call has transitioned out of ringing
93
97
  return () => {
94
- if (!foregroundServiceStartedRef.current) {
95
- return;
98
+ if (activeCallCid) {
99
+ notifee.cancelDisplayedNotification(activeCallCid);
96
100
  }
97
- // stop foreground service when the call is not active
98
- stopForegroundService();
99
- foregroundServiceStartedRef.current = false;
100
101
  };
102
+ } else if (
103
+ callingState === CallingState.IDLE ||
104
+ callingState === CallingState.LEFT
105
+ ) {
106
+ if (!foregroundServiceStartedRef.current) {
107
+ return;
108
+ }
109
+ // stop foreground service when the call is not active
110
+ stopForegroundService();
111
+ foregroundServiceStartedRef.current = false;
101
112
  }
102
- }, [callingState]);
113
+ }, [activeCallCid, callingState]);
103
114
 
104
115
  useEffect(() => {
105
116
  return () => {
@@ -38,17 +38,17 @@ export const MediaStreamManagement = ({
38
38
  // Memoization is needed to avoid unnecessary useEffect triggers
39
39
  const targetResolutionSetting = useMemo<TargetResolution | undefined>(() => {
40
40
  if (
41
- settings?.video.target_resolution?.width === undefined ||
42
- settings?.video.target_resolution?.height === undefined ||
43
- settings?.video.target_resolution?.bitrate === undefined
41
+ settings?.video.target_resolution?.width !== undefined ||
42
+ settings?.video.target_resolution?.height !== undefined ||
43
+ settings?.video.target_resolution?.bitrate !== undefined
44
44
  ) {
45
- return undefined;
45
+ return {
46
+ width: settings?.video.target_resolution.width,
47
+ height: settings?.video.target_resolution.height,
48
+ bitrate: settings?.video.target_resolution.bitrate,
49
+ };
46
50
  }
47
- return {
48
- width: settings?.video.target_resolution.width,
49
- height: settings?.video.target_resolution.height,
50
- bitrate: settings?.video.target_resolution.bitrate,
51
- };
51
+ return undefined;
52
52
  }, [
53
53
  settings?.video.target_resolution.width,
54
54
  settings?.video.target_resolution.height,
@@ -82,25 +82,15 @@ export const MediaStreamManagement = ({
82
82
  * This is the object is used to track the initial audio/video enablement
83
83
  * Uses backend settings or the Prop to set initial audio/video enabled
84
84
  * Backend settings is applied only if the prop was undefined -- meaning user did not provide any value
85
+ * Memoization is needed to avoid unnecessary useEffect triggers
85
86
  */
86
87
  const { initialAudioEnabled, initialVideoEnabled } =
87
88
  useMemo<MediaDevicesInitialState>(() => {
88
- let _initialAudioEnabled: boolean | undefined;
89
- let _initialVideoEnabled: boolean | undefined;
90
- if (propInitialAudioEnabled !== undefined) {
91
- _initialAudioEnabled = propInitialAudioEnabled;
92
- } else if (settings?.audio.mic_default_on !== undefined) {
93
- _initialAudioEnabled = settings?.audio.mic_default_on;
94
- }
95
-
96
- if (propInitialVideoEnabled !== undefined) {
97
- _initialVideoEnabled = propInitialVideoEnabled;
98
- } else if (settings?.video.camera_default_on !== undefined) {
99
- _initialVideoEnabled = settings?.video.camera_default_on;
100
- }
101
89
  return {
102
- initialAudioEnabled: _initialAudioEnabled,
103
- initialVideoEnabled: _initialVideoEnabled,
90
+ initialAudioEnabled:
91
+ propInitialAudioEnabled ?? settings?.audio.mic_default_on,
92
+ initialVideoEnabled:
93
+ propInitialVideoEnabled ?? settings?.video.camera_default_on,
104
94
  };
105
95
  }, [
106
96
  settings?.audio.mic_default_on,
@@ -112,28 +102,29 @@ export const MediaStreamManagement = ({
112
102
  // The main logic
113
103
  // Enable or Disable the audio/video stream based on the initial state
114
104
  useEffect(() => {
115
- if (initialAudioEnabled === undefined) {
105
+ if (!call) {
116
106
  return;
117
107
  }
118
- if (initialVideoEnabled === undefined) {
119
- return;
108
+
109
+ if (initialAudioEnabled !== undefined) {
110
+ if (initialAudioEnabled) {
111
+ call.microphone.enable();
112
+ } else {
113
+ call.microphone.disable();
114
+ }
120
115
  }
116
+
121
117
  // we wait until we receive the resolution settings from the backend
122
- if (!call || !targetResolutionSetting) {
118
+ if (!targetResolutionSetting) {
123
119
  return;
124
120
  }
125
-
126
- if (initialAudioEnabled) {
127
- call.microphone.enable();
128
- } else {
129
- call.microphone.disable();
130
- }
131
-
132
121
  call.camera.selectTargetResolution(targetResolutionSetting);
133
- if (initialVideoEnabled) {
134
- call.camera.enable();
135
- } else {
136
- call.camera.disable();
122
+ if (initialVideoEnabled !== undefined) {
123
+ if (initialVideoEnabled) {
124
+ call.camera.enable();
125
+ } else {
126
+ call.camera.disable();
127
+ }
137
128
  }
138
129
  }, [call, initialAudioEnabled, initialVideoEnabled, targetResolutionSetting]);
139
130
 
@@ -1,12 +1,16 @@
1
1
  import { StreamCallProvider } from '@stream-io/video-react-bindings';
2
- import React, { PropsWithChildren } from 'react';
2
+ import React, { PropsWithChildren, useEffect } from 'react';
3
3
  import { Call } from '@stream-io/video-client';
4
- import { useAndroidKeepCallAliveEffect } from '../hooks';
5
4
  import { useIosCallkeepWithCallingStateEffect } from '../hooks/push/useIosCallkeepWithCallingStateEffect';
6
5
  import {
7
6
  MediaDevicesInitialState,
8
7
  MediaStreamManagement,
9
8
  } from './MediaStreamManagement';
9
+ import {
10
+ canAddPushWSSubscriptionsRef,
11
+ clearPushWSEventSubscriptions,
12
+ } from '../utils/push/utils';
13
+ import { useAndroidKeepCallAliveEffect } from '../hooks/useAndroidKeepCallAliveEffect';
10
14
 
11
15
  export type StreamCallProps = {
12
16
  /**
@@ -15,7 +19,9 @@ export type StreamCallProps = {
15
19
  */
16
20
  call: Call;
17
21
  /**
18
- * Provides the initial status of the media devices(audio/video) to the `MediaStreamManagement`.
22
+ * Optionally provide the initial status of the media devices(audio/video) to the `MediaStreamManagement`.
23
+ * Note: It will override the default state of the media devices set from the server side.
24
+ * It is used to control the initial state of the media devices(audio/video) in a custom lobby component.
19
25
  */
20
26
  mediaDeviceInitialState?: MediaDevicesInitialState;
21
27
  };
@@ -36,6 +42,7 @@ export const StreamCall = ({
36
42
  <MediaStreamManagement {...mediaDeviceInitialState}>
37
43
  <AndroidKeepCallAlive />
38
44
  <IosInformCallkeepCallEnd />
45
+ <ClearPushWSSubscriptions />
39
46
  {children}
40
47
  </MediaStreamManagement>
41
48
  </StreamCallProvider>
@@ -59,3 +66,18 @@ const IosInformCallkeepCallEnd = () => {
59
66
  useIosCallkeepWithCallingStateEffect();
60
67
  return null;
61
68
  };
69
+
70
+ /**
71
+ * This is a renderless component to clear all push ws event subscriptions
72
+ * and set whether push ws subscriptions can be added or not.
73
+ */
74
+ const ClearPushWSSubscriptions = () => {
75
+ useEffect(() => {
76
+ clearPushWSEventSubscriptions();
77
+ canAddPushWSSubscriptionsRef.current = false;
78
+ return () => {
79
+ canAddPushWSSubscriptionsRef.current = true;
80
+ };
81
+ }, []);
82
+ return null;
83
+ };
@@ -1,7 +1,7 @@
1
1
  import notifee, { EventType, Event } from '@notifee/react-native';
2
2
  import { FirebaseMessagingTypes } from '@react-native-firebase/messaging';
3
- import { StreamVideoClient } from '@stream-io/video-client';
4
- import { Platform } from 'react-native';
3
+ import { Call, RxUtils, StreamVideoClient } from '@stream-io/video-client';
4
+ import { AppState, Platform } from 'react-native';
5
5
  import type {
6
6
  NonRingingPushEvent,
7
7
  StreamVideoConfig,
@@ -16,8 +16,14 @@ import {
16
16
  pushRejectedIncomingCallCId$,
17
17
  pushTappedIncomingCallCId$,
18
18
  pushNonRingingCallData$,
19
+ pushUnsubscriptionCallbacks$,
19
20
  } from './rxSubjects';
20
- import { processCallFromPushInBackground } from './utils';
21
+ import {
22
+ canAddPushWSSubscriptionsRef,
23
+ clearPushWSEventSubscriptions,
24
+ processCallFromPushInBackground,
25
+ shouldCallBeEnded,
26
+ } from './utils';
21
27
  import { setPushLogoutCallback } from '../internal/pushLogoutCallback';
22
28
  import { getAndroidDefaultRingtoneUrl } from '../getAndroidDefaultRingtoneUrl';
23
29
 
@@ -153,6 +159,54 @@ const firebaseMessagingOnMessageHandler = async (
153
159
  }
154
160
 
155
161
  if (data.type === 'call.ring') {
162
+ const call_cid = data.call_cid;
163
+ const created_by_id = data.created_by_id;
164
+ const receiver_id = data.receiver_id;
165
+
166
+ function shouldCallBeClosed(callToCheck: Call) {
167
+ const { mustEndCall } = shouldCallBeEnded(
168
+ callToCheck,
169
+ created_by_id,
170
+ receiver_id,
171
+ );
172
+ return mustEndCall;
173
+ }
174
+
175
+ const canListenToWS = () =>
176
+ canAddPushWSSubscriptionsRef.current &&
177
+ AppState.currentState !== 'active';
178
+ const asForegroundService = canListenToWS();
179
+
180
+ if (asForegroundService) {
181
+ // Listen to call events from WS through fg service
182
+ // note: this will replace the current empty fg service runner
183
+ notifee.registerForegroundService(() => {
184
+ return new Promise(async () => {
185
+ const client = await pushConfig.createStreamVideoClient();
186
+ if (!client) {
187
+ notifee.stopForegroundService();
188
+ return;
189
+ }
190
+ const callFromPush = await client.onRingingCall(call_cid);
191
+ if (shouldCallBeClosed(callFromPush)) {
192
+ notifee.stopForegroundService();
193
+ return;
194
+ }
195
+ const unsubscribe = callFromPush.on('all', () => {
196
+ if (!canListenToWS() || shouldCallBeClosed(callFromPush)) {
197
+ unsubscribe();
198
+ notifee.stopForegroundService();
199
+ }
200
+ });
201
+ const unsubscriptionCallbacks =
202
+ RxUtils.getCurrentValue(pushUnsubscriptionCallbacks$) ?? [];
203
+ pushUnsubscriptionCallbacks$.next([
204
+ ...unsubscriptionCallbacks,
205
+ unsubscribe,
206
+ ]);
207
+ });
208
+ });
209
+ }
156
210
  const incomingCallChannel = pushConfig.android.incomingCallChannel;
157
211
  const incomingCallNotificationTextGetters =
158
212
  pushConfig.android.incomingCallNotificationTextGetters;
@@ -172,12 +226,13 @@ const firebaseMessagingOnMessageHandler = async (
172
226
 
173
227
  const channelId = incomingCallChannel.id;
174
228
  await notifee.displayNotification({
175
- id: data.call_cid,
229
+ id: call_cid,
176
230
  title: getTitle(createdUserName),
177
231
  body: getBody(createdUserName),
178
232
  data,
179
233
  android: {
180
234
  channelId,
235
+ asForegroundService,
181
236
  sound: incomingCallChannel.sound,
182
237
  vibrationPattern: incomingCallChannel.vibrationPattern,
183
238
  pressAction: {
@@ -202,6 +257,23 @@ const firebaseMessagingOnMessageHandler = async (
202
257
  timeoutAfter: 60000, // 60 seconds, after which the notification will be dismissed automatically
203
258
  },
204
259
  });
260
+
261
+ // check if call needs to be closed if accept/decline event was done
262
+ // before the notification was shown
263
+ const client = await pushConfig.createStreamVideoClient();
264
+ if (!client) {
265
+ return;
266
+ }
267
+ const callFromPush = await client.onRingingCall(call_cid);
268
+
269
+ if (shouldCallBeClosed(callFromPush)) {
270
+ if (asForegroundService) {
271
+ notifee.stopForegroundService();
272
+ } else {
273
+ notifee.cancelDisplayedNotification(call_cid);
274
+ }
275
+ return;
276
+ }
205
277
  } else {
206
278
  // the other types are call.live_started and call.notification
207
279
  const callChannel = pushConfig.android.callChannel;
@@ -272,6 +344,12 @@ const onNotifeeEvent = async (event: Event, pushConfig: PushConfig) => {
272
344
  const mustAccept =
273
345
  type === EventType.ACTION_PRESS &&
274
346
  pressAction.id === ACCEPT_CALL_ACTION_ID;
347
+
348
+ if (mustAccept || mustDecline || type === EventType.ACTION_PRESS) {
349
+ clearPushWSEventSubscriptions();
350
+ notifee.stopForegroundService();
351
+ }
352
+
275
353
  if (mustAccept) {
276
354
  pushAcceptedIncomingCallCId$.next(call_cid);
277
355
  // NOTE: accept will be handled by the app with rxjs observers as the app will go to foreground always
@@ -10,7 +10,10 @@ import {
10
10
  voipCallkeepAcceptedCallOnNativeDialerMap$,
11
11
  pushNonRingingCallData$,
12
12
  } from './rxSubjects';
13
- import { processCallFromPushInBackground } from './utils';
13
+ import {
14
+ clearPushWSEventSubscriptions,
15
+ processCallFromPushInBackground,
16
+ } from './utils';
14
17
  import { getExpoNotificationsLib, getPushNotificationIosLib } from './libs';
15
18
  import { StreamVideoClient } from '@stream-io/video-client';
16
19
  import { setPushLogoutCallback } from '../internal/pushLogoutCallback';
@@ -47,6 +50,7 @@ export const iosCallkeepAcceptCall = (
47
50
  if (!shouldProcessCallFromCallkeep(call_cid, callUUIDFromCallkeep)) {
48
51
  return;
49
52
  }
53
+ clearPushWSEventSubscriptions();
50
54
  // to call end callkeep later if ended in app and not through callkeep
51
55
  voipCallkeepAcceptedCallOnNativeDialerMap$.next({
52
56
  uuid: callUUIDFromCallkeep,
@@ -66,6 +70,7 @@ export const iosCallkeepRejectCall = async (
66
70
  if (!shouldProcessCallFromCallkeep(call_cid, callUUIDFromCallkeep)) {
67
71
  return;
68
72
  }
73
+ clearPushWSEventSubscriptions();
69
74
  // no need to keep these references anymore
70
75
  voipCallkeepAcceptedCallOnNativeDialerMap$.next(undefined);
71
76
  voipCallkeepCallOnForegroundMap$.next(undefined);