@telnyx/react-voice-commons-sdk 0.1.1 → 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 (51) hide show
  1. package/CHANGELOG.md +42 -0
  2. package/README.md +483 -0
  3. package/TelnyxVoiceCommons.podspec +31 -31
  4. package/ios/CallKitBridge.m +43 -43
  5. package/ios/CallKitBridge.swift +874 -879
  6. package/ios/README.md +211 -211
  7. package/ios/VoicePnBridge.m +30 -30
  8. package/ios/VoicePnBridge.swift +86 -86
  9. package/lib/callkit/callkit-coordinator.d.ts +2 -5
  10. package/lib/callkit/callkit-coordinator.js +15 -32
  11. package/lib/callkit/use-callkit-coordinator.d.ts +21 -21
  12. package/lib/callkit/use-callkit-coordinator.js +53 -53
  13. package/lib/hooks/useNetworkStateHandler.d.ts +0 -0
  14. package/lib/hooks/useNetworkStateHandler.js +0 -0
  15. package/lib/internal/calls/call-state-controller.d.ts +2 -10
  16. package/lib/internal/calls/call-state-controller.js +48 -54
  17. package/lib/internal/session/session-manager.d.ts +1 -5
  18. package/lib/internal/session/session-manager.js +35 -25
  19. package/lib/internal/voice-pn-bridge.d.ts +103 -1
  20. package/lib/internal/voice-pn-bridge.js +209 -2
  21. package/lib/models/call-state.d.ts +3 -1
  22. package/lib/models/call-state.js +5 -1
  23. package/lib/models/call.d.ts +31 -3
  24. package/lib/models/call.js +105 -5
  25. package/lib/telnyx-voice-app.js +78 -38
  26. package/lib/telnyx-voip-client.d.ts +4 -2
  27. package/lib/telnyx-voip-client.js +5 -3
  28. package/package.json +111 -104
  29. package/src/callkit/callkit-coordinator.ts +830 -846
  30. package/src/callkit/callkit.ts +322 -322
  31. package/src/callkit/index.ts +4 -4
  32. package/src/callkit/use-callkit.ts +345 -345
  33. package/src/context/TelnyxVoiceContext.tsx +33 -33
  34. package/src/hooks/use-callkit-coordinator.ts +60 -60
  35. package/src/hooks/useAppReadyNotifier.ts +25 -25
  36. package/src/hooks/useAppStateHandler.ts +134 -134
  37. package/src/hooks/useNetworkStateHandler.ts +0 -0
  38. package/src/index.ts +56 -56
  39. package/src/internal/CallKitHandler.tsx +149 -149
  40. package/src/internal/callkit-manager.ts +335 -335
  41. package/src/internal/calls/call-state-controller.ts +407 -384
  42. package/src/internal/session/session-manager.ts +483 -467
  43. package/src/internal/user-defaults-helpers.ts +58 -58
  44. package/src/internal/voice-pn-bridge.ts +266 -18
  45. package/src/models/call-state.ts +105 -98
  46. package/src/models/call.ts +502 -388
  47. package/src/models/config.ts +125 -125
  48. package/src/models/connection-state.ts +50 -50
  49. package/src/telnyx-voice-app.tsx +737 -690
  50. package/src/telnyx-voip-client.ts +551 -539
  51. package/src/types/telnyx-sdk.d.ts +93 -79
