@telnyx/react-voice-commons-sdk 0.4.3 → 1.1.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 (47) hide show
  1. package/CHANGELOG.md +23 -0
  2. package/README.md +28 -7
  3. package/android/src/main/java/com/telnyx/react_voice_commons/CallForegroundService.kt +16 -2
  4. package/android/src/main/java/com/telnyx/react_voice_commons/TelnyxFirebaseMessagingService.kt +16 -2
  5. package/android/src/main/java/com/telnyx/react_voice_commons/TelnyxNotificationHelper.kt +345 -24
  6. package/android/src/main/java/com/telnyx/react_voice_commons/VoicePnBridgeModule.kt +74 -1
  7. package/android/src/main/java/com/telnyx/react_voice_commons/VoicePnManager.kt +36 -1
  8. package/ios/CallKitBridge.m +20 -1
  9. package/ios/CallKitBridge.swift +935 -60
  10. package/ios/README.md +2 -2
  11. package/ios/VoicePnBridge.m +21 -1
  12. package/ios/VoicePnBridge.swift +62 -1
  13. package/lib/callkit/callkit-coordinator.d.ts +39 -3
  14. package/lib/callkit/callkit-coordinator.js +561 -148
  15. package/lib/callkit/callkit.d.ts +7 -0
  16. package/lib/callkit/callkit.js +83 -11
  17. package/lib/hooks/useAppStateHandler.js +6 -2
  18. package/lib/index.d.ts +1 -0
  19. package/lib/index.js +9 -0
  20. package/lib/internal/CallKitHandler.d.ts +3 -0
  21. package/lib/internal/CallKitHandler.js +52 -46
  22. package/lib/internal/calls/call-state-controller.d.ts +39 -1
  23. package/lib/internal/calls/call-state-controller.js +152 -64
  24. package/lib/internal/session/session-manager.d.ts +16 -1
  25. package/lib/internal/session/session-manager.js +229 -58
  26. package/lib/internal/voice-pn-bridge.d.ts +44 -0
  27. package/lib/internal/voice-pn-bridge.js +94 -0
  28. package/lib/models/call.js +35 -0
  29. package/lib/models/config.d.ts +18 -0
  30. package/lib/models/config.js +2 -0
  31. package/lib/telnyx-voice-app.js +85 -43
  32. package/lib/telnyx-voip-client.d.ts +48 -5
  33. package/lib/telnyx-voip-client.js +276 -22
  34. package/package.json +7 -5
  35. package/src/callkit/callkit-coordinator.ts +642 -160
  36. package/src/callkit/callkit.ts +110 -12
  37. package/src/hooks/useAppStateHandler.ts +7 -2
  38. package/src/index.ts +3 -0
  39. package/src/internal/CallKitHandler.tsx +60 -55
  40. package/src/internal/calls/call-state-controller.ts +173 -74
  41. package/src/internal/session/session-manager.ts +277 -61
  42. package/src/internal/voice-pn-bridge.ts +111 -0
  43. package/src/models/call.ts +34 -0
  44. package/src/models/config.ts +18 -0
  45. package/src/telnyx-voice-app.tsx +96 -45
  46. package/src/telnyx-voip-client.ts +260 -24
  47. package/src/types/telnyx-sdk.d.ts +12 -1
@@ -7,6 +7,7 @@ export declare enum CallEndReason {
7
7
  }
8
8
  export interface CallKitEvent {
9
9
  callUUID: string;
10
+ isOnHold?: boolean;
10
11
  [key: string]: any;
11
12
  }
