@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.
- package/CHANGELOG.md +23 -0
- package/README.md +28 -7
- package/android/src/main/java/com/telnyx/react_voice_commons/CallForegroundService.kt +16 -2
- package/android/src/main/java/com/telnyx/react_voice_commons/TelnyxFirebaseMessagingService.kt +16 -2
- package/android/src/main/java/com/telnyx/react_voice_commons/TelnyxNotificationHelper.kt +345 -24
- package/android/src/main/java/com/telnyx/react_voice_commons/VoicePnBridgeModule.kt +74 -1
- package/android/src/main/java/com/telnyx/react_voice_commons/VoicePnManager.kt +36 -1
- package/ios/CallKitBridge.m +20 -1
- package/ios/CallKitBridge.swift +935 -60
- package/ios/README.md +2 -2
- package/ios/VoicePnBridge.m +21 -1
- package/ios/VoicePnBridge.swift +62 -1
- package/lib/callkit/callkit-coordinator.d.ts +39 -3
- package/lib/callkit/callkit-coordinator.js +561 -148
- package/lib/callkit/callkit.d.ts +7 -0
- package/lib/callkit/callkit.js +83 -11
- package/lib/hooks/useAppStateHandler.js +6 -2
- package/lib/index.d.ts +1 -0
- package/lib/index.js +9 -0
- package/lib/internal/CallKitHandler.d.ts +3 -0
- package/lib/internal/CallKitHandler.js +52 -46
- package/lib/internal/calls/call-state-controller.d.ts +39 -1
- package/lib/internal/calls/call-state-controller.js +152 -64
- package/lib/internal/session/session-manager.d.ts +16 -1
- package/lib/internal/session/session-manager.js +229 -58
- package/lib/internal/voice-pn-bridge.d.ts +44 -0
- package/lib/internal/voice-pn-bridge.js +94 -0
- package/lib/models/call.js +35 -0
- package/lib/models/config.d.ts +18 -0
- package/lib/models/config.js +2 -0
- package/lib/telnyx-voice-app.js +85 -43
- package/lib/telnyx-voip-client.d.ts +48 -5
- package/lib/telnyx-voip-client.js +276 -22
- package/package.json +7 -5
- package/src/callkit/callkit-coordinator.ts +642 -160
- package/src/callkit/callkit.ts +110 -12
- package/src/hooks/useAppStateHandler.ts +7 -2
- package/src/index.ts +3 -0
- package/src/internal/CallKitHandler.tsx +60 -55
- package/src/internal/calls/call-state-controller.ts +173 -74
- package/src/internal/session/session-manager.ts +277 -61
- package/src/internal/voice-pn-bridge.ts +111 -0
- package/src/models/call.ts +34 -0
- package/src/models/config.ts +18 -0
- package/src/telnyx-voice-app.tsx +96 -45
- package/src/telnyx-voip-client.ts +260 -24
- package/src/types/telnyx-sdk.d.ts +12 -1
package/src/callkit/callkit.ts
CHANGED
|
@@ -11,8 +11,21 @@ interface CallKitBridgeInterface {
|
|
|
11
11
|
handle: string,
|
|
12
12
|
displayName: string
|
|
13
13
|
): Promise<{ success: boolean; callUUID: string }>;
|
|
14
|
+
isCallRegistered(callUUID: string): Promise<{ registered: boolean; callUUID: string }>;
|
|
14
15
|
answerCall(callUUID: string): Promise<{ success: boolean; callUUID: string }>;
|
|
15
16
|
endCall(callUUID: string): Promise<{ success: boolean; callUUID: string }>;
|
|
17
|
+
completeHeldCallAction(
|
|
18
|
+
callUUID: string,
|
|
19
|
+
success: boolean
|
|
20
|
+
): Promise<{ success: boolean; callUUID: string }>;
|
|
21
|
+
setCallHeld(
|
|
22
|
+
callUUID: string,
|
|
23
|
+
isOnHold: boolean
|
|
24
|
+
): Promise<{ success: boolean; callUUID: string; isOnHold: boolean }>;
|
|
25
|
+
swapCalls(
|
|
26
|
+
activeCallUUID: string,
|
|
27
|
+
heldCallUUID: string
|
|
28
|
+
): Promise<{ success: boolean; activeCallUUID: string; heldCallUUID: string }>;
|
|
16
29
|
reportCallConnected(callUUID: string): Promise<{ success: boolean }>;
|
|
17
30
|
reportCallEnded(callUUID: string, reason: number): Promise<{ success: boolean }>;
|
|
18
31
|
updateCall(callUUID: string, displayName: string, handle: string): Promise<{ success: boolean }>;
|
|
@@ -31,13 +44,14 @@ export enum CallEndReason {
|
|
|
31
44
|
// CallKit event types
|
|
32
45
|
export interface CallKitEvent {
|
|
33
46
|
callUUID: string;
|
|
47
|
+
isOnHold?: boolean;
|
|
34
48
|
[key: string]: any;
|
|
35
49
|
}
|
|
36
50
|
|
|
37
51
|
class CallKitManager {
|
|
38
52
|
private bridge: CallKitBridgeInterface | null = null;
|
|
39
53
|
private eventEmitter: NativeEventEmitter | null = null;
|
|
40
|
-
private listeners: Map<string, (event: CallKitEvent) => void
|
|
54
|
+
private listeners: Map<string, Set<(event: CallKitEvent) => void>> = new Map();
|
|
41
55
|
|
|
42
56
|
/**
|
|
43
57
|
* Normalize UUID to lowercase for consistent handling in React Native
|
|
@@ -100,6 +114,12 @@ class CallKitManager {
|
|
|
100
114
|
this.notifyListeners('endCall', normalizedEvent);
|
|
101
115
|
});
|
|
102
116
|
|
|
117
|
+
this.eventEmitter.addListener('CallKitDidPerformHeldCallAction', (event) => {
|
|
118
|
+
const normalizedEvent = this.normalizeEvent(event);
|
|
119
|
+
console.log('CallKit: Received held call action', normalizedEvent);
|
|
120
|
+
this.notifyListeners('heldCall', normalizedEvent);
|
|
121
|
+
});
|
|
122
|
+
|
|
103
123
|
this.eventEmitter.addListener('CallKitDidReceivePush', (event) => {
|
|
104
124
|
const normalizedEvent = this.normalizeEvent(event);
|
|
105
125
|
console.log('CallKit: Received push notification event', normalizedEvent);
|
|
@@ -108,9 +128,11 @@ class CallKitManager {
|
|
|
108
128
|
}
|
|
109
129
|
|
|
110
130
|
private notifyListeners(eventType: string, event: CallKitEvent) {
|
|
111
|
-
const
|
|
112
|
-
if (
|
|
113
|
-
listener
|
|
131
|
+
const listeners = this.listeners.get(eventType);
|
|
132
|
+
if (listeners) {
|
|
133
|
+
for (const listener of listeners) {
|
|
134
|
+
listener(event);
|
|
135
|
+
}
|
|
114
136
|
}
|
|
115
137
|
}
|
|
116
138
|
|
|
@@ -189,6 +211,23 @@ class CallKitManager {
|
|
|
189
211
|
}
|
|
190
212
|
}
|
|
191
213
|
|
|
214
|
+
public async isCallRegistered(callUUID: string): Promise<boolean> {
|
|
215
|
+
if (!this.bridge || Platform.OS !== 'ios') {
|
|
216
|
+
return false;
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
try {
|
|
220
|
+
const result = await this.bridge.isCallRegistered(this.denormalizeUUID(callUUID));
|
|
221
|
+
return result.registered;
|
|
222
|
+
} catch (error) {
|
|
223
|
+
console.error('CallKit: Failed to check incoming call registration', {
|
|
224
|
+
callUUID,
|
|
225
|
+
error,
|
|
226
|
+
});
|
|
227
|
+
return false;
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
|
|
192
231
|
public async endCall(callUUID: string): Promise<boolean> {
|
|
193
232
|
if (!this.bridge || Platform.OS !== 'ios') {
|
|
194
233
|
console.warn('CallKit: Not available on this platform');
|
|
@@ -208,6 +247,52 @@ class CallKitManager {
|
|
|
208
247
|
}
|
|
209
248
|
}
|
|
210
249
|
|
|
250
|
+
public async completeHeldCallAction(callUUID: string, success: boolean): Promise<boolean> {
|
|
251
|
+
if (!this.bridge || Platform.OS !== 'ios') {
|
|
252
|
+
return false;
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
try {
|
|
256
|
+
const uppercaseUUID = this.denormalizeUUID(callUUID);
|
|
257
|
+
const result = await this.bridge.completeHeldCallAction(uppercaseUUID, success);
|
|
258
|
+
return result.success;
|
|
259
|
+
} catch (error) {
|
|
260
|
+
console.error('CallKit: Failed to complete held call action', error);
|
|
261
|
+
return false;
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
public async setCallHeld(callUUID: string, isOnHold: boolean): Promise<boolean> {
|
|
266
|
+
if (!this.bridge || Platform.OS !== 'ios') {
|
|
267
|
+
return false;
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
try {
|
|
271
|
+
const result = await this.bridge.setCallHeld(this.denormalizeUUID(callUUID), isOnHold);
|
|
272
|
+
return result.success;
|
|
273
|
+
} catch (error) {
|
|
274
|
+
console.error('CallKit: Failed to change held state', { callUUID, isOnHold, error });
|
|
275
|
+
return false;
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
public async swapCalls(activeCallUUID: string, heldCallUUID: string): Promise<boolean> {
|
|
280
|
+
if (!this.bridge || Platform.OS !== 'ios') {
|
|
281
|
+
return false;
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
try {
|
|
285
|
+
const result = await this.bridge.swapCalls(
|
|
286
|
+
this.denormalizeUUID(activeCallUUID),
|
|
287
|
+
this.denormalizeUUID(heldCallUUID)
|
|
288
|
+
);
|
|
289
|
+
return result.success;
|
|
290
|
+
} catch (error) {
|
|
291
|
+
console.error('CallKit: Failed to swap calls', error);
|
|
292
|
+
return false;
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
|
|
211
296
|
public async reportCallConnected(callUUID: string): Promise<boolean> {
|
|
212
297
|
if (!this.bridge || Platform.OS !== 'ios') {
|
|
213
298
|
return false;
|
|
@@ -283,23 +368,36 @@ class CallKitManager {
|
|
|
283
368
|
// Event listener management
|
|
284
369
|
|
|
285
370
|
public onStartCall(listener: (event: CallKitEvent) => void): () => void {
|
|
286
|
-
this.
|
|
287
|
-
return () => this.listeners.delete('startCall');
|
|
371
|
+
return this.addListener('startCall', listener);
|
|
288
372
|
}
|
|
289
373
|
|
|
290
374
|
public onAnswerCall(listener: (event: CallKitEvent) => void): () => void {
|
|
291
|
-
this.
|
|
292
|
-
return () => this.listeners.delete('answerCall');
|
|
375
|
+
return this.addListener('answerCall', listener);
|
|
293
376
|
}
|
|
294
377
|
|
|
295
378
|
public onEndCall(listener: (event: CallKitEvent) => void): () => void {
|
|
296
|
-
this.
|
|
297
|
-
|
|
379
|
+
return this.addListener('endCall', listener);
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
public onHeldCall(listener: (event: CallKitEvent) => void): () => void {
|
|
383
|
+
return this.addListener('heldCall', listener);
|
|
298
384
|
}
|
|
299
385
|
|
|
300
386
|
public onReceivePush(listener: (event: CallKitEvent) => void): () => void {
|
|
301
|
-
this.
|
|
302
|
-
|
|
387
|
+
return this.addListener('receivePush', listener);
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
private addListener(eventType: string, listener: (event: CallKitEvent) => void): () => void {
|
|
391
|
+
const listeners = this.listeners.get(eventType) ?? new Set();
|
|
392
|
+
listeners.add(listener);
|
|
393
|
+
this.listeners.set(eventType, listeners);
|
|
394
|
+
|
|
395
|
+
return () => {
|
|
396
|
+
listeners.delete(listener);
|
|
397
|
+
if (listeners.size === 0) {
|
|
398
|
+
this.listeners.delete(eventType);
|
|
399
|
+
}
|
|
400
|
+
};
|
|
303
401
|
}
|
|
304
402
|
|
|
305
403
|
// Utility methods
|
|
@@ -34,8 +34,13 @@ export const useAppStateHandler = ({
|
|
|
34
34
|
log('AppStateHandler: App went to background, checking for active calls...');
|
|
35
35
|
|
|
36
36
|
const connectionState = voipClient.currentConnectionState;
|
|
37
|
-
|
|
38
|
-
|
|
37
|
+
const shouldHandleBackgroundConnection =
|
|
38
|
+
connectionState === TelnyxConnectionState.CONNECTED ||
|
|
39
|
+
connectionState === TelnyxConnectionState.CONNECTING ||
|
|
40
|
+
connectionState === TelnyxConnectionState.RECONNECTING;
|
|
41
|
+
|
|
42
|
+
if (!shouldHandleBackgroundConnection) {
|
|
43
|
+
log('AppStateHandler: Not connected or connecting, skipping background handling');
|
|
39
44
|
appState.current = nextAppState;
|
|
40
45
|
return;
|
|
41
46
|
}
|
package/src/index.ts
CHANGED
|
@@ -55,5 +55,8 @@ export type { Call as TelnyxCall } from '@telnyx/react-native-voice-sdk';
|
|
|
55
55
|
// Export CallKit functionality
|
|
56
56
|
export * from './callkit';
|
|
57
57
|
|
|
58
|
+
// Native push notification helpers
|
|
59
|
+
export { VoicePnBridge } from './internal/voice-pn-bridge';
|
|
60
|
+
|
|
58
61
|
// Export hooks
|
|
59
62
|
export { useAppReadyNotifier } from './hooks/useAppReadyNotifier';
|
|
@@ -1,10 +1,8 @@
|
|
|
1
1
|
import React, { useEffect, useRef } from 'react';
|
|
2
2
|
import { Platform, DeviceEventEmitter } from 'react-native';
|
|
3
3
|
import AsyncStorage from '@react-native-async-storage/async-storage';
|
|
4
|
-
import { useTelnyxVoice } from '../context/TelnyxVoiceContext';
|
|
5
|
-
import { callKitCoordinator } from '../callkit';
|
|
6
4
|
|
|
7
|
-
// Global flag to ensure only one CallKitHandler is active
|
|
5
|
+
// Global flag to ensure only one CallKitHandler is active.
|
|
8
6
|
let isCallKitHandlerActive = false;
|
|
9
7
|
|
|
10
8
|
interface CallData {
|
|
@@ -28,6 +26,9 @@ interface CallKitHandlerProps {
|
|
|
28
26
|
* Internal CallKit handler for iOS push notifications
|
|
29
27
|
* This component is automatically included in TelnyxVoiceApp
|
|
30
28
|
*
|
|
29
|
+
* The active instance removes only its own DeviceEventEmitter subscriptions on
|
|
30
|
+
* unmount via each listener's `.remove()` method.
|
|
31
|
+
*
|
|
31
32
|
* @internal - Users should not use this component directly
|
|
32
33
|
*/
|
|
33
34
|
export const CallKitHandler: React.FC<CallKitHandlerProps> = ({
|
|
@@ -35,11 +36,65 @@ export const CallKitHandler: React.FC<CallKitHandlerProps> = ({
|
|
|
35
36
|
onNavigateToDialer,
|
|
36
37
|
onNavigateBack,
|
|
37
38
|
}) => {
|
|
38
|
-
const { voipClient } = useTelnyxVoice();
|
|
39
|
-
|
|
40
39
|
// Store active calls by CallKit UUID for coordination
|
|
41
40
|
const activeCallsRef = useRef<Map<string, any>>(new Map());
|
|
42
41
|
|
|
42
|
+
// DeviceEventEmitter listeners are registered once below, so read callbacks
|
|
43
|
+
// from refs that are updated during every render rather than stale closures.
|
|
44
|
+
const onLoginRequiredRef = useRef(onLoginRequired);
|
|
45
|
+
const onNavigateToDialerRef = useRef(onNavigateToDialer);
|
|
46
|
+
const onNavigateBackRef = useRef(onNavigateBack);
|
|
47
|
+
|
|
48
|
+
onLoginRequiredRef.current = onLoginRequired;
|
|
49
|
+
onNavigateToDialerRef.current = onNavigateToDialer;
|
|
50
|
+
onNavigateBackRef.current = onNavigateBack;
|
|
51
|
+
|
|
52
|
+
const handleIncomingCall = async (eventData: CallData) => {
|
|
53
|
+
console.log('CallKitHandler: Handling incoming call', {
|
|
54
|
+
callUUID: eventData.callUUID,
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
// Store the push notification payload
|
|
58
|
+
await AsyncStorage.setItem('@push_notification_payload', JSON.stringify(eventData.payload));
|
|
59
|
+
|
|
60
|
+
// Mark this call as being processed
|
|
61
|
+
activeCallsRef.current.set(eventData.callUUID, {
|
|
62
|
+
processing: true,
|
|
63
|
+
timestamp: Date.now(),
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
// Trigger login required callback if provided
|
|
67
|
+
if (onLoginRequiredRef.current) {
|
|
68
|
+
onLoginRequiredRef.current(eventData.payload);
|
|
69
|
+
}
|
|
70
|
+
};
|
|
71
|
+
|
|
72
|
+
const handleAnswerCall = async (eventData: CallData) => {
|
|
73
|
+
console.log('CallKitHandler: User answered call via CallKit', {
|
|
74
|
+
callUUID: eventData.callUUID,
|
|
75
|
+
isTrackedCall: activeCallsRef.current.has(eventData.callUUID),
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
if (onNavigateToDialerRef.current) {
|
|
79
|
+
onNavigateToDialerRef.current();
|
|
80
|
+
}
|
|
81
|
+
};
|
|
82
|
+
|
|
83
|
+
const handleEndCall = async (eventData: CallData) => {
|
|
84
|
+
console.log('CallKitHandler: User ended call via CallKit', {
|
|
85
|
+
callUUID: eventData.callUUID,
|
|
86
|
+
isTrackedCall: activeCallsRef.current.has(eventData.callUUID),
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
// Clean up our local tracking info
|
|
90
|
+
activeCallsRef.current.delete(eventData.callUUID);
|
|
91
|
+
await AsyncStorage.removeItem('@push_notification_payload');
|
|
92
|
+
|
|
93
|
+
if (onNavigateBackRef.current) {
|
|
94
|
+
onNavigateBackRef.current();
|
|
95
|
+
}
|
|
96
|
+
};
|
|
97
|
+
|
|
43
98
|
useEffect(() => {
|
|
44
99
|
if (Platform.OS !== 'ios') return;
|
|
45
100
|
|
|
@@ -51,9 +106,6 @@ export const CallKitHandler: React.FC<CallKitHandlerProps> = ({
|
|
|
51
106
|
isCallKitHandlerActive = true;
|
|
52
107
|
console.log('CallKitHandler: Setting up DeviceEventEmitter listeners (singleton instance)...');
|
|
53
108
|
|
|
54
|
-
DeviceEventEmitter.removeAllListeners('incomingVoIPCall');
|
|
55
|
-
DeviceEventEmitter.removeAllListeners('callKitAction');
|
|
56
|
-
|
|
57
109
|
const incomingCallListener = DeviceEventEmitter.addListener(
|
|
58
110
|
'incomingVoIPCall',
|
|
59
111
|
async (eventData: CallData) => {
|
|
@@ -89,53 +141,6 @@ export const CallKitHandler: React.FC<CallKitHandlerProps> = ({
|
|
|
89
141
|
};
|
|
90
142
|
}, []);
|
|
91
143
|
|
|
92
|
-
const handleIncomingCall = async (eventData: CallData) => {
|
|
93
|
-
console.log('CallKitHandler: Handling incoming call', {
|
|
94
|
-
callUUID: eventData.callUUID,
|
|
95
|
-
hasClient: !!voipClient,
|
|
96
|
-
});
|
|
97
|
-
|
|
98
|
-
// Store the push notification payload
|
|
99
|
-
await AsyncStorage.setItem('@push_notification_payload', JSON.stringify(eventData.payload));
|
|
100
|
-
|
|
101
|
-
// Mark this call as being processed
|
|
102
|
-
activeCallsRef.current.set(eventData.callUUID, {
|
|
103
|
-
processing: true,
|
|
104
|
-
timestamp: Date.now(),
|
|
105
|
-
});
|
|
106
|
-
|
|
107
|
-
// Trigger login required callback if provided
|
|
108
|
-
if (onLoginRequired) {
|
|
109
|
-
onLoginRequired(eventData.payload);
|
|
110
|
-
}
|
|
111
|
-
};
|
|
112
|
-
|
|
113
|
-
const handleAnswerCall = async (eventData: CallData) => {
|
|
114
|
-
console.log('CallKitHandler: User answered call via CallKit', {
|
|
115
|
-
callUUID: eventData.callUUID,
|
|
116
|
-
isTrackedCall: activeCallsRef.current.has(eventData.callUUID),
|
|
117
|
-
});
|
|
118
|
-
|
|
119
|
-
if (onNavigateToDialer) {
|
|
120
|
-
onNavigateToDialer();
|
|
121
|
-
}
|
|
122
|
-
};
|
|
123
|
-
|
|
124
|
-
const handleEndCall = async (eventData: CallData) => {
|
|
125
|
-
console.log('CallKitHandler: User ended call via CallKit', {
|
|
126
|
-
callUUID: eventData.callUUID,
|
|
127
|
-
isTrackedCall: activeCallsRef.current.has(eventData.callUUID),
|
|
128
|
-
});
|
|
129
|
-
|
|
130
|
-
// Clean up our local tracking info
|
|
131
|
-
activeCallsRef.current.delete(eventData.callUUID);
|
|
132
|
-
await AsyncStorage.removeItem('@push_notification_payload');
|
|
133
|
-
|
|
134
|
-
if (onNavigateBack) {
|
|
135
|
-
onNavigateBack();
|
|
136
|
-
}
|
|
137
|
-
};
|
|
138
|
-
|
|
139
144
|
// This component doesn't render anything, it just handles events
|
|
140
145
|
return null;
|
|
141
146
|
};
|