@@ -1,345 +1,345 @@
1
- import { useState, useEffect, useCallback, useRef } from 'react';
2
- import { Call } from '@telnyx/react-native-voice-sdk';
3
- import CallKit, { CallEndReason, CallKitEvent } from './callkit';
4
-
5
- interface UseCallKitOptions {
6
- onAnswerCall?: (callUUID: string) => void;
7
- onEndCall?: (callUUID: string) => void;
8
- onStartCall?: (callUUID: string) => void;
9
- }
10
-
11
- interface CallKitCall {
12
- uuid: string;
13
- handle: string;
14
- displayName: string;
15
- isActive: boolean;
16
- direction: 'incoming' | 'outgoing';
17
- }
18
-
19
- export function useCallKit(options: UseCallKitOptions = {}) {
20
- const { onAnswerCall, onEndCall, onStartCall } = options;
21
- const [activeCalls, setActiveCalls] = useState<CallKitCall[]>([]);
22
- const [isAvailable, setIsAvailable] = useState(false);
23
-
24
- // Use refs to store stable callback references and current state
25
- const onAnswerCallRef = useRef(onAnswerCall);
26
- const onEndCallRef = useRef(onEndCall);
27
- const onStartCallRef = useRef(onStartCall);
28
- const activeCallsRef = useRef(activeCalls);
29
-
30
- // Update refs when callbacks change
31
- useEffect(() => {
32
- onAnswerCallRef.current = onAnswerCall;
33
- }, [onAnswerCall]);
34
-
35
- useEffect(() => {
36
- onEndCallRef.current = onEndCall;
37
- }, [onEndCall]);
38
-
39
- useEffect(() => {
40
- onStartCallRef.current = onStartCall;
41
- }, [onStartCall]);
42
-
43
- // Update active calls ref
44
- useEffect(() => {
45
- activeCallsRef.current = activeCalls;
46
- }, [activeCalls]);
47
-
48
- useEffect(() => {
49
- setIsAvailable(CallKit.isAvailable());
50
-
51
- if (!CallKit.isAvailable()) {
52
- console.log('CallKit: Not available on this platform');
53
- return;
54
- }
55
-
56
- // Load existing active calls only once
57
- const loadActiveCalls = async () => {
58
- const calls = await CallKit.getActiveCalls();
59
- setActiveCalls(
60
- calls.map((call) => ({
61
- uuid: call.uuid,
62
- handle: call.handle || call.caller || 'Unknown',
63
- displayName: call.caller || call.displayName || 'Unknown Caller',
64
- isActive: true,
65
- direction: call.direction || 'incoming',
66
- }))
67
- );
68
- };
69
-
70
- loadActiveCalls();
71
-
72
- // Set up event listeners using stable refs
73
- const unsubscribeAnswer = CallKit.onAnswerCall((event: CallKitEvent) => {
74
- console.log('useCallKit: Call answered via CallKit', event);
75
- onAnswerCallRef.current?.(event.callUUID);
76
- });
77
-
78
- const unsubscribeEnd = CallKit.onEndCall((event: CallKitEvent) => {
79
- console.log('useCallKit: Call ended via CallKit', event);
80
- setActiveCalls((prev) => prev.filter((call) => call.uuid !== event.callUUID));
81
- onEndCallRef.current?.(event.callUUID);
82
- });
83
-
84
- const unsubscribeStart = CallKit.onStartCall((event: CallKitEvent) => {
85
- console.log('useCallKit: Call started via CallKit', event);
86
- onStartCallRef.current?.(event.callUUID);
87
- });
88
-
89
- return () => {
90
- unsubscribeAnswer();
91
- unsubscribeEnd();
92
- unsubscribeStart();
93
- };
94
- }, []); // Empty dependency array - only run once
95
-
96
- // Start an outgoing call
97
- const startOutgoingCall = useCallback(
98
- async (call: Call, handle?: string, displayName?: string): Promise<string | null> => {
99
- if (!CallKit.isAvailable()) {
100
- console.warn('CallKit: Not available, cannot start outgoing call');
101
- return null;
102
- }
103
-
104
- const callUUID = CallKit.generateCallUUID();
105
- const callHandle = handle || (call as any).destinationNumber || 'Unknown';
106
- const callDisplayName = displayName || callHandle;
107
-
108
- console.log('useCallKit: Starting outgoing call', {
109
- callUUID,
110
- callHandle,
111
- callDisplayName,
112
- telnyxCallId: call.callId,
113
- });
114
-
115
- const success = await CallKit.startOutgoingCall(callUUID, callHandle, callDisplayName);
116
-
117
- if (success) {
118
- const newCall: CallKitCall = {
119
- uuid: callUUID,
120
- handle: callHandle,
121
- displayName: callDisplayName,
122
- isActive: false,
123
- direction: 'outgoing',
124
- };
125
-
126
- setActiveCalls((prev) => [...prev, newCall]);
127
-
128
- // Store mapping between CallKit UUID and Telnyx call for later reference
129
- (call as any)._callKitUUID = callUUID;
130
-
131
- return callUUID;
132
- }
133
-
134
- return null;
135
- },
136
- []
137
- );
138
-
139
- // Report an incoming call
140
- const reportIncomingCall = useCallback(
141
- async (call: Call, handle?: string, displayName?: string): Promise<string | null> => {
142
- if (!CallKit.isAvailable()) {
143
- console.warn('CallKit: Not available, cannot report incoming call');
144
- return null;
145
- }
146
-
147
- const callUUID = CallKit.generateCallUUID();
148
- const callHandle = handle || (call as any).destinationNumber || call.callId || 'Unknown';
149
- const callDisplayName = displayName || callHandle;
150
-
151
- console.log('useCallKit: Reporting incoming call', {
152
- callUUID,
153
- callHandle,
154
- callDisplayName,
155
- telnyxCallId: call.callId,
156
- });
157
-
158
- const success = await CallKit.reportIncomingCall(callUUID, callHandle, callDisplayName);
159
-
160
- if (success) {
161
- const newCall: CallKitCall = {
162
- uuid: callUUID,
163
- handle: callHandle,
164
- displayName: callDisplayName,
165
- isActive: false,
166
- direction: 'incoming',
167
- };
168
-
169
- setActiveCalls((prev) => [...prev, newCall]);
170
-
171
- // Store mapping between CallKit UUID and Telnyx call for later reference
172
- (call as any)._callKitUUID = callUUID;
173
-
174
- return callUUID;
175
- }
176
-
177
- return null;
178
- },
179
- []
180
- );
181
-
182
- // End a call
183
- const endCall = useCallback(
184
- async (
185
- callUUID: string,
186
- reason: CallEndReason = CallEndReason.RemoteEnded
187
- ): Promise<boolean> => {
188
- if (!CallKit.isAvailable()) {
189
- return false;
190
- }
191
-
192
- console.log('useCallKit: Ending call', { callUUID, reason });
193
-
194
- try {
195
- // For incoming calls, we should use reportCallEnded (which dismisses the UI)
196
- // For outgoing calls, we should use endCall (which sends the end request)
197
-
198
- // Check if this is likely an incoming call by looking at our active calls
199
- const activeCall = activeCallsRef.current.find((call) => call.uuid === callUUID);
200
- const isIncomingCall = activeCall?.direction === 'incoming';
201
-
202
- if (isIncomingCall) {
203
- await CallKit.reportCallEnded(callUUID, reason);
204
- } else {
205
- await CallKit.endCall(callUUID);
206
- }
207
-
208
- // Update our local state
209
- setActiveCalls((prev) => prev.filter((call) => call.uuid !== callUUID));
210
- return true;
211
- } catch (error) {
212
- console.log('useCallKit: Error ending call (may already be ended):', error);
213
-
214
- // Still remove from our local state even if CallKit operation failed
215
- // This ensures our UI stays in sync
216
- setActiveCalls((prev) => prev.filter((call) => call.uuid !== callUUID));
217
-
218
- // Return true since the call is effectively ended from our perspective
219
- return true;
220
- }
221
- },
222
- []
223
- );
224
-
225
- // Report call connected
226
- const reportCallConnected = useCallback(async (callUUID: string): Promise<boolean> => {
227
- if (!CallKit.isAvailable()) {
228
- return false;
229
- }
230
-
231
- console.log('useCallKit: Reporting call connected', { callUUID });
232
-
233
- const success = await CallKit.reportCallConnected(callUUID);
234
-
235
- if (success) {
236
- // Update our local state to mark the call as active
237
- setActiveCalls((prev) =>
238
- prev.map((call) => (call.uuid === callUUID ? { ...call, isActive: true } : call))
239
- );
240
- }
241
-
242
- return success;
243
- }, []);
244
-
245
- // Update call information
246
- const updateCall = useCallback(
247
- async (callUUID: string, displayName: string, handle: string): Promise<boolean> => {
248
- if (!CallKit.isAvailable()) {
249
- return false;
250
- }
251
-
252
- console.log('useCallKit: Updating call', { callUUID, displayName, handle });
253
-
254
- const success = await CallKit.updateCall(callUUID, displayName, handle);
255
-
256
- if (success) {
257
- // Update our local state
258
- setActiveCalls((prev) =>
259
- prev.map((call) => (call.uuid === callUUID ? { ...call, displayName, handle } : call))
260
- );
261
- }
262
-
263
- return success;
264
- },
265
- []
266
- );
267
-
268
- // Answer a call
269
- const answerCall = useCallback(async (callUUID: string): Promise<boolean> => {
270
- if (!CallKit.isAvailable()) {
271
- return false;
272
- }
273
-
274
- console.log('useCallKit: Answering call', { callUUID });
275
-
276
- const success = await CallKit.answerCall(callUUID);
277
-
278
- if (success) {
279
- // Update our local state to mark call as active
280
- setActiveCalls((prev) =>
281
- prev.map((call) => (call.uuid === callUUID ? { ...call, isActive: true } : call))
282
- );
283
- }
284
-
285
- return success;
286
- }, []);
287
-
288
- // Get CallKit UUID for a Telnyx call
289
- const getCallKitUUID = useCallback((call: Call): string | null => {
290
- return (call as any)._callKitUUID || null;
291
- }, []);
292
-
293
- // Set up automatic CallKit integration for a Telnyx call
294
- const integrateCall = useCallback(
295
- async (call: Call, direction: 'incoming' | 'outgoing') => {
296
- if (!CallKit.isAvailable()) {
297
- return null;
298
- }
299
-
300
- let callUUID: string | null = null;
301
-
302
- if (direction === 'incoming') {
303
- callUUID = await reportIncomingCall(call);
304
- } else {
305
- callUUID = await startOutgoingCall(call);
306
- }
307
-
308
- if (callUUID) {
309
- // Set up automatic state reporting
310
- const handleStateChange = async (call: Call, state: string) => {
311
- if (state === 'active') {
312
- await reportCallConnected(callUUID!);
313
- } else if (state === 'ended' || state === 'failed') {
314
- const reason = state === 'failed' ? CallEndReason.Failed : CallEndReason.RemoteEnded;
315
- await endCall(callUUID!, reason);
316
- }
317
- };
318
-
319
- call.on('telnyx.call.state', handleStateChange);
320
- }
321
-
322
- return callUUID;
323
- },
324
- [reportIncomingCall, startOutgoingCall, reportCallConnected, endCall]
325
- );
326
-
327
- return {
328
- // State
329
- isAvailable,
330
- activeCalls,
331
-
332
- // Methods
333
- startOutgoingCall,
334
- reportIncomingCall,
335
- answerCall,
336
- endCall,
337
- reportCallConnected,
338
- updateCall,
339
- getCallKitUUID,
340
- integrateCall,
341
-
342
- // Utility
343
- generateCallUUID: CallKit.generateCallUUID,
344
- };
345
- }
1
+ import { useState, useEffect, useCallback, useRef } from 'react';
2
+ import { Call } from '@telnyx/react-native-voice-sdk';
3
+ import CallKit, { CallEndReason, CallKitEvent } from './callkit';
4
+
5
+ interface UseCallKitOptions {
6
+ onAnswerCall?: (callUUID: string) => void;
7
+ onEndCall?: (callUUID: string) => void;
8
+ onStartCall?: (callUUID: string) => void;
9
+ }
10
+
11
+ interface CallKitCall {
12
+ uuid: string;
13
+ handle: string;
14
+ displayName: string;
15
+ isActive: boolean;
16
+ direction: 'incoming' | 'outgoing';
17
+ }
18
+
19
+ export function useCallKit(options: UseCallKitOptions = {}) {
20
+ const { onAnswerCall, onEndCall, onStartCall } = options;
21
+ const [activeCalls, setActiveCalls] = useState<CallKitCall[]>([]);
22
+ const [isAvailable, setIsAvailable] = useState(false);
23
+
24
+ // Use refs to store stable callback references and current state
25
+ const onAnswerCallRef = useRef(onAnswerCall);
26
+ const onEndCallRef = useRef(onEndCall);
27
+ const onStartCallRef = useRef(onStartCall);
28
+ const activeCallsRef = useRef(activeCalls);
29
+
30
+ // Update refs when callbacks change
31
+ useEffect(() => {
32
+ onAnswerCallRef.current = onAnswerCall;
33
+ }, [onAnswerCall]);
34
+
35
+ useEffect(() => {
36
+ onEndCallRef.current = onEndCall;
37
+ }, [onEndCall]);
38
+
39
+ useEffect(() => {
40
+ onStartCallRef.current = onStartCall;
41
+ }, [onStartCall]);
42
+
43
+ // Update active calls ref
44
+ useEffect(() => {
45
+ activeCallsRef.current = activeCalls;
46
+ }, [activeCalls]);
47
+
48
+ useEffect(() => {
49
+ setIsAvailable(CallKit.isAvailable());
50
+
51
+ if (!CallKit.isAvailable()) {
52
+ console.log('CallKit: Not available on this platform');
53
+ return;
54
+ }
55
+
56
+ // Load existing active calls only once
57
+ const loadActiveCalls = async () => {
58
+ const calls = await CallKit.getActiveCalls();
59
+ setActiveCalls(
60
+ calls.map((call) => ({
61
+ uuid: call.uuid,
62
+ handle: call.handle || call.caller || 'Unknown',
63
+ displayName: call.caller || call.displayName || 'Unknown Caller',
64
+ isActive: true,
65
+ direction: call.direction || 'incoming',
66
+ }))
67
+ );
68
+ };
69
+
70
+ loadActiveCalls();
71
+
72
+ // Set up event listeners using stable refs
73
+ const unsubscribeAnswer = CallKit.onAnswerCall((event: CallKitEvent) => {
74
+ console.log('useCallKit: Call answered via CallKit', event);
75
+ onAnswerCallRef.current?.(event.callUUID);
76
+ });
77
+
78
+ const unsubscribeEnd = CallKit.onEndCall((event: CallKitEvent) => {
79
+ console.log('useCallKit: Call ended via CallKit', event);
80
+ setActiveCalls((prev) => prev.filter((call) => call.uuid !== event.callUUID));
81
+ onEndCallRef.current?.(event.callUUID);
82
+ });
83
+
84
+ const unsubscribeStart = CallKit.onStartCall((event: CallKitEvent) => {
85
+ console.log('useCallKit: Call started via CallKit', event);
86
+ onStartCallRef.current?.(event.callUUID);
87
+ });
88
+
89
+ return () => {
90
+ unsubscribeAnswer();
91
+ unsubscribeEnd();
92
+ unsubscribeStart();
93
+ };
94
+ }, []); // Empty dependency array - only run once
95
+
96
+ // Start an outgoing call
97
+ const startOutgoingCall = useCallback(
98
+ async (call: Call, handle?: string, displayName?: string): Promise<string | null> => {
99
+ if (!CallKit.isAvailable()) {
100
+ console.warn('CallKit: Not available, cannot start outgoing call');
101
+ return null;
102
+ }
103
+
104
+ const callUUID = CallKit.generateCallUUID();
105
+ const callHandle = handle || (call as any).destinationNumber || 'Unknown';
106
+ const callDisplayName = displayName || callHandle;
107
+
108
+ console.log('useCallKit: Starting outgoing call', {
109
+ callUUID,
110
+ callHandle,
111
+ callDisplayName,
112
+ telnyxCallId: call.callId,
113
+ });
114
+
115
+ const success = await CallKit.startOutgoingCall(callUUID, callHandle, callDisplayName);
116
+
117
+ if (success) {
118
+ const newCall: CallKitCall = {
119
+ uuid: callUUID,
120
+ handle: callHandle,
121
+ displayName: callDisplayName,
122
+ isActive: false,
123
+ direction: 'outgoing',
124
+ };
125
+
126
+ setActiveCalls((prev) => [...prev, newCall]);
127
+
128
+ // Store mapping between CallKit UUID and Telnyx call for later reference
129
+ (call as any)._callKitUUID = callUUID;
130
+
131
+ return callUUID;
132
+ }
133
+
134
+ return null;
135
+ },
136
+ []
137
+ );
138
+
139
+ // Report an incoming call
140
+ const reportIncomingCall = useCallback(
141
+ async (call: Call, handle?: string, displayName?: string): Promise<string | null> => {
142
+ if (!CallKit.isAvailable()) {
143
+ console.warn('CallKit: Not available, cannot report incoming call');
144
+ return null;
145
+ }
146
+
147
+ const callUUID = CallKit.generateCallUUID();
148
+ const callHandle = handle || (call as any).destinationNumber || call.callId || 'Unknown';
149
+ const callDisplayName = displayName || callHandle;
150
+
151
+ console.log('useCallKit: Reporting incoming call', {
152
+ callUUID,
153
+ callHandle,
154
+ callDisplayName,
155
+ telnyxCallId: call.callId,
156
+ });
157
+
158
+ const success = await CallKit.reportIncomingCall(callUUID, callHandle, callDisplayName);
159
+
160
+ if (success) {
161
+ const newCall: CallKitCall = {
162
+ uuid: callUUID,
163
+ handle: callHandle,
164
+ displayName: callDisplayName,
165
+ isActive: false,
166
+ direction: 'incoming',
167
+ };
168
+
169
+ setActiveCalls((prev) => [...prev, newCall]);
170
+
171
+ // Store mapping between CallKit UUID and Telnyx call for later reference
172
+ (call as any)._callKitUUID = callUUID;
173
+
174
+ return callUUID;
175
+ }
176
+
177
+ return null;
178
+ },
179
+ []
180
+ );
181
+
182
+ // End a call
183
+ const endCall = useCallback(
184
+ async (
185
+ callUUID: string,
186
+ reason: CallEndReason = CallEndReason.RemoteEnded
187
+ ): Promise<boolean> => {
188
+ if (!CallKit.isAvailable()) {
189
+ return false;
190
+ }
191
+
192
+ console.log('useCallKit: Ending call', { callUUID, reason });
193
+
194
+ try {
195
+ // For incoming calls, we should use reportCallEnded (which dismisses the UI)
196
+ // For outgoing calls, we should use endCall (which sends the end request)
197
+
198
+ // Check if this is likely an incoming call by looking at our active calls
199
+ const activeCall = activeCallsRef.current.find((call) => call.uuid === callUUID);
200
+ const isIncomingCall = activeCall?.direction === 'incoming';
201
+
202
+ if (isIncomingCall) {
203
+ await CallKit.reportCallEnded(callUUID, reason);
204
+ } else {
205
+ await CallKit.endCall(callUUID);
206
+ }
207
+
208
+ // Update our local state
209
+ setActiveCalls((prev) => prev.filter((call) => call.uuid !== callUUID));
210
+ return true;
211
+ } catch (error) {
212
+ console.log('useCallKit: Error ending call (may already be ended):', error);
213
+
214
+ // Still remove from our local state even if CallKit operation failed
215
+ // This ensures our UI stays in sync
216
+ setActiveCalls((prev) => prev.filter((call) => call.uuid !== callUUID));
217
+
218
+ // Return true since the call is effectively ended from our perspective
219
+ return true;
220
+ }
221
+ },
222
+ []
223
+ );
224
+
225
+ // Report call connected
226
+ const reportCallConnected = useCallback(async (callUUID: string): Promise<boolean> => {
227
+ if (!CallKit.isAvailable()) {
228
+ return false;
229
+ }
230
+
231
+ console.log('useCallKit: Reporting call connected', { callUUID });
232
+
233
+ const success = await CallKit.reportCallConnected(callUUID);
234
+
235
+ if (success) {
236
+ // Update our local state to mark the call as active
237
+ setActiveCalls((prev) =>
238
+ prev.map((call) => (call.uuid === callUUID ? { ...call, isActive: true } : call))
239
+ );
240
+ }
241
+
242
+ return success;
243
+ }, []);
244
+
245
+ // Update call information
246
+ const updateCall = useCallback(
247
+ async (callUUID: string, displayName: string, handle: string): Promise<boolean> => {
248
+ if (!CallKit.isAvailable()) {
249
+ return false;
250
+ }
251
+
252
+ console.log('useCallKit: Updating call', { callUUID, displayName, handle });
253
+
254
+ const success = await CallKit.updateCall(callUUID, displayName, handle);
255
+
256
+ if (success) {
257
+ // Update our local state
258
+ setActiveCalls((prev) =>
259
+ prev.map((call) => (call.uuid === callUUID ? { ...call, displayName, handle } : call))
260
+ );
261
+ }
262
+
263
+ return success;
264
+ },
265
+ []
266
+ );
267
+
268
+ // Answer a call
269
+ const answerCall = useCallback(async (callUUID: string): Promise<boolean> => {
270
+ if (!CallKit.isAvailable()) {
271
+ return false;
272
+ }
273
+
274
+ console.log('useCallKit: Answering call', { callUUID });
275
+
276
+ const success = await CallKit.answerCall(callUUID);
277
+
278
+ if (success) {
279
+ // Update our local state to mark call as active
280
+ setActiveCalls((prev) =>
281
+ prev.map((call) => (call.uuid === callUUID ? { ...call, isActive: true } : call))
282
+ );
283
+ }
284
+
285
+ return success;
286
+ }, []);
287
+
288
+ // Get CallKit UUID for a Telnyx call
289
+ const getCallKitUUID = useCallback((call: Call): string | null => {
290
+ return (call as any)._callKitUUID || null;
291
+ }, []);
292
+
293
+ // Set up automatic CallKit integration for a Telnyx call
294
+ const integrateCall = useCallback(
295
+ async (call: Call, direction: 'incoming' | 'outgoing') => {
296
+ if (!CallKit.isAvailable()) {
297
+ return null;
298
+ }
299
+
300
+ let callUUID: string | null = null;
301
+
302
+ if (direction === 'incoming') {
303
+ callUUID = await reportIncomingCall(call);
304
+ } else {
305
+ callUUID = await startOutgoingCall(call);
306
+ }
307
+
308
+ if (callUUID) {
309
+ // Set up automatic state reporting
310
+ const handleStateChange = async (call: Call, state: string) => {
311
+ if (state === 'active') {
312
+ await reportCallConnected(callUUID!);
313
+ } else if (state === 'ended' || state === 'failed') {
314
+ const reason = state === 'failed' ? CallEndReason.Failed : CallEndReason.RemoteEnded;
315
+ await endCall(callUUID!, reason);
316
+ }
317
+ };
318
+
319
+ call.on('telnyx.call.state', handleStateChange);
320
+ }
321
+
322
+ return callUUID;
323
+ },
324
+ [reportIncomingCall, startOutgoingCall, reportCallConnected, endCall]
325
+ );
326
+
327
+ return {
328
+ // State
329
+ isAvailable,
330
+ activeCalls,
331
+
332
+ // Methods
333
+ startOutgoingCall,
334
+ reportIncomingCall,
335
+ answerCall,
336
+ endCall,
337
+ reportCallConnected,
338
+ updateCall,
339
+ getCallKitUUID,
340
+ integrateCall,
341
+
342
+ // Utility
343
+ generateCallUUID: CallKit.generateCallUUID,
344
+ };
345
+ }