@telnyx/react-voice-commons-sdk 0.1.2 → 0.1.3

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 (58) hide show
  1. package/CHANGELOG.md +42 -0
  2. package/TelnyxVoiceCommons.podspec +31 -31
  3. package/ios/CallKitBridge.m +43 -43
  4. package/ios/CallKitBridge.swift +874 -879
  5. package/ios/VoicePnBridge.m +30 -30
  6. package/ios/VoicePnBridge.swift +86 -86
  7. package/lib/callkit/callkit-coordinator.d.ts +110 -117
  8. package/lib/callkit/callkit-coordinator.js +664 -727
  9. package/lib/callkit/callkit.d.ts +41 -41
  10. package/lib/callkit/callkit.js +252 -242
  11. package/lib/callkit/index.js +15 -47
  12. package/lib/callkit/use-callkit.d.ts +19 -19
  13. package/lib/callkit/use-callkit.js +270 -310
  14. package/lib/context/TelnyxVoiceContext.d.ts +9 -9
  15. package/lib/context/TelnyxVoiceContext.js +10 -13
  16. package/lib/hooks/use-callkit-coordinator.d.ts +9 -17
  17. package/lib/hooks/use-callkit-coordinator.js +45 -50
  18. package/lib/hooks/useAppReadyNotifier.js +13 -15
  19. package/lib/hooks/useAppStateHandler.d.ts +6 -11
  20. package/lib/hooks/useAppStateHandler.js +95 -110
  21. package/lib/hooks/useNetworkStateHandler.d.ts +0 -0
  22. package/lib/hooks/useNetworkStateHandler.js +0 -0
  23. package/lib/index.d.ts +3 -21
  24. package/lib/index.js +50 -201
  25. package/lib/internal/CallKitHandler.d.ts +6 -6
  26. package/lib/internal/CallKitHandler.js +96 -104
  27. package/lib/internal/callkit-manager.d.ts +57 -57
  28. package/lib/internal/callkit-manager.js +299 -316
  29. package/lib/internal/calls/call-state-controller.d.ts +73 -86
  30. package/lib/internal/calls/call-state-controller.js +263 -307
  31. package/lib/internal/session/session-manager.d.ts +71 -75
  32. package/lib/internal/session/session-manager.js +360 -424
  33. package/lib/internal/user-defaults-helpers.js +49 -39
  34. package/lib/internal/voice-pn-bridge.d.ts +114 -12
  35. package/lib/internal/voice-pn-bridge.js +212 -5
  36. package/lib/models/call-state.d.ts +46 -44
  37. package/lib/models/call-state.js +70 -68
  38. package/lib/models/call.d.ts +161 -133
  39. package/lib/models/call.js +454 -382
  40. package/lib/models/config.d.ts +11 -18
  41. package/lib/models/config.js +37 -35
  42. package/lib/models/connection-state.d.ts +10 -10
  43. package/lib/models/connection-state.js +16 -16
  44. package/lib/telnyx-voice-app.d.ts +28 -28
  45. package/lib/telnyx-voice-app.js +463 -481
  46. package/lib/telnyx-voip-client.d.ts +167 -167
  47. package/lib/telnyx-voip-client.js +385 -390
  48. package/package.json +11 -4
  49. package/src/callkit/callkit-coordinator.ts +18 -34
  50. package/src/hooks/useNetworkStateHandler.ts +0 -0
  51. package/src/internal/calls/call-state-controller.ts +81 -58
  52. package/src/internal/session/session-manager.ts +42 -26
  53. package/src/internal/voice-pn-bridge.ts +250 -2
  54. package/src/models/call-state.ts +8 -1
  55. package/src/models/call.ts +119 -5
  56. package/src/telnyx-voice-app.tsx +87 -40
  57. package/src/telnyx-voip-client.ts +15 -3
  58. package/src/types/telnyx-sdk.d.ts +16 -2
@@ -1,4 +1,10 @@
1
- import { NativeModules } from 'react-native';
1
+ import { NativeModules, DeviceEventEmitter, EmitterSubscription } from 'react-native';
2
+
3
+ export interface CallActionEvent {
4
+ action: string;
5
+ callId?: string;
6
+ timestamp: number;
7
+ }
2
8
 
3
9
  export interface VoicePnBridgeInterface {
4
10
  getPendingPushAction(): Promise<{
@@ -7,6 +13,25 @@ export interface VoicePnBridgeInterface {
7
13
  }>;
8
14
  setPendingPushAction(action: string, metadata: string): Promise<boolean>;
9
15
  clearPendingPushAction(): Promise<boolean>;
16
+
17
+ // Call action methods (reliable @ReactMethod pattern)
18
+ getPendingCallAction(): Promise<{
19
+ action: string | null;
20
+ callId: string | null;
21
+ timestamp: number | null;
22
+ }>;
23
+ clearPendingCallAction(): Promise<boolean>;
24
+
25
+ // Call control methods (Android specific)
26
+ endCall(callId: string | null): Promise<boolean>;
27
+ showOngoingCallNotification(
28
+ callerName: string | null,
29
+ callerNumber: string | null,
30
+ callId: string | null
31
+ ): Promise<boolean>;
32
+ hideOngoingCallNotification(): Promise<boolean>;
33
+ hideIncomingCallNotification(): Promise<boolean>;
34
+
10
35
  // Additional UserDefaults methods
11
36
  getVoipToken(): Promise<string | null>;
12
37
  getPendingVoipPush(): Promise<string | null>;
@@ -15,4 +40,227 @@ export interface VoicePnBridgeInterface {
15
40
  clearPendingVoipAction(): Promise<boolean>;
16
41
  }
17
42
 
18
- export const VoicePnBridge: VoicePnBridgeInterface = NativeModules.VoicePnBridge;
43
+ const NativeBridge: VoicePnBridgeInterface = NativeModules.VoicePnBridge;
44
+
45
+ /**
46
+ * Enhanced VoicePnBridge with call control and event handling capabilities
47
+ */
48
+ export class VoicePnBridge {
49
+ /**
50
+ * Get any pending push notification action from native side
51
+ */
52
+ static async getPendingPushAction(): Promise<{ action?: string; metadata?: string }> {
53
+ try {
54
+ const result = await NativeBridge.getPendingPushAction();
55
+ return {
56
+ action: result?.action || undefined,
57
+ metadata: result?.metadata || undefined,
58
+ };
59
+ } catch (error) {
60
+ console.error('VoicePnBridge: Error getting pending push action:', error);
61
+ return {};
62
+ }
63
+ }
64
+
65
+ /**
66
+ * Set a pending push notification action to native side
67
+ */
68
+ static async setPendingPushAction(action: string, metadata: string): Promise<boolean> {
69
+ try {
70
+ return await NativeBridge.setPendingPushAction(action, metadata);
71
+ } catch (error) {
72
+ console.error('VoicePnBridge: Error setting pending push action:', error);
73
+ return false;
74
+ }
75
+ }
76
+
77
+ /**
78
+ * Get any pending call action from native side (reliable polling pattern)
79
+ */
80
+ static async getPendingCallAction(): Promise<{
81
+ action?: string;
82
+ callId?: string;
83
+ timestamp?: number;
84
+ }> {
85
+ try {
86
+ const result = await NativeBridge.getPendingCallAction();
87
+ return {
88
+ action: result?.action || undefined,
89
+ callId: result?.callId || undefined,
90
+ timestamp: result?.timestamp || undefined,
91
+ };
92
+ } catch (error) {
93
+ console.error('VoicePnBridge: Error getting pending call action:', error);
94
+ return {};
95
+ }
96
+ }
97
+
98
+ /**
99
+ * Clear any pending call action
100
+ */
101
+ static async clearPendingCallAction(): Promise<boolean> {
102
+ try {
103
+ return await NativeBridge.clearPendingCallAction();
104
+ } catch (error) {
105
+ console.error('VoicePnBridge: Error clearing pending call action:', error);
106
+ return false;
107
+ }
108
+ }
109
+
110
+ /**
111
+ * Clear any pending push notification action
112
+ */
113
+ static async clearPendingPushAction(): Promise<boolean> {
114
+ try {
115
+ return await NativeBridge.clearPendingPushAction();
116
+ } catch (error) {
117
+ console.error('VoicePnBridge: Error clearing pending push action:', error);
118
+ return false;
119
+ }
120
+ }
121
+
122
+ /**
123
+ * React Native → Android: End/hang up the current call
124
+ * This will hide the ongoing call notification and notify the native side
125
+ */
126
+ static async endCall(callId?: string): Promise<boolean> {
127
+ try {
128
+ return await NativeBridge.endCall(callId || null);
129
+ } catch (error) {
130
+ console.error('VoicePnBridge: Error ending call:', error);
131
+ return false;
132
+ }
133
+ }
134
+
135
+ /**
136
+ * React Native → Android: Show ongoing call notification to keep app alive
137
+ * Should be called when a call becomes active to prevent background termination
138
+ */
139
+ static async showOngoingCallNotification(
140
+ callerName?: string,
141
+ callerNumber?: string,
142
+ callId?: string
143
+ ): Promise<boolean> {
144
+ try {
145
+ return await NativeBridge.showOngoingCallNotification(
146
+ callerName || null,
147
+ callerNumber || null,
148
+ callId || null
149
+ );
150
+ } catch (error) {
151
+ console.error('VoicePnBridge: Error showing ongoing call notification:', error);
152
+ return false;
153
+ }
154
+ }
155
+
156
+ /**
157
+ * React Native → Android: Hide ongoing call notification
158
+ * Should be called when a call ends to clean up notifications
159
+ */
160
+ static async hideOngoingCallNotification(): Promise<boolean> {
161
+ try {
162
+ return await NativeBridge.hideOngoingCallNotification();
163
+ } catch (error) {
164
+ console.error('VoicePnBridge: Error hiding ongoing call notification:', error);
165
+ return false;
166
+ }
167
+ }
168
+
169
+ /**
170
+ * React Native → Android: Hide incoming call notification
171
+ * Useful for dismissing notifications when call is answered/rejected in app
172
+ */
173
+ static async hideIncomingCallNotification(): Promise<boolean> {
174
+ try {
175
+ return await NativeBridge.hideIncomingCallNotification();
176
+ } catch (error) {
177
+ console.error('VoicePnBridge: Error hiding incoming call notification:', error);
178
+ return false;
179
+ }
180
+ }
181
+
182
+ /**
183
+ * Get VoIP token from native storage
184
+ */
185
+ static async getVoipToken(): Promise<string | null> {
186
+ try {
187
+ return await NativeBridge.getVoipToken();
188
+ } catch (error) {
189
+ console.error('VoicePnBridge: Error getting VoIP token:', error);
190
+ return null;
191
+ }
192
+ }
193
+
194
+ /**
195
+ * Get pending VoIP push from native storage
196
+ */
197
+ static async getPendingVoipPush(): Promise<string | null> {
198
+ try {
199
+ return await NativeBridge.getPendingVoipPush();
200
+ } catch (error) {
201
+ console.error('VoicePnBridge: Error getting pending VoIP push:', error);
202
+ return null;
203
+ }
204
+ }
205
+
206
+ /**
207
+ * Clear pending VoIP push from native storage
208
+ */
209
+ static async clearPendingVoipPush(): Promise<boolean> {
210
+ try {
211
+ return await NativeBridge.clearPendingVoipPush();
212
+ } catch (error) {
213
+ console.error('VoicePnBridge: Error clearing pending VoIP push:', error);
214
+ return false;
215
+ }
216
+ }
217
+
218
+ /**
219
+ * Get pending VoIP action from native storage
220
+ */
221
+ static async getPendingVoipAction(): Promise<string | null> {
222
+ try {
223
+ return await NativeBridge.getPendingVoipAction();
224
+ } catch (error) {
225
+ console.error('VoicePnBridge: Error getting pending VoIP action:', error);
226
+ return null;
227
+ }
228
+ }
229
+
230
+ /**
231
+ * Clear pending VoIP action from native storage
232
+ */
233
+ static async clearPendingVoipAction(): Promise<boolean> {
234
+ try {
235
+ return await NativeBridge.clearPendingVoipAction();
236
+ } catch (error) {
237
+ console.error('VoicePnBridge: Error clearing pending VoIP action:', error);
238
+ return false;
239
+ }
240
+ }
241
+
242
+ /**
243
+ * Android → React Native: Listen for immediate call action events from notification buttons
244
+ * Use this for active calls where immediate response is needed (e.g., ending ongoing calls)
245
+ */
246
+ static addCallActionListener(listener: (event: CallActionEvent) => void): EmitterSubscription {
247
+ return DeviceEventEmitter.addListener('TelnyxCallAction', listener);
248
+ }
249
+
250
+ /**
251
+ * Remove call action listener
252
+ */
253
+ static removeCallActionListener(subscription: EmitterSubscription): void {
254
+ subscription.remove();
255
+ }
256
+
257
+ /**
258
+ * Remove all call action listeners
259
+ */
260
+ static removeAllCallActionListeners(): void {
261
+ DeviceEventEmitter.removeAllListeners('TelnyxCallAction');
262
+ }
263
+ }
264
+
265
+ // Export the native bridge for direct access if needed
266
+ export { NativeBridge as VoicePnBridgeNative };
@@ -22,6 +22,9 @@ export enum TelnyxCallState {
22
22
 
23
23
  /** Call failed to connect or was rejected */
24
24
  FAILED = 'FAILED',
25
+
26
+ /** Call was dropped due to network issues */
27
+ DROPPED = 'DROPPED',
25
28
  }
26
29
 
27
30
  /**
@@ -79,7 +82,11 @@ export const CallStateHelpers = {
79
82
  * Is the call in a terminated state?
80
83
  */
81
84
  isTerminated(state: TelnyxCallState): boolean {
82
- return state === TelnyxCallState.ENDED || state === TelnyxCallState.FAILED;
85
+ return (
86
+ state === TelnyxCallState.ENDED ||
87
+ state === TelnyxCallState.FAILED ||
88
+ state === TelnyxCallState.DROPPED
89
+ );
83
90
  },
84
91
 
85
92
  /**
@@ -24,8 +24,17 @@ export class Call {
24
24
  private readonly _telnyxCall: TelnyxCall,
25
25
  private readonly _callId: string,
26
26
  private readonly _destination: string,
27
- private readonly _isIncoming: boolean
27
+ private readonly _isIncoming: boolean,
28
+ isReattached: boolean = false,
29
+ private readonly _originalCallerName?: string,
30
+ private readonly _originalCallerNumber?: string
28
31
  ) {
32
+ // Set initial state based on whether this is a reattached call
33
+ if (isReattached) {
34
+ console.log('Call: Setting initial state to ACTIVE for reattached call');
35
+ this._callState.next(TelnyxCallState.ACTIVE);
36
+ }
37
+
29
38
  this._setupCallListeners();
30
39
  }
31
40
 
@@ -85,6 +94,24 @@ export class Call {
85
94
  return this._duration.value;
86
95
  }
87
96
 
97
+ /**
98
+ * Custom headers received from the WebRTC INVITE message.
99
+ * These headers are passed during call initiation and can contain application-specific information.
100
+ * Format should be [{"name": "X-Header-Name", "value": "Value"}] where header names must start with "X-".
101
+ */
102
+ get inviteCustomHeaders(): { name: string; value: string }[] | null {
103
+ return this._telnyxCall.inviteCustomHeaders;
104
+ }
105
+
106
+ /**
107
+ * Custom headers received from the WebRTC ANSWER message.
108
+ * These headers are passed during call acceptance and can contain application-specific information.
109
+ * Format should be [{"name": "X-Header-Name", "value": "Value"}] where header names must start with "X-".
110
+ */
111
+ get answerCustomHeaders(): { name: string; value: string }[] | null {
112
+ return this._telnyxCall.answerCustomHeaders;
113
+ }
114
+
88
115
  /**
89
116
  * Get the underlying Telnyx Call object (for internal use)
90
117
  * @internal
@@ -163,8 +190,9 @@ export class Call {
163
190
 
164
191
  /**
165
192
  * Answer the incoming call
193
+ * @param customHeaders Optional custom headers to include with the answer
166
194
  */
167
- async answer(): Promise<void> {
195
+ async answer(customHeaders?: { name: string; value: string }[]): Promise<void> {
168
196
  if (!CallStateHelpers.canAnswer(this.currentState)) {
169
197
  throw new Error(`Cannot answer call in state: ${this.currentState}`);
170
198
  }
@@ -184,7 +212,8 @@ export class Call {
184
212
  console.log('Call: Setting state to CONNECTING before answering');
185
213
  this._callState.next(TelnyxCallState.CONNECTING);
186
214
 
187
- await this._telnyxCall.answer();
215
+ // Pass custom headers to the underlying Telnyx call
216
+ await this._telnyxCall.answer(customHeaders);
188
217
  } catch (error) {
189
218
  console.error('Failed to answer call:', error);
190
219
  throw error;
@@ -193,8 +222,9 @@ export class Call {
193
222
 
194
223
  /**
195
224
  * Hang up the call
225
+ * @param customHeaders Optional custom headers to include with the hangup request
196
226
  */
197
- async hangup(): Promise<void> {
227
+ async hangup(customHeaders?: { name: string; value: string }[]): Promise<void> {
198
228
  if (!CallStateHelpers.canHangup(this.currentState)) {
199
229
  throw new Error(`Cannot hang up call in state: ${this.currentState}`);
200
230
  }
@@ -211,7 +241,19 @@ export class Call {
211
241
  }
212
242
 
213
243
  // Fallback for Android or when CallKit is not available
214
- await this._telnyxCall.hangup();
244
+ await this._telnyxCall.hangup(customHeaders);
245
+
246
+ // On Android, also notify the native side to hide ongoing notification
247
+ if (Platform.OS === 'android') {
248
+ try {
249
+ const { VoicePnBridge } = await import('../internal/voice-pn-bridge');
250
+ await VoicePnBridge.endCall(this._callId);
251
+ console.log('Call: Notified Android to hide ongoing notification');
252
+ } catch (error) {
253
+ console.error('Call: Failed to notify Android about call end:', error);
254
+ // Don't fail the hangup if notification hiding fails
255
+ }
256
+ }
215
257
  } catch (error) {
216
258
  console.error('Failed to hang up call:', error);
217
259
  throw error;
@@ -327,11 +369,79 @@ export class Call {
327
369
  // Start duration timer when call becomes active
328
370
  if (telnyxState === TelnyxCallState.ACTIVE && !this._startTime) {
329
371
  this._startDurationTimer();
372
+
373
+ // Show ongoing call notification on Android when call becomes active
374
+ // This covers both locally answered calls and calls that become active from remote side
375
+ if (Platform.OS === 'android') {
376
+ (async () => {
377
+ try {
378
+ const { VoicePnBridge } = await import('../internal/voice-pn-bridge');
379
+
380
+ // Extract caller information based on call direction
381
+ let callerNumber: string | undefined;
382
+ let callerName: string | undefined;
383
+
384
+ if (this._isIncoming) {
385
+ // For incoming calls, use the remote caller ID (who's calling us)
386
+ callerNumber = this._telnyxCall.remoteCallerIdNumber;
387
+ callerName = this._telnyxCall.remoteCallerIdName;
388
+ } else {
389
+ // For outgoing calls, use our own caller ID (what we're showing to them)
390
+ // These are the values we set when making the call
391
+ callerNumber = this._telnyxCall.localCallerIdNumber || this._originalCallerNumber;
392
+ callerName = this._telnyxCall.localCallerIdName || this._originalCallerName;
393
+ }
394
+
395
+ // Fallback logic for better notification display - avoid "Unknown" when possible
396
+ let displayName: string;
397
+ let displayNumber: string;
398
+
399
+ if (this._isIncoming) {
400
+ // For incoming calls: use caller name or fall back to caller number, then destination
401
+ displayName = callerName || callerNumber || this._destination;
402
+ displayNumber = callerNumber || this._destination;
403
+ } else {
404
+ // For outgoing calls: use our caller ID or descriptive text
405
+ displayName = callerName || `${this._destination}`;
406
+ displayNumber = callerNumber || this._destination;
407
+ }
408
+
409
+ await VoicePnBridge.showOngoingCallNotification(
410
+ displayName,
411
+ displayNumber,
412
+ this._callId
413
+ );
414
+ console.log('Call: Showed ongoing call notification on Android (call active)', {
415
+ isIncoming: this._isIncoming,
416
+ callerName: displayName,
417
+ callerNumber: displayNumber,
418
+ });
419
+ } catch (error) {
420
+ console.error('Call: Failed to show ongoing call notification on active:', error);
421
+ }
422
+ })();
423
+ }
330
424
  }
331
425
 
332
426
  // Stop duration timer when call ends
333
427
  if (CallStateHelpers.isTerminated(telnyxState)) {
334
428
  this._stopDurationTimer();
429
+
430
+ // Clean up ongoing call notification on Android when call ends
431
+ if (Platform.OS === 'android') {
432
+ (async () => {
433
+ try {
434
+ const { VoicePnBridge } = await import('../internal/voice-pn-bridge');
435
+ await VoicePnBridge.endCall(this._callId);
436
+ console.log('Call: Cleaned up ongoing call notification (call terminated)');
437
+ } catch (error) {
438
+ console.error(
439
+ 'Call: Failed to clean up ongoing call notification on termination:',
440
+ error
441
+ );
442
+ }
443
+ })();
444
+ }
335
445
  }
336
446
  });
337
447
  }
@@ -346,6 +456,8 @@ export class Call {
346
456
  case 'ringing':
347
457
  case 'new':
348
458
  return TelnyxCallState.RINGING;
459
+ case 'connecting':
460
+ return TelnyxCallState.CONNECTING;
349
461
  case 'active':
350
462
  case 'answered':
351
463
  return TelnyxCallState.ACTIVE;
@@ -357,6 +469,8 @@ export class Call {
357
469
  case 'failed':
358
470
  case 'rejected':
359
471
  return TelnyxCallState.FAILED;
472
+ case 'dropped':
473
+ return TelnyxCallState.DROPPED;
360
474
  default:
361
475
  console.warn(`Unknown call state: ${telnyxState}`);
362
476
  return TelnyxCallState.RINGING;
@@ -262,27 +262,15 @@ const TelnyxVoiceAppComponent: React.FC<TelnyxVoiceAppProps> = ({
262
262
  // If auto-reconnection fails, redirect to login screen
263
263
  if (!success) {
264
264
  log('Auto-reconnection failed - redirecting to login screen');
265
- // Import router dynamically to avoid circular dependency issues
266
- const { router } = require('expo-router');
267
-
268
- // Small delay to ensure state is settled
269
- setTimeout(() => {
270
- router.replace('/');
271
- }, 100);
272
265
  }
273
266
  } catch (e) {
274
267
  log('Auto-reconnection error:', e);
275
-
276
268
  // On error, also redirect to login
277
269
  log('Auto-reconnection error - redirecting to login screen');
278
- const { router } = require('expo-router');
279
- setTimeout(() => {
280
- router.replace('/');
281
- }, 100);
282
270
  }
283
271
  }, [voipClient, log]);
284
272
 
285
- // Check for initial push notification when app launches
273
+ // Check for initial push notification action when app launches
286
274
  const checkForInitialPushNotification = useCallback(
287
275
  async (fromAppResume: boolean = false) => {
288
276
  log(`checkForInitialPushNotification called${fromAppResume ? ' (from app resume)' : ''}`);
@@ -309,6 +297,42 @@ const TelnyxVoiceAppComponent: React.FC<TelnyxVoiceAppProps> = ({
309
297
 
310
298
  if (VoicePnBridge) {
311
299
  log('Checking for pending push actions via VoicePnBridge');
300
+
301
+ // First check for pending call actions (notification button taps like hangup/answer)
302
+ const pendingCallAction = await VoicePnBridge.getPendingCallAction();
303
+ log('Raw pending call action response:', pendingCallAction);
304
+
305
+ if (pendingCallAction && pendingCallAction.action != null) {
306
+ log('Found pending call action:', pendingCallAction);
307
+
308
+ // Handle call actions directly
309
+ if (pendingCallAction.action === 'hangup' && pendingCallAction.callId) {
310
+ log(
311
+ 'Processing hangup action from notification for call:',
312
+ pendingCallAction.callId
313
+ );
314
+
315
+ // Find and hangup the call
316
+ const activeCall = voipClient.currentActiveCall;
317
+ if (activeCall && activeCall.callId === pendingCallAction.callId) {
318
+ log('Hanging up active call from notification action');
319
+ try {
320
+ await activeCall.hangup();
321
+ log('Call hung up successfully from notification action');
322
+ } catch (error) {
323
+ log('Error hanging up call from notification action:', error);
324
+ }
325
+ } else {
326
+ log('No matching active call found for hangup action');
327
+ }
328
+
329
+ // Clear the pending action
330
+ await VoicePnBridge.clearPendingCallAction();
331
+ return; // Don't process as push data
332
+ }
333
+ }
334
+
335
+ // Then check for regular push notification data
312
336
  const pendingAction = await VoicePnBridge.getPendingPushAction();
313
337
  log('Raw pending action response:', pendingAction);
314
338
 
@@ -317,33 +341,12 @@ const TelnyxVoiceAppComponent: React.FC<TelnyxVoiceAppProps> = ({
317
341
 
318
342
  // Parse the metadata if it's a string
319
343
  let metadata = pendingAction.metadata;
320
- if (typeof metadata === 'string') {
321
- try {
322
- // First try parsing as JSON
323
- metadata = JSON.parse(metadata);
324
- log('Parsed metadata as JSON:', metadata);
325
- } catch (e) {
326
- // If JSON parsing fails, try parsing Android key-value format
327
- // Format: "{call_id=value, action=value}"
328
- log('JSON parse failed, trying Android key-value format');
329
- try {
330
- const cleanedString = metadata.replace(/[{}]/g, '').trim();
331
- const pairs = cleanedString.split(',').map((pair) => pair.trim());
332
- const parsed: Record<string, any> = {};
333
-
334
- for (const pair of pairs) {
335
- const [key, value] = pair.split('=').map((s) => s.trim());
336
- if (key && value) {
337
- parsed[key] = value;
338
- }
339
- }
340
-
341
- metadata = parsed;
342
- log('Parsed metadata as Android key-value format:', metadata);
343
- } catch (parseError) {
344
- log('Failed to parse metadata in any format, using as-is:', parseError);
345
- }
346
- }
344
+ try {
345
+ // First try parsing as JSON
346
+ metadata = JSON.parse(metadata);
347
+ log('Parsed metadata as JSON:', metadata);
348
+ } catch (e) {
349
+ log('JSON parse failed, trying Android key-value format');
347
350
  }
348
351
 
349
352
  // Create push data structure that matches what the VoIP client expects
@@ -516,6 +519,41 @@ const TelnyxVoiceAppComponent: React.FC<TelnyxVoiceAppProps> = ({
516
519
  }
517
520
  });
518
521
 
522
+ // Listen for immediate call action events from notification buttons (Android only)
523
+ let callActionSubscription: any = null;
524
+ if (Platform.OS === 'android') {
525
+ try {
526
+ const { VoicePnBridge } = require('./internal/voice-pn-bridge');
527
+ callActionSubscription = VoicePnBridge.addCallActionListener((event) => {
528
+ log(`Received immediate call action: ${event.action} for callId: ${event.callId}`);
529
+
530
+ // Handle immediate call actions (mainly for ending active calls from notification)
531
+ if (
532
+ event.action === 'hangup' ||
533
+ event.action === 'endCall' ||
534
+ event.action === 'reject'
535
+ ) {
536
+ log(`Processing immediate end call action for callId: ${event.callId}`);
537
+
538
+ // Find the call by ID and end it
539
+ const targetCall = voipClient.currentCalls.find((call) => call.callId === event.callId);
540
+ if (targetCall) {
541
+ log(`Found active call ${event.callId}, ending it immediately`);
542
+ targetCall.hangup().catch((error) => {
543
+ log(`Error ending call ${event.callId}:`, error);
544
+ });
545
+ } else {
546
+ log(`No active call found with ID ${event.callId}`);
547
+ }
548
+ }
549
+ });
550
+
551
+ log('Call action listener registered for immediate notification handling');
552
+ } catch (e) {
553
+ log('Error setting up call action listener (VoicePnBridge not available):', e);
554
+ }
555
+ }
556
+
519
557
  // Add app state listener if not skipping web background detection or not on web
520
558
  // AND if app state management is enabled in the client options
521
559
  let appStateSubscription: any = null;
@@ -538,6 +576,15 @@ const TelnyxVoiceAppComponent: React.FC<TelnyxVoiceAppProps> = ({
538
576
  return () => {
539
577
  connectionStateSubscription.unsubscribe();
540
578
  callsSubscription.unsubscribe();
579
+ if (callActionSubscription) {
580
+ try {
581
+ const { VoicePnBridge } = require('./internal/voice-pn-bridge');
582
+ VoicePnBridge.removeCallActionListener(callActionSubscription);
583
+ log('Call action listener removed');
584
+ } catch (e) {
585
+ log('Error removing call action listener:', e);
586
+ }
587
+ }
541
588
  if (appStateSubscription) {
542
589
  appStateSubscription.remove();
543
590
  }
@@ -286,12 +286,19 @@ export class TelnyxVoipClient {
286
286
  * Initiates a new outgoing call.
287
287
  *
288
288
  * @param destination The destination number or SIP URI to call
289
- * @param debug Optional flag to enable call quality metrics for this call
289
+ * @param callerName Optional caller name to display
290
+ * @param callerNumber Optional caller ID number
291
+ * @param customHeaders Optional custom headers to include with the call
290
292
  * @returns A Promise that completes with the Call object once the invitation has been sent
291
293
  *
292
294
  * The call's state can be monitored through the returned Call object's streams.
293
295
  */
294
- async newCall(destination: string, debug: boolean = false): Promise<Call> {
296
+ async newCall(
297
+ destination: string,
298
+ callerName?: string,
299
+ callerNumber?: string,
300
+ customHeaders?: Record<string, string>
301
+ ): Promise<Call> {
295
302
  this._throwIfDisposed();
296
303
 
297
304
  if (!destination || destination.trim() === '') {
@@ -306,7 +313,12 @@ export class TelnyxVoipClient {
306
313
  console.log('TelnyxVoipClient: Creating new call to:', destination);
307
314
  }
308
315
 
309
- return await this._callStateController.newCall(destination, undefined, undefined, debug);
316
+ return await this._callStateController.newCall(
317
+ destination,
318
+ callerName,
319
+ callerNumber,
320
+ customHeaders
321
+ );
310
322
  }
311
323
 
312
324
  // ========== Push Notification Methods ==========