12
13
  declare class CallKitManager {
@@ -33,7 +34,11 @@ declare class CallKitManager {
33
34
  startOutgoingCall(callUUID: string, handle: string, displayName: string): Promise<boolean>;
34
35
  reportIncomingCall(callUUID: string, handle: string, displayName: string): Promise<boolean>;
35
36
  answerCall(callUUID: string): Promise<boolean>;
37
+ isCallRegistered(callUUID: string): Promise<boolean>;
36
38
  endCall(callUUID: string): Promise<boolean>;
39
+ completeHeldCallAction(callUUID: string, success: boolean): Promise<boolean>;
40
+ setCallHeld(callUUID: string, isOnHold: boolean): Promise<boolean>;
41
+ swapCalls(activeCallUUID: string, heldCallUUID: string): Promise<boolean>;
37
42
  reportCallConnected(callUUID: string): Promise<boolean>;
38
43
  reportCallEnded(callUUID: string, reason?: CallEndReason): Promise<boolean>;
39
44
  updateCall(callUUID: string, displayName: string, handle: string): Promise<boolean>;
@@ -41,7 +46,9 @@ declare class CallKitManager {
41
46
  onStartCall(listener: (event: CallKitEvent) => void): () => void;
42
47
  onAnswerCall(listener: (event: CallKitEvent) => void): () => void;
43
48
  onEndCall(listener: (event: CallKitEvent) => void): () => void;
49
+ onHeldCall(listener: (event: CallKitEvent) => void): () => void;
44
50
  onReceivePush(listener: (event: CallKitEvent) => void): () => void;
51
+ private addListener;
45
52
  generateCallUUID(): string;
46
53
  isAvailable(): boolean;
47
54
  }
@@ -68,6 +68,11 @@ class CallKitManager {
68
68
  console.log('CallKit: Received end call action', normalizedEvent);
69
69
  this.notifyListeners('endCall', normalizedEvent);
70
70
  });
71
+ this.eventEmitter.addListener('CallKitDidPerformHeldCallAction', (event) => {
72
+ const normalizedEvent = this.normalizeEvent(event);
73
+ console.log('CallKit: Received held call action', normalizedEvent);
74
+ this.notifyListeners('heldCall', normalizedEvent);
75
+ });
71
76
  this.eventEmitter.addListener('CallKitDidReceivePush', (event) => {
72
77
  const normalizedEvent = this.normalizeEvent(event);
73
78
  console.log('CallKit: Received push notification event', normalizedEvent);
@@ -75,9 +80,11 @@ class CallKitManager {
75
80
  });
76
81
  }
77
82
  notifyListeners(eventType, event) {
78
- const listener = this.listeners.get(eventType);
79
- if (listener) {
80
- listener(event);
83
+ const listeners = this.listeners.get(eventType);
84
+ if (listeners) {
85
+ for (const listener of listeners) {
86
+ listener(event);
87
+ }
81
88
  }
82
89
  }
83
90
  // Public API methods
@@ -140,6 +147,21 @@ class CallKitManager {
140
147
  return false;
141
148
  }
142
149
  }
150
+ async isCallRegistered(callUUID) {
151
+ if (!this.bridge || react_native_1.Platform.OS !== 'ios') {
152
+ return false;
153
+ }
154
+ try {
155
+ const result = await this.bridge.isCallRegistered(this.denormalizeUUID(callUUID));
156
+ return result.registered;
157
+ } catch (error) {
158
+ console.error('CallKit: Failed to check incoming call registration', {
159
+ callUUID,
160
+ error,
161
+ });
162
+ return false;
163
+ }
164
+ }
143
165
  async endCall(callUUID) {
144
166
  if (!this.bridge || react_native_1.Platform.OS !== 'ios') {
145
167
  console.warn('CallKit: Not available on this platform');
@@ -157,6 +179,46 @@ class CallKitManager {
157
179
  return false;
158
180
  }
159
181
  }
182
+ async completeHeldCallAction(callUUID, success) {
183
+ if (!this.bridge || react_native_1.Platform.OS !== 'ios') {
184
+ return false;
185
+ }
186
+ try {
187
+ const uppercaseUUID = this.denormalizeUUID(callUUID);
188
+ const result = await this.bridge.completeHeldCallAction(uppercaseUUID, success);
189
+ return result.success;
190
+ } catch (error) {
191
+ console.error('CallKit: Failed to complete held call action', error);
192
+ return false;
193
+ }
194
+ }
195
+ async setCallHeld(callUUID, isOnHold) {
196
+ if (!this.bridge || react_native_1.Platform.OS !== 'ios') {
197
+ return false;
198
+ }
199
+ try {
200
+ const result = await this.bridge.setCallHeld(this.denormalizeUUID(callUUID), isOnHold);
201
+ return result.success;
202
+ } catch (error) {
203
+ console.error('CallKit: Failed to change held state', { callUUID, isOnHold, error });
204
+ return false;
205
+ }
206
+ }
207
+ async swapCalls(activeCallUUID, heldCallUUID) {
208
+ if (!this.bridge || react_native_1.Platform.OS !== 'ios') {
209
+ return false;
210
+ }
211
+ try {
212
+ const result = await this.bridge.swapCalls(
213
+ this.denormalizeUUID(activeCallUUID),
214
+ this.denormalizeUUID(heldCallUUID)
215
+ );
216
+ return result.success;
217
+ } catch (error) {
218
+ console.error('CallKit: Failed to swap calls', error);
219
+ return false;
220
+ }
221
+ }
160
222
  async reportCallConnected(callUUID) {
161
223
  if (!this.bridge || react_native_1.Platform.OS !== 'ios') {
162
224
  return false;
@@ -220,20 +282,30 @@ class CallKitManager {
220
282
  }
221
283
  // Event listener management
222
284
  onStartCall(listener) {
223
- this.listeners.set('startCall', listener);
224
- return () => this.listeners.delete('startCall');
285
+ return this.addListener('startCall', listener);
225
286
  }
226
287
  onAnswerCall(listener) {
227
- this.listeners.set('answerCall', listener);
228
- return () => this.listeners.delete('answerCall');
288
+ return this.addListener('answerCall', listener);
229
289
  }
230
290
  onEndCall(listener) {
231
- this.listeners.set('endCall', listener);
232
- return () => this.listeners.delete('endCall');
291
+ return this.addListener('endCall', listener);
292
+ }
293
+ onHeldCall(listener) {
294
+ return this.addListener('heldCall', listener);
233
295
  }
234
296
  onReceivePush(listener) {
235
- this.listeners.set('receivePush', listener);
236
- return () => this.listeners.delete('receivePush');
297
+ return this.addListener('receivePush', listener);
298
+ }
299
+ addListener(eventType, listener) {
300
+ const listeners = this.listeners.get(eventType) ?? new Set();
301
+ listeners.add(listener);
302
+ this.listeners.set(eventType, listeners);
303
+ return () => {
304
+ listeners.delete(listener);
305
+ if (listeners.size === 0) {
306
+ this.listeners.delete(eventType);
307
+ }
308
+ };
237
309
  }
238
310
  // Utility methods
239
311
  generateCallUUID() {
@@ -30,8 +30,12 @@ const useAppStateHandler = ({
30
30
  if (appState.current.match(/active/) && nextAppState === 'background') {
31
31
  log('AppStateHandler: App went to background, checking for active calls...');
32
32
  const connectionState = voipClient.currentConnectionState;
33
- if (connectionState !== connection_state_1.TelnyxConnectionState.CONNECTED) {
34
- log('AppStateHandler: Not connected, skipping background handling');
33
+ const shouldHandleBackgroundConnection =
34
+ connectionState === connection_state_1.TelnyxConnectionState.CONNECTED ||
35
+ connectionState === connection_state_1.TelnyxConnectionState.CONNECTING ||
36
+ connectionState === connection_state_1.TelnyxConnectionState.RECONNECTING;
37
+ if (!shouldHandleBackgroundConnection) {
38
+ log('AppStateHandler: Not connected or connecting, skipping background handling');
35
39
  appState.current = nextAppState;
36
40
  return;
37
41
  }
package/lib/index.d.ts CHANGED
@@ -41,4 +41,5 @@ export {
41
41
  export type { Config, CredentialConfig, TokenConfig } from './models/config';
42
42
  export type { Call as TelnyxCall } from '@telnyx/react-native-voice-sdk';
43
43
  export * from './callkit';
44
+ export { VoicePnBridge } from './internal/voice-pn-bridge';
44
45
  export { useAppReadyNotifier } from './hooks/useAppReadyNotifier';
package/lib/index.js CHANGED
@@ -38,6 +38,7 @@ var __exportStar =
38
38
  };
39
39
  Object.defineProperty(exports, '__esModule', { value: true });
40
40
  exports.useAppReadyNotifier =
41
+ exports.VoicePnBridge =
41
42
  exports.createTokenConfig =
42
43
  exports.createCredentialConfig =
43
44
  exports.validateTokenConfig =
@@ -215,6 +216,14 @@ Object.defineProperty(exports, 'createTokenConfig', {
215
216
  });
216
217
  // Export CallKit functionality
217
218
  __exportStar(require('./callkit'), exports);
219
+ // Native push notification helpers
220
+ var voice_pn_bridge_1 = require('./internal/voice-pn-bridge');
221
+ Object.defineProperty(exports, 'VoicePnBridge', {
222
+ enumerable: true,
223
+ get: function () {
224
+ return voice_pn_bridge_1.VoicePnBridge;
225
+ },
226
+ });
218
227
  // Export hooks
219
228
  var useAppReadyNotifier_1 = require('./hooks/useAppReadyNotifier');
220
229
  Object.defineProperty(exports, 'useAppReadyNotifier', {
@@ -11,6 +11,9 @@ interface CallKitHandlerProps {
11
11
  * Internal CallKit handler for iOS push notifications
12
12
  * This component is automatically included in TelnyxVoiceApp
13
13
  *
14
+ * The active instance removes only its own DeviceEventEmitter subscriptions on
15
+ * unmount via each listener's `.remove()` method.
16
+ *
14
17
  * @internal - Users should not use this component directly
15
18
  */
16
19
  export declare const CallKitHandler: React.FC<CallKitHandlerProps>;
@@ -9,19 +9,68 @@ exports.CallKitHandler = void 0;
9
9
  const react_1 = require('react');
10
10
  const react_native_1 = require('react-native');
11
11
  const async_storage_1 = __importDefault(require('@react-native-async-storage/async-storage'));
12
- const TelnyxVoiceContext_1 = require('../context/TelnyxVoiceContext');
13
- // Global flag to ensure only one CallKitHandler is active
12
+ // Global flag to ensure only one CallKitHandler is active.
14
13
  let isCallKitHandlerActive = false;
15
14
  /**
16
15
  * Internal CallKit handler for iOS push notifications
17
16
  * This component is automatically included in TelnyxVoiceApp
18
17
  *
18
+ * The active instance removes only its own DeviceEventEmitter subscriptions on
19
+ * unmount via each listener's `.remove()` method.
20
+ *
19
21
  * @internal - Users should not use this component directly
20
22
  */
21
23
  const CallKitHandler = ({ onLoginRequired, onNavigateToDialer, onNavigateBack }) => {
22
- const { voipClient } = (0, TelnyxVoiceContext_1.useTelnyxVoice)();
23
24
  // Store active calls by CallKit UUID for coordination
24
25
  const activeCallsRef = (0, react_1.useRef)(new Map());
26
+ // DeviceEventEmitter listeners are registered once below, so read callbacks
27
+ // from refs that are updated during every render rather than stale closures.
28
+ const onLoginRequiredRef = (0, react_1.useRef)(onLoginRequired);
29
+ const onNavigateToDialerRef = (0, react_1.useRef)(onNavigateToDialer);
30
+ const onNavigateBackRef = (0, react_1.useRef)(onNavigateBack);
31
+ onLoginRequiredRef.current = onLoginRequired;
32
+ onNavigateToDialerRef.current = onNavigateToDialer;
33
+ onNavigateBackRef.current = onNavigateBack;
34
+ const handleIncomingCall = async (eventData) => {
35
+ console.log('CallKitHandler: Handling incoming call', {
36
+ callUUID: eventData.callUUID,
37
+ });
38
+ // Store the push notification payload
39
+ await async_storage_1.default.setItem(
40
+ '@push_notification_payload',
41
+ JSON.stringify(eventData.payload)
42
+ );
43
+ // Mark this call as being processed
44
+ activeCallsRef.current.set(eventData.callUUID, {
45
+ processing: true,
46
+ timestamp: Date.now(),
47
+ });
48
+ // Trigger login required callback if provided
49
+ if (onLoginRequiredRef.current) {
50
+ onLoginRequiredRef.current(eventData.payload);
51
+ }
52
+ };
53
+ const handleAnswerCall = async (eventData) => {
54
+ console.log('CallKitHandler: User answered call via CallKit', {
55
+ callUUID: eventData.callUUID,
56
+ isTrackedCall: activeCallsRef.current.has(eventData.callUUID),
57
+ });
58
+ if (onNavigateToDialerRef.current) {
59
+ onNavigateToDialerRef.current();
60
+ }
61
+ };
62
+ const handleEndCall = async (eventData) => {
63
+ console.log('CallKitHandler: User ended call via CallKit', {
64
+ callUUID: eventData.callUUID,
65
+ isTrackedCall: activeCallsRef.current.has(eventData.callUUID),
66
+ });
67
+ // Clean up our local tracking info
68
+ activeCallsRef.current.delete(eventData.callUUID);
69
+ await async_storage_1.default.removeItem('@push_notification_payload');
70
+ if (onNavigateBackRef.current) {
71
+ onNavigateBackRef.current();
72
+ }
73
+ };
25
74
  (0, react_1.useEffect)(() => {
26
75
  if (react_native_1.Platform.OS !== 'ios') return;
27
76
  if (isCallKitHandlerActive) {
@@ -30,8 +79,6 @@ const CallKitHandler = ({ onLoginRequired, onNavigateToDialer, onNavigateBack })
30
79
  }
31
80
  isCallKitHandlerActive = true;
32
81
  console.log('CallKitHandler: Setting up DeviceEventEmitter listeners (singleton instance)...');
33
- react_native_1.DeviceEventEmitter.removeAllListeners('incomingVoIPCall');
34
- react_native_1.DeviceEventEmitter.removeAllListeners('callKitAction');
35
82
  const incomingCallListener = react_native_1.DeviceEventEmitter.addListener(
36
83
  'incomingVoIPCall',
37
84
  async (eventData) => {
@@ -63,47 +110,6 @@ const CallKitHandler = ({ onLoginRequired, onNavigateToDialer, onNavigateBack })
63
110
  isCallKitHandlerActive = false;
64
111
  };
65
112
  }, []);
66
- const handleIncomingCall = async (eventData) => {
67
- console.log('CallKitHandler: Handling incoming call', {
68
- callUUID: eventData.callUUID,
69
- hasClient: !!voipClient,
70
- });
71
- // Store the push notification payload
72
- await async_storage_1.default.setItem(
73
- '@push_notification_payload',
74
- JSON.stringify(eventData.payload)
75
- );
76
- // Mark this call as being processed
77
- activeCallsRef.current.set(eventData.callUUID, {
78
- processing: true,
79
- timestamp: Date.now(),
80
- });
81
- // Trigger login required callback if provided
82
- if (onLoginRequired) {
83
- onLoginRequired(eventData.payload);
84
- }
85
- };
86
- const handleAnswerCall = async (eventData) => {
87
- console.log('CallKitHandler: User answered call via CallKit', {
88
- callUUID: eventData.callUUID,
89
- isTrackedCall: activeCallsRef.current.has(eventData.callUUID),
90
- });
91
- if (onNavigateToDialer) {
92
- onNavigateToDialer();
93
- }
94
- };
95
- const handleEndCall = async (eventData) => {
96
- console.log('CallKitHandler: User ended call via CallKit', {
97
- callUUID: eventData.callUUID,
98
- isTrackedCall: activeCallsRef.current.has(eventData.callUUID),
99
- });
100
- // Clean up our local tracking info
101
- activeCallsRef.current.delete(eventData.callUUID);
102
- await async_storage_1.default.removeItem('@push_notification_payload');
103
- if (onNavigateBack) {
104
- onNavigateBack();
105
- }
106
- };
107
113
  // This component doesn't render anything, it just handles events
108
114
  return null;
109
115
  };
@@ -1,6 +1,11 @@
1
1
  import { Observable } from 'rxjs';
2
2
  import { Call } from '../../models/call';
3
3
  import { SessionManager } from '../session/session-manager';
4
+ export type CustomHeader = {
5
+ name: string;
6
+ value: string;
7
+ };
8
+ export type CustomHeaders = Record<string, string> | CustomHeader[];
4
9
  /**
5
10
  * Central state machine for call management.
6
11
  *
@@ -12,8 +17,10 @@ export declare class CallStateController {
12
17
  private readonly _calls;
13
18
  private readonly _callMap;
14
19
  private _disposed;
20
+ private _listenerClient?;
15
21
  private _isWaitingForInvite?;
16
22
  private _onInviteAutoAccepted?;
23
+ private _activeCallId;
17
24
  constructor(_sessionManager: SessionManager);
18
25
  /**
19
26
  * Observable stream of all current calls
@@ -45,11 +52,33 @@ export declare class CallStateController {
45
52
  * @param callId The ID of the call to set to connecting state
46
53
  */
47
54
  setCallConnecting(callId: string): void;
55
+ /**
56
+ * Explicitly set the active call for multi-call scenarios.
57
+ * When set, activeCall$ and currentActiveCall prefer this call over
58
+ * the first-match heuristic. The ID is cleared automatically when the
59
+ * call reaches a terminal state.
60
+ * @param callId The ID of the call to mark as active
61
+ */
62
+ setActiveCall(callId: string): void;
63
+ /**
64
+ * Clear the explicitly-tracked active call ID, reverting to the
65
+ * first-match heuristic for active call selection.
66
+ */
67
+ clearActiveCall(): void;
48
68
  /**
49
69
  * Find a call by its underlying Telnyx call ID
50
70
  * @param telnyxCall The Telnyx call object to find
51
71
  */
52
72
  findCallByTelnyxCall(telnyxCall: any): Call | null;
73
+ /**
74
+ * Select the active call, preferring the explicitly-tracked call ID
75
+ * over the first-match heuristic.
76
+ */
77
+ private _selectActiveCall;
78
+ /**
79
+ * Check if a call is in a non-terminal (active or connecting) state.
80
+ */
81
+ private _isNonTerminal;
53
82
  /**
54
83
  * Initialize client listeners when the Telnyx client becomes available
55
84
  * This should be called by the session manager after client creation
@@ -62,8 +91,12 @@ export declare class CallStateController {
62
91
  destination: string,
63
92
  callerName?: string,
64
93
  callerNumber?: string,
65
- customHeaders?: Record<string, string>
94
+ customHeaders?: CustomHeaders
66
95
  ): Promise<Call>;
96
+ /**
97
+ * Normalize public custom headers into the format expected by the underlying SDK.
98
+ */
99
+ private _normalizeCustomHeaders;
67
100
  /**
68
101
  * Set callbacks for waiting for invite logic (used for push notifications)
69
102
  */
@@ -86,6 +119,11 @@ export declare class CallStateController {
86
119
  * Set up event listeners for the Telnyx client
87
120
  */
88
121
  private _setupClientListeners;
122
+ private _removeClientListeners;
123
+ private _handleTelnyxIncomingCall;
124
+ private _handleTelnyxReattachedCall;
125
+ private _handleTelnyxCallStateChanged;
126
+ private _handleTelnyxCallRemoved;
89
127
  /**
90
128
  * Handle incoming call or reattached call
91
129
  */