@stream-io/react-native-callingx 0.1.1-beta.1 → 0.1.1

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.
package/CHANGELOG.md ADDED
@@ -0,0 +1,15 @@
1
+ # Changelog
2
+
3
+ This file was generated using [@jscutlery/semver](https://github.com/jscutlery/semver).
4
+
5
+ ## [0.1.1](https://github.com/GetStream/stream-video-js/compare/@stream-io/react-native-callingx-0.1.0...@stream-io/react-native-callingx-0.1.1) (2026-04-09)
6
+
7
+ ### Bug Fixes
8
+
9
+ - callingx docs update ([#2195](https://github.com/GetStream/stream-video-js/issues/2195)) ([7a6b632](https://github.com/GetStream/stream-video-js/commit/7a6b632270ec1187236a0e4e5c5396a98a20fd16))
10
+
11
+ ## 0.1.0 (2026-04-09)
12
+
13
+ ### Features
14
+
15
+ - callkit/telecom integration ([#2028](https://github.com/GetStream/stream-video-js/issues/2028)) ([d579acd](https://github.com/GetStream/stream-video-js/commit/d579acd1975fb4945e40452b27e372694c737628))
package/README.md CHANGED
@@ -1,395 +1,83 @@
1
- # react-native-callingx
1
+ # @stream-io/react-native-callingx
2
2
 
3
- A React Native Turbo Module for seamless native calling integration. This library provides a unified API to integrate with **CallKit** on iOS and the **Telecom/ConnectionService** API on Android, enabling your app to display system-level calling UI and interact with native call controls.
3
+ React Native native-calling bridge for:
4
4
 
5
- ## Features
5
+ - iOS CallKit
6
+ - Android Telecom/ConnectionService
6
7
 
7
- - 📞 **Incoming call UI** — Display native incoming call screens (even when the app is killed)
8
- - 📲 **Outgoing call registration** — Register outgoing calls with the system
9
- - 🎛️ **Call controls** — Mute, hold, end calls with native system integration
10
- - 🔔 **Custom notifications** — Configurable Android notification channels
11
- - ⚡ **Turbo Module** — Built with the New Architecture for optimal performance
12
- - 📱 **Background support** — Handle calls when the app is backgrounded or killed
13
-
14
- ## Requirements
15
-
16
- - React Native 0.73+ (New Architecture / Turbo Modules)
17
- - iOS 13.0+
18
- - Android API 26+ (Android 8.0 Oreo)
19
-
20
- ## Installation
8
+ ## Install
21
9
 
22
10
  ```sh
23
- npm install @stream-io/react-native-callingx
24
- # or
25
11
  yarn add @stream-io/react-native-callingx
26
12
  ```
27
13
 
28
- ### iOS Setup
29
-
30
- 1. Add the required background modes to your `Info.plist`:
31
-
32
- ```xml
33
- <key>UIBackgroundModes</key>
34
- <array>
35
- <string>voip</string>
36
- <string>audio</string>
37
- </array>
38
- ```
39
-
40
- 2. Run pod install:
14
+ Then run iOS pods:
41
15
 
42
16
  ```sh
43
17
  cd ios && pod install
44
18
  ```
45
19
 
46
- 3. For VoIP push notifications, configure your `AppDelegate` to report incoming calls:
47
-
48
- ```objc
49
- #import <Callingx/CallingxPublic.h>
50
-
51
- - (void)pushRegistry:(PKPushRegistry *)registry
52
- didReceiveIncomingPushWithPayload:(PKPushPayload *)payload
53
- forType:(PKPushType)type
54
- withCompletionHandler:(void (^)(void))completion {
55
-
56
- // Extract call information from payload
57
- NSString *callId = payload.dictionaryPayload[@"call_id"];
58
- NSString *callerName = payload.dictionaryPayload[@"caller_name"];
59
- NSString *handle = payload.dictionaryPayload[@"handle"];
60
- BOOL hasVideo = [payload.dictionaryPayload[@"has_video"] boolValue];
61
-
62
- [Callingx reportNewIncomingCall:callId
63
- handle:handle
64
- handleType:@"generic"
65
- hasVideo:hasVideo
66
- localizedCallerName:callerName
67
- supportsHolding:YES
68
- supportsDTMF:NO
69
- supportsGrouping:NO
70
- supportsUngrouping:NO
71
- payload:payload.dictionaryPayload
72
- withCompletionHandler:completion];
73
- }
74
- ```
75
-
76
- ### Android Setup
20
+ ## Quick start
77
21
 
78
- ## Usage
22
+ ```ts
23
+ import { CallingxModule } from '@stream-io/react-native-callingx';
79
24
 
80
- ### Setup
81
-
82
- Initialize the module with platform-specific configuration:
83
-
84
- ```typescript
85
- import { CallingxModule } from 'react-native-callingx';
86
-
87
- // Setup must be called before any other method
88
25
  CallingxModule.setup({
89
26
  ios: {
90
- appName: 'My App',
91
27
  supportsVideo: true,
92
- maximumCallsPerCallGroup: 1,
93
- maximumCallGroups: 1,
94
- handleType: 'generic', // 'generic' | 'number' | 'phone' | 'email'
28
+ callsHistory: false,
95
29
  },
96
30
  android: {
97
31
  incomingChannel: {
98
- id: 'incoming_calls',
99
- name: 'Incoming Calls',
100
- sound: 'ringtone', // optional custom sound
101
- vibration: true,
32
+ id: 'incoming_calls_channel',
33
+ name: 'Incoming calls',
102
34
  },
103
- outgoingChannel: {
104
- id: 'ongoing_calls',
105
- name: 'Ongoing Calls',
106
- },
107
- // Optional: transform display text
108
- titleTransformer: (name) => `Call from ${name}`,
109
- subtitleTransformer: (phoneNumber) => phoneNumber,
110
35
  },
111
36
  });
112
- ```
113
-
114
- ### Request Permissions
115
37
 
116
- Before displaying calls, request the required permissions:
117
-
118
- ```typescript
119
- const permissions = await CallingxModule.requestPermissions();
120
- console.log('Audio permission:', permissions.recordAudio);
121
- console.log('Notification permission:', permissions.postNotifications);
122
- ```
123
-
124
- ### Display Incoming Call
125
-
126
- Show the native incoming call UI:
127
-
128
- ```typescript
129
38
  await CallingxModule.displayIncomingCall(
130
- 'unique-call-id',
131
- '+1234567890', // phone number / handle
132
- 'John Doe', // caller name
133
- true, // has video
134
- );
135
- ```
136
-
137
- ### Start Outgoing Call
138
-
139
- Register an outgoing call with the system:
140
-
141
- ```typescript
142
- await CallingxModule.startCall(
143
- 'unique-call-id',
144
- '+1234567890',
39
+ 'call-id',
40
+ '+123456789',
145
41
  'John Doe',
146
- false, // audio only
42
+ true,
147
43
  );
148
44
  ```
149
45
 
150
- ### Answer Call
151
-
152
- Answer an incoming call programmatically:
153
-
154
- ```typescript
155
- await CallingxModule.answerIncomingCall('unique-call-id');
156
- ```
157
-
158
- ### Activate Call
159
-
160
- Mark a call as active (connected):
161
-
162
- ```typescript
163
- await CallingxModule.setCurrentCallActive('unique-call-id');
164
- ```
165
-
166
- ### End Call
167
-
168
- End a call with a specific reason:
169
-
170
- ```typescript
171
- import type { EndCallReason } from 'react-native-callingx';
172
-
173
- // Available reasons:
174
- // 'local' | 'remote' | 'rejected' | 'busy' | 'answeredElsewhere' |
175
- // 'missed' | 'error' | 'canceled' | 'restricted' | 'unknown'
176
- await CallingxModule.endCallWithReason('unique-call-id', 'remote');
177
- ```
178
-
179
- ### Mute/Unmute
180
-
181
- Toggle call mute state:
182
-
183
- ```typescript
184
- await CallingxModule.setMutedCall('unique-call-id', true); // mute
185
- await CallingxModule.setMutedCall('unique-call-id', false); // unmute
186
- ```
187
-
188
- ### Hold/Unhold
189
-
190
- Toggle call hold state:
191
-
192
- ```typescript
193
- await CallingxModule.setOnHoldCall('unique-call-id', true); // hold
194
- await CallingxModule.setOnHoldCall('unique-call-id', false); // unhold
195
- ```
196
-
197
- ### Update Display
198
-
199
- Update the caller information during a call:
200
-
201
- ```typescript
202
- await CallingxModule.updateDisplay(
203
- 'unique-call-id',
204
- '+1234567890',
205
- 'Updated Name',
206
- );
207
- ```
208
-
209
- ### Event Listeners
210
-
211
- Subscribe to call events:
212
-
213
- ```typescript
214
- import { CallingxModule } from 'react-native-callingx';
215
- import type { EventName } from 'react-native-callingx';
216
-
217
- // Answer event - user answered from system UI
218
- const answerSubscription = CallingxModule.addEventListener(
219
- 'answerCall',
220
- (params) => {
221
- console.log('Call answered:', params.callId);
222
- },
223
- );
224
-
225
- // End event - call ended
226
- const endSubscription = CallingxModule.addEventListener('endCall', (params) => {
227
- console.log('Call ended:', params.callId, 'Cause:', params.cause);
228
- });
229
-
230
- // Hold toggle event
231
- const holdSubscription = CallingxModule.addEventListener(
232
- 'didToggleHoldCallAction',
233
- (params) => {
234
- console.log('Hold toggled:', params.callId, 'On hold:', params.hold);
235
- },
236
- );
237
-
238
- // Mute toggle event
239
- const muteSubscription = CallingxModule.addEventListener(
240
- 'didPerformSetMutedCallAction',
241
- (params) => {
242
- console.log('Mute toggled:', params.callId, 'Muted:', params.muted);
243
- },
244
- );
245
-
246
- // Start call action (outgoing call initiated from system)
247
- const startSubscription = CallingxModule.addEventListener(
248
- 'didReceiveStartCallAction',
249
- (params) => {
250
- console.log('Start call action:', params.callId);
251
- },
252
- );
253
-
254
- // Clean up when done
255
- answerSubscription.remove();
256
- endSubscription.remove();
257
- // ... remove other subscriptions
258
- ```
259
-
260
- ### Handle Initial Events
261
-
262
- When the app is launched from a killed state by a call action, retrieve queued events:
263
-
264
- ```typescript
265
- // Get events that occurred before the module was initialized
266
- const initialEvents = CallingxModule.getInitialEvents();
267
- initialEvents.forEach((event) => {
268
- console.log('Initial event:', event.eventName, event.params);
269
- });
270
-
271
- // Clear initial events after processing
272
- await CallingxModule.clearInitialEvents();
273
- ```
274
-
275
- ### Background Tasks (Android)
276
-
277
- Run background tasks for call-related operations:
278
-
279
- ```typescript
280
- // Start a managed background task
281
- await CallingxModule.startBackgroundTask(async (taskData, stopTask) => {
282
- try {
283
- // Perform background work (e.g., connect to call server)
284
- await connectToCallServer();
285
- } finally {
286
- stopTask(); // Always call when done
287
- }
288
- });
289
-
290
- // Or stop manually
291
- await CallingxModule.stopBackgroundTask();
292
- ```
293
-
294
- ## API Reference
295
-
296
- ### CallingxModule
297
-
298
- | Method | Description |
299
- | ---------------------------------------------------------------- | ---------------------------------------------------- |
300
- | `setup(options)` | Initialize the module with platform-specific options |
301
- | `requestPermissions()` | Request required permissions (audio, notifications) |
302
- | `checkPermissions()` | Check current permission status |
303
- | `displayIncomingCall(callId, phoneNumber, callerName, hasVideo)` | Display incoming call UI |
304
- | `answerIncomingCall(callId)` | Answer an incoming call |
305
- | `startCall(callId, phoneNumber, callerName, hasVideo)` | Register an outgoing call |
306
- | `setCurrentCallActive(callId)` | Mark call as active/connected |
307
- | `updateDisplay(callId, phoneNumber, callerName)` | Update caller display info |
308
- | `endCallWithReason(callId, reason)` | End call with specified reason |
309
- | `setMutedCall(callId, isMuted)` | Toggle call mute state |
310
- | `setOnHoldCall(callId, isOnHold)` | Toggle call hold state |
311
- | `addEventListener(eventName, callback)` | Subscribe to call events |
312
- | `getInitialEvents()` | Get queued events from app launch |
313
- | `clearInitialEvents()` | Clear queued initial events |
314
- | `startBackgroundTask(taskProvider)` | Start Android background task |
315
- | `stopBackgroundTask()` | Stop Android background task |
316
- | `log(message, level)` | Log message to native console |
317
-
318
- ### Events
319
-
320
- | Event | Parameters | Description |
321
- | ------------------------------ | ------------------- | --------------------------------- |
322
- | `answerCall` | `{ callId }` | User answered call from system UI |
323
- | `endCall` | `{ callId, cause }` | Call ended |
324
- | `didToggleHoldCallAction` | `{ callId, hold }` | Hold state changed |
325
- | `didPerformSetMutedCallAction` | `{ callId, muted }` | Mute state changed |
326
- | `didReceiveStartCallAction` | `{ callId }` | Outgoing call action received |
327
-
328
- ### Types
329
-
330
- ```typescript
331
- type EndCallReason =
332
- | 'local' // Call ended by the local user (e.g., hanging up)
333
- | 'remote' // Call ended by the remote party, or outgoing not answered
334
- | 'rejected' // Call was rejected/declined
335
- | 'busy' // Remote party was busy
336
- | 'answeredElsewhere' // Answered on another device
337
- | 'missed' // No response to an incoming call
338
- | 'error' // Call failed due to an error (e.g., network issue)
339
- | 'canceled' // Call canceled before the remote party could answer
340
- | 'restricted' // Call restricted (e.g., airplane mode)
341
- | 'unknown'; // Unknown or unspecified disconnect reason
342
-
343
- type CallingExpiOSOptions = {
344
- appName: string;
345
- supportsVideo?: boolean;
346
- maximumCallsPerCallGroup?: number;
347
- maximumCallGroups?: number;
348
- handleType?: 'generic' | 'number' | 'phone' | 'email';
349
- };
350
-
351
- type CallingExpAndroidOptions = {
352
- incomingChannel?: {
353
- id: string;
354
- name: string;
355
- sound?: string;
356
- vibration?: boolean;
357
- };
358
- outgoingChannel?: {
359
- id: string;
360
- name: string;
361
- sound?: string;
362
- vibration?: boolean;
363
- };
364
- };
365
-
366
- type PermissionsResult = {
367
- recordAudio: boolean;
368
- postNotifications: boolean;
369
- };
370
- ```
371
-
372
- ## Troubleshooting
373
-
374
- ### iOS
375
-
376
- - **Incoming call not showing**: Ensure `voip` background mode is enabled and VoIP push certificate is configured
377
- - **CallKit errors**: Check that `appName` is set in setup options
378
- - **Audio issues**: The module automatically configures the audio session, but ensure no conflicts with other audio libraries
46
+ ## Main APIs
379
47
 
380
- ### Android
48
+ - `setup(options)` - required before any call action.
49
+ - `displayIncomingCall(callId, phoneNumber, callerName, hasVideo)`.
50
+ - `startCall(callId, phoneNumber, callerName, hasVideo)`.
51
+ - `setCurrentCallActive(callId)`.
52
+ - `updateDisplay(callId, phoneNumber, callerName, incoming)`.
53
+ - `endCallWithReason(callId, reason)`.
54
+ - `setMutedCall(callId, isMuted)`.
55
+ - `setOnHoldCall(callId, isOnHold)`.
56
+ - `addEventListener(eventName, callback)`.
57
+ - `getInitialEvents()` and `getInitialVoipEvents()`.
58
+ - `registerBackgroundTask(taskProvider)` / `startBackgroundTask()` / `stopBackgroundTask()` (Android).
381
59
 
382
- - **Notifications not showing**: Check POST_NOTIFICATIONS permission on Android 13+
383
- - **Call not answered on tap**: Ensure `handleCallingIntent` is called in both `onCreate` and `onNewIntent` in your MainActivity
60
+ ## Event names
384
61
 
385
- ## Contributing
62
+ Call events:
386
63
 
387
- See the [contributing guide](CONTRIBUTING.md) to learn how to contribute to the repository and the development workflow.
64
+ - `answerCall`
65
+ - `endCall`
66
+ - `didDisplayIncomingCall`
67
+ - `didToggleHoldCallAction`
68
+ - `didPerformSetMutedCallAction`
69
+ - `didChangeAudioRoute`
70
+ - `didReceiveStartCallAction`
71
+ - `didActivateAudioSession`
72
+ - `didDeactivateAudioSession`
388
73
 
389
- ## License
74
+ VoIP events:
390
75
 
391
- MIT
76
+ - `voipNotificationsRegistered`
77
+ - `voipNotificationReceived`
392
78
 
393
- ---
79
+ ## Notes
394
80
 
395
- Made with [create-react-native-library](https://github.com/callstack/react-native-builder-bob)
81
+ - Import from `@stream-io/react-native-callingx`.
82
+ - iOS-only helpers: `registerVoipToken`, `fulfillAnswerCallAction`, `fulfillEndCallAction`.
83
+ - Android helpers: `canPostNotifications`, `isOngoingCallsEnabled`.
@@ -48,6 +48,8 @@ class CallService : Service(), CallRepository.Listener {
48
48
  companion object {
49
49
  private const val TAG = "[Callingx] CallService"
50
50
 
51
+ internal const val DEFAULT_DISPLAY_NAME = "Unknown Caller"
52
+
51
53
  internal const val EXTRA_CALL_ID = "extra_call_id"
52
54
  internal const val EXTRA_NAME = "extra_name"
53
55
  internal const val EXTRA_URI = "extra_uri"
@@ -104,8 +106,12 @@ class CallService : Service(), CallRepository.Listener {
104
106
  )
105
107
  return
106
108
  }
109
+
110
+ val createdById = data["created_by_id"]
111
+ val createdName = data["created_by_display_name"].orEmpty()
112
+ val displayName = data["call_display_name"].orEmpty()
113
+ val callDisplayName = displayName.ifEmpty { createdName.ifEmpty { DEFAULT_DISPLAY_NAME } }
107
114
 
108
- val callName = data["created_by_display_name"].orEmpty()
109
115
  val isVideo = data["video"] == "true"
110
116
 
111
117
  CallRegistrationStore.trackCallRegistration(callCid, null)
@@ -114,8 +120,8 @@ class CallService : Service(), CallRepository.Listener {
114
120
  Intent(context, CallService::class.java).apply {
115
121
  action = ACTION_INCOMING_CALL
116
122
  putExtra(EXTRA_CALL_ID, callCid)
117
- putExtra(EXTRA_URI, callCid.toUri())
118
- putExtra(EXTRA_NAME, callName)
123
+ putExtra(EXTRA_URI, createdById?.toUri() ?: callDisplayName.toUri())
124
+ putExtra(EXTRA_NAME, callDisplayName)
119
125
  putExtra(EXTRA_IS_VIDEO, isVideo)
120
126
  }
121
127
 
@@ -317,25 +317,19 @@ class CallNotificationManager(
317
317
  if (call.isIncoming() && !call.isActive && optimisticState == OptimisticState.NONE) {
318
318
  return NotificationCompat.CallStyle.forIncomingCall(
319
319
  caller,
320
- NotificationIntentFactory.getPendingBroadcastIntent(
321
- context,
322
- CallingxModuleImpl.CALL_END_ACTION,
323
- call.id
324
- ) {
325
- putExtra(
326
- CallingxModuleImpl.EXTRA_DISCONNECT_CAUSE,
327
- getDisconnectCauseString(DisconnectCause(DisconnectCause.REJECTED))
328
- )
329
- putExtra(
330
- CallingxModuleImpl.EXTRA_SOURCE,
331
- CallRepository.EventSource.SYS.name.lowercase()
332
- )
333
- },
320
+ NotificationIntentFactory.getPendingNotificationIntent(
321
+ context,
322
+ CallingxModuleImpl.CALL_END_ACTION,
323
+ call.id,
324
+ CallRepository.EventSource.SYS.name.lowercase(),
325
+ false
326
+ ),
334
327
  NotificationIntentFactory.getPendingNotificationIntent(
335
328
  context,
336
329
  CallingxModuleImpl.CALL_ANSWERED_ACTION,
337
330
  call.id,
338
- CallRepository.EventSource.SYS.name.lowercase()
331
+ CallRepository.EventSource.SYS.name.lowercase(),
332
+ true
339
333
  )
340
334
  )
341
335
  }
@@ -22,10 +22,11 @@ object NotificationIntentFactory {
22
22
  context: Context,
23
23
  action: String,
24
24
  callId: String,
25
- source: String
25
+ source: String,
26
+ includeLaunchActivity: Boolean
26
27
  ): PendingIntent {
27
28
  return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
28
- getReceiverActivityIntent(context, action, callId, source)
29
+ getReceiverActivityIntent(context, action, callId, source, includeLaunchActivity)
29
30
  } else {
30
31
  getPendingServiceIntent(context, action, callId, source)
31
32
  }
@@ -47,7 +48,7 @@ object NotificationIntentFactory {
47
48
  )
48
49
  }
49
50
 
50
- fun getReceiverActivityIntent(context: Context, action: String, callId: String, source: String): PendingIntent {
51
+ fun getReceiverActivityIntent(context: Context, action: String, callId: String, source: String, includeLaunchActivity: Boolean): PendingIntent {
51
52
  val receiverIntent =
52
53
  Intent(context, NotificationReceiverActivity::class.java).apply {
53
54
  this.action = action
@@ -57,14 +58,25 @@ object NotificationIntentFactory {
57
58
 
58
59
  val launchActivity = context.packageManager.getLaunchIntentForPackage(context.packageName)
59
60
  val launchActivityIntent =
60
- Intent(launchActivity).apply {
61
- addFlags(Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TOP)
61
+ launchActivity?.let { base ->
62
+ Intent(base).apply {
63
+ addFlags(Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TOP)
64
+ }
65
+ }
66
+
67
+ // intents are started in order and build a synthetic back stack
68
+ // the last intent is the one on top, so the launch activity should come first
69
+ val intents =
70
+ if (includeLaunchActivity && launchActivityIntent != null) {
71
+ arrayOf(launchActivityIntent, receiverIntent)
72
+ } else {
73
+ arrayOf(receiverIntent)
62
74
  }
63
75
 
64
76
  return PendingIntent.getActivities(
65
77
  context,
66
78
  requestCodeFor(callId, REQUEST_CODE_RECEIVER_ACTIVITY),
67
- arrayOf(launchActivityIntent, receiverIntent),
79
+ intents,
68
80
  PendingIntent.FLAG_MUTABLE or PendingIntent.FLAG_UPDATE_CURRENT
69
81
  )
70
82
  }
@@ -3,8 +3,10 @@ package io.getstream.rn.callingx.notifications
3
3
  import android.app.Activity
4
4
  import android.content.Intent
5
5
  import android.os.Bundle
6
+ import android.telecom.DisconnectCause
6
7
  import io.getstream.rn.callingx.CallingxModuleImpl
7
8
  import io.getstream.rn.callingx.debugLog
9
+ import io.getstream.rn.callingx.getDisconnectCauseString
8
10
 
9
11
  // For Android 12+
10
12
  class NotificationReceiverActivity : Activity() {
@@ -21,33 +23,54 @@ class NotificationReceiverActivity : Activity() {
21
23
  finish()
22
24
  }
23
25
 
24
- //re-send intent from notification to the turbo module
25
26
  private fun handleIntent(intent: Intent?) {
26
- if (intent == null) {
27
- return
27
+ val nonNullIntent = intent ?: return
28
+ val action = nonNullIntent.action ?: return
29
+
30
+ when (action) {
31
+ CallingxModuleImpl.CALL_ANSWERED_ACTION -> onCallAnswered(nonNullIntent)
32
+ CallingxModuleImpl.CALL_END_ACTION -> onCallEnded(nonNullIntent)
28
33
  }
34
+ }
29
35
 
30
- if (intent.action == CallingxModuleImpl.CALL_ANSWERED_ACTION) {
31
- debugLog("[Callingx] NotificationReceiverActivity", "[receiver] answered call action")
32
- val callId = intent.getStringExtra(CallingxModuleImpl.EXTRA_CALL_ID)
33
- val source = intent.getStringExtra(CallingxModuleImpl.EXTRA_SOURCE)
34
-
35
- if (callId != null) {
36
- Intent(CallingxModuleImpl.CALL_OPTIMISTIC_ACCEPT_ACTION)
37
- .apply {
38
- setPackage(packageName)
39
- putExtra(CallingxModuleImpl.EXTRA_CALL_ID, callId)
40
- }
41
- .also { sendBroadcast(it) }
42
- }
36
+ private fun onCallAnswered(intent: Intent) {
37
+ debugLog("[Callingx] NotificationReceiverActivity", "[receiver] answered call action")
38
+ val callId = intent.getStringExtra(CallingxModuleImpl.EXTRA_CALL_ID)
39
+ val source = intent.getStringExtra(CallingxModuleImpl.EXTRA_SOURCE)
43
40
 
44
- Intent(CallingxModuleImpl.CALL_ANSWERED_ACTION)
41
+ if (callId != null) {
42
+ Intent(CallingxModuleImpl.CALL_OPTIMISTIC_ACCEPT_ACTION)
45
43
  .apply {
46
44
  setPackage(packageName)
47
45
  putExtra(CallingxModuleImpl.EXTRA_CALL_ID, callId)
48
- putExtra(CallingxModuleImpl.EXTRA_SOURCE, source)
49
46
  }
50
47
  .also { sendBroadcast(it) }
51
48
  }
49
+
50
+ Intent(CallingxModuleImpl.CALL_ANSWERED_ACTION)
51
+ .apply {
52
+ setPackage(packageName)
53
+ putExtra(CallingxModuleImpl.EXTRA_CALL_ID, callId)
54
+ putExtra(CallingxModuleImpl.EXTRA_SOURCE, source)
55
+ }
56
+ .also { sendBroadcast(it) }
57
+ }
58
+
59
+ private fun onCallEnded(intent: Intent) {
60
+ debugLog("[Callingx] NotificationReceiverActivity", "[receiver] rejected call action")
61
+ val callId = intent.getStringExtra(CallingxModuleImpl.EXTRA_CALL_ID)
62
+ val source = intent.getStringExtra(CallingxModuleImpl.EXTRA_SOURCE)
63
+
64
+ Intent(CallingxModuleImpl.CALL_END_ACTION)
65
+ .apply {
66
+ setPackage(packageName)
67
+ putExtra(CallingxModuleImpl.EXTRA_CALL_ID, callId)
68
+ putExtra(CallingxModuleImpl.EXTRA_SOURCE, source)
69
+ putExtra(
70
+ CallingxModuleImpl.EXTRA_DISCONNECT_CAUSE,
71
+ getDisconnectCauseString(DisconnectCause(DisconnectCause.REJECTED))
72
+ )
73
+ }
74
+ .also { sendBroadcast(it) }
52
75
  }
53
76
  }
@@ -3,8 +3,10 @@ package io.getstream.rn.callingx.notifications
3
3
  import android.app.Service
4
4
  import android.content.Intent
5
5
  import android.os.IBinder
6
+ import android.telecom.DisconnectCause
6
7
  import android.util.Log
7
8
  import io.getstream.rn.callingx.CallingxModuleImpl
9
+ import io.getstream.rn.callingx.getDisconnectCauseString
8
10
 
9
11
  class NotificationReceiverService : Service() {
10
12
 
@@ -23,6 +25,7 @@ class NotificationReceiverService : Service() {
23
25
 
24
26
  when (action) {
25
27
  CallingxModuleImpl.CALL_ANSWERED_ACTION -> onCallAnswered(intent)
28
+ CallingxModuleImpl.CALL_END_ACTION -> onCallEnded(intent)
26
29
  }
27
30
 
28
31
  stopSelf(startId)
@@ -60,4 +63,25 @@ class NotificationReceiverService : Service() {
60
63
  }
61
64
  }
62
65
  }
66
+
67
+ /** Mirrors [NotificationReceiverActivity] on API levels below 33 where the service receives notification actions. */
68
+ private fun onCallEnded(intent: Intent) {
69
+ val callId = intent.getStringExtra(CallingxModuleImpl.EXTRA_CALL_ID)
70
+ val source = intent.getStringExtra(CallingxModuleImpl.EXTRA_SOURCE)
71
+ try {
72
+ Intent(CallingxModuleImpl.CALL_END_ACTION)
73
+ .apply {
74
+ setPackage(packageName)
75
+ putExtra(CallingxModuleImpl.EXTRA_CALL_ID, callId)
76
+ putExtra(CallingxModuleImpl.EXTRA_SOURCE, source)
77
+ putExtra(
78
+ CallingxModuleImpl.EXTRA_DISCONNECT_CAUSE,
79
+ getDisconnectCauseString(DisconnectCause(DisconnectCause.REJECTED))
80
+ )
81
+ }
82
+ .also { sendBroadcast(it) }
83
+ } catch (e: Exception) {
84
+ Log.e(TAG, "Error sending call end intent", e)
85
+ }
86
+ }
63
87
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@stream-io/react-native-callingx",
3
- "version": "0.1.1-beta.1",
3
+ "version": "0.1.1",
4
4
  "description": "CallKit and Telecom API capabilities for React Native",
5
5
  "main": "./dist/module/index.js",
6
6
  "module": "./dist/module/index.js",
@@ -61,7 +61,7 @@
61
61
  "devDependencies": {
62
62
  "@react-native-community/cli": "20.0.1",
63
63
  "@react-native/babel-preset": "^0.81.5",
64
- "@stream-io/react-native-webrtc": "137.1.2",
64
+ "@stream-io/react-native-webrtc": "137.1.3",
65
65
  "@types/react": "^19.1.0",
66
66
  "del-cli": "^6.0.0",
67
67
  "react": "19.1.0",