@telnyx/react-voice-commons-sdk 0.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/TelnyxVoiceCommons.podspec +32 -0
- package/ios/CallKitBridge.m +44 -0
- package/ios/CallKitBridge.swift +879 -0
- package/ios/README.md +211 -0
- package/ios/VoicePnBridge.m +31 -0
- package/ios/VoicePnBridge.swift +87 -0
- package/lib/callkit/callkit-coordinator.d.ts +126 -0
- package/lib/callkit/callkit-coordinator.js +728 -0
- package/lib/callkit/callkit.d.ts +49 -0
- package/lib/callkit/callkit.js +262 -0
- package/lib/callkit/index.d.ts +4 -0
- package/lib/callkit/index.js +15 -0
- package/lib/callkit/use-callkit-coordinator.d.ts +21 -0
- package/lib/callkit/use-callkit-coordinator.js +53 -0
- package/lib/callkit/use-callkit.d.ts +28 -0
- package/lib/callkit/use-callkit.js +279 -0
- package/lib/context/TelnyxVoiceContext.d.ts +18 -0
- package/lib/context/TelnyxVoiceContext.js +18 -0
- package/lib/hooks/use-callkit-coordinator.d.ts +13 -0
- package/lib/hooks/use-callkit-coordinator.js +48 -0
- package/lib/hooks/useAppReadyNotifier.d.ts +9 -0
- package/lib/hooks/useAppReadyNotifier.js +25 -0
- package/lib/hooks/useAppStateHandler.d.ts +16 -0
- package/lib/hooks/useAppStateHandler.js +105 -0
- package/lib/index.d.ts +24 -0
- package/lib/index.js +66 -0
- package/lib/internal/CallKitHandler.d.ts +17 -0
- package/lib/internal/CallKitHandler.js +110 -0
- package/lib/internal/callkit-manager.d.ts +69 -0
- package/lib/internal/callkit-manager.js +326 -0
- package/lib/internal/calls/call-state-controller.d.ts +92 -0
- package/lib/internal/calls/call-state-controller.js +294 -0
- package/lib/internal/session/session-manager.d.ts +87 -0
- package/lib/internal/session/session-manager.js +385 -0
- package/lib/internal/user-defaults-helpers.d.ts +10 -0
- package/lib/internal/user-defaults-helpers.js +69 -0
- package/lib/internal/voice-pn-bridge.d.ts +14 -0
- package/lib/internal/voice-pn-bridge.js +5 -0
- package/lib/models/call-state.d.ts +61 -0
- package/lib/models/call-state.js +87 -0
- package/lib/models/call.d.ts +145 -0
- package/lib/models/call.js +372 -0
- package/lib/models/config.d.ts +64 -0
- package/lib/models/config.js +92 -0
- package/lib/models/connection-state.d.ts +34 -0
- package/lib/models/connection-state.js +50 -0
- package/lib/telnyx-voice-app.d.ts +48 -0
- package/lib/telnyx-voice-app.js +486 -0
- package/lib/telnyx-voip-client.d.ts +184 -0
- package/lib/telnyx-voip-client.js +386 -0
- package/package.json +104 -0
- package/src/callkit/callkit-coordinator.ts +846 -0
- package/src/callkit/callkit.ts +322 -0
- package/src/callkit/index.ts +4 -0
- package/src/callkit/use-callkit.ts +345 -0
- package/src/context/TelnyxVoiceContext.tsx +33 -0
- package/src/hooks/use-callkit-coordinator.ts +60 -0
- package/src/hooks/useAppReadyNotifier.ts +25 -0
- package/src/hooks/useAppStateHandler.ts +134 -0
- package/src/index.ts +56 -0
- package/src/internal/CallKitHandler.tsx +149 -0
- package/src/internal/callkit-manager.ts +335 -0
- package/src/internal/calls/call-state-controller.ts +384 -0
- package/src/internal/session/session-manager.ts +467 -0
- package/src/internal/user-defaults-helpers.ts +58 -0
- package/src/internal/voice-pn-bridge.ts +18 -0
- package/src/models/call-state.ts +98 -0
- package/src/models/call.ts +388 -0
- package/src/models/config.ts +125 -0
- package/src/models/connection-state.ts +50 -0
- package/src/telnyx-voice-app.tsx +690 -0
- package/src/telnyx-voip-client.ts +475 -0
- package/src/types/telnyx-sdk.d.ts +79 -0
|
@@ -0,0 +1,322 @@
|
|
|
1
|
+
import { Platform, NativeModules, NativeEventEmitter } from 'react-native';
|
|
2
|
+
|
|
3
|
+
interface CallKitBridgeInterface {
|
|
4
|
+
startOutgoingCall(
|
|
5
|
+
callUUID: string,
|
|
6
|
+
handle: string,
|
|
7
|
+
displayName: string
|
|
8
|
+
): Promise<{ success: boolean; callUUID: string }>;
|
|
9
|
+
reportIncomingCall(
|
|
10
|
+
callUUID: string,
|
|
11
|
+
handle: string,
|
|
12
|
+
displayName: string
|
|
13
|
+
): Promise<{ success: boolean; callUUID: string }>;
|
|
14
|
+
answerCall(callUUID: string): Promise<{ success: boolean; callUUID: string }>;
|
|
15
|
+
endCall(callUUID: string): Promise<{ success: boolean; callUUID: string }>;
|
|
16
|
+
reportCallConnected(callUUID: string): Promise<{ success: boolean }>;
|
|
17
|
+
reportCallEnded(callUUID: string, reason: number): Promise<{ success: boolean }>;
|
|
18
|
+
updateCall(callUUID: string, displayName: string, handle: string): Promise<{ success: boolean }>;
|
|
19
|
+
getActiveCalls(): Promise<any[]>;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
// Call end reasons
|
|
23
|
+
export enum CallEndReason {
|
|
24
|
+
Failed = 1,
|
|
25
|
+
RemoteEnded = 2,
|
|
26
|
+
Unanswered = 3,
|
|
27
|
+
AnsweredElsewhere = 4,
|
|
28
|
+
DeclinedElsewhere = 5,
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
// CallKit event types
|
|
32
|
+
export interface CallKitEvent {
|
|
33
|
+
callUUID: string;
|
|
34
|
+
[key: string]: any;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
class CallKitManager {
|
|
38
|
+
private bridge: CallKitBridgeInterface | null = null;
|
|
39
|
+
private eventEmitter: NativeEventEmitter | null = null;
|
|
40
|
+
private listeners: Map<string, (event: CallKitEvent) => void> = new Map();
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Normalize UUID to lowercase for consistent handling in React Native
|
|
44
|
+
* iOS CallKit provides UUIDs in uppercase, but we want to use lowercase throughout React Native
|
|
45
|
+
*/
|
|
46
|
+
private normalizeUUID(uuid: string): string {
|
|
47
|
+
return uuid.toLowerCase();
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Denormalize UUID back to uppercase for iOS CallKit bridge calls
|
|
52
|
+
* iOS CallKit expects UUIDs in uppercase format
|
|
53
|
+
*/
|
|
54
|
+
private denormalizeUUID(uuid: string): string {
|
|
55
|
+
return uuid.toUpperCase();
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Normalize event object by converting callUUID to lowercase
|
|
60
|
+
*/
|
|
61
|
+
private normalizeEvent(event: CallKitEvent): CallKitEvent {
|
|
62
|
+
return {
|
|
63
|
+
...event,
|
|
64
|
+
callUUID: this.normalizeUUID(event.callUUID),
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
constructor() {
|
|
69
|
+
if (Platform.OS === 'ios') {
|
|
70
|
+
const { CallKitBridge } = NativeModules;
|
|
71
|
+
if (CallKitBridge) {
|
|
72
|
+
this.bridge = CallKitBridge;
|
|
73
|
+
this.eventEmitter = new NativeEventEmitter(CallKitBridge);
|
|
74
|
+
this.setupEventListeners();
|
|
75
|
+
} else {
|
|
76
|
+
console.warn('CallKit: CallKitBridge not available');
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
private setupEventListeners() {
|
|
82
|
+
if (!this.eventEmitter) return;
|
|
83
|
+
|
|
84
|
+
// Listen for CallKit actions - normalize UUIDs to lowercase for React Native
|
|
85
|
+
this.eventEmitter.addListener('CallKitDidReceiveStartCallAction', (event) => {
|
|
86
|
+
const normalizedEvent = this.normalizeEvent(event);
|
|
87
|
+
console.log('CallKit: Received start call action', normalizedEvent);
|
|
88
|
+
this.notifyListeners('startCall', normalizedEvent);
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
this.eventEmitter.addListener('CallKitDidPerformAnswerCallAction', (event) => {
|
|
92
|
+
const normalizedEvent = this.normalizeEvent(event);
|
|
93
|
+
console.log('CallKit: Received answer call action', normalizedEvent);
|
|
94
|
+
this.notifyListeners('answerCall', normalizedEvent);
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
this.eventEmitter.addListener('CallKitDidPerformEndCallAction', (event) => {
|
|
98
|
+
const normalizedEvent = this.normalizeEvent(event);
|
|
99
|
+
console.log('CallKit: Received end call action', normalizedEvent);
|
|
100
|
+
this.notifyListeners('endCall', normalizedEvent);
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
this.eventEmitter.addListener('CallKitDidReceivePush', (event) => {
|
|
104
|
+
const normalizedEvent = this.normalizeEvent(event);
|
|
105
|
+
console.log('CallKit: Received push notification event', normalizedEvent);
|
|
106
|
+
this.notifyListeners('receivePush', normalizedEvent);
|
|
107
|
+
});
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
private notifyListeners(eventType: string, event: CallKitEvent) {
|
|
111
|
+
const listener = this.listeners.get(eventType);
|
|
112
|
+
if (listener) {
|
|
113
|
+
listener(event);
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
// Public API methods
|
|
118
|
+
|
|
119
|
+
public async startOutgoingCall(
|
|
120
|
+
callUUID: string,
|
|
121
|
+
handle: string,
|
|
122
|
+
displayName: string
|
|
123
|
+
): Promise<boolean> {
|
|
124
|
+
if (!this.bridge || Platform.OS !== 'ios') {
|
|
125
|
+
console.warn('CallKit: Not available on this platform');
|
|
126
|
+
return false;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
try {
|
|
130
|
+
// Convert to uppercase for iOS CallKit bridge
|
|
131
|
+
const uppercaseUUID = this.denormalizeUUID(callUUID);
|
|
132
|
+
console.log('CallKit: Starting outgoing call', {
|
|
133
|
+
callUUID: uppercaseUUID,
|
|
134
|
+
handle,
|
|
135
|
+
displayName,
|
|
136
|
+
});
|
|
137
|
+
const result = await this.bridge.startOutgoingCall(uppercaseUUID, handle, displayName);
|
|
138
|
+
console.log('CallKit: Outgoing call started successfully', result);
|
|
139
|
+
return result.success;
|
|
140
|
+
} catch (error) {
|
|
141
|
+
console.error('CallKit: Failed to start outgoing call', error);
|
|
142
|
+
return false;
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
public async reportIncomingCall(
|
|
147
|
+
callUUID: string,
|
|
148
|
+
handle: string,
|
|
149
|
+
displayName: string
|
|
150
|
+
): Promise<boolean> {
|
|
151
|
+
if (!this.bridge || Platform.OS !== 'ios') {
|
|
152
|
+
console.warn('CallKit: Not available on this platform');
|
|
153
|
+
return false;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
try {
|
|
157
|
+
// Convert to uppercase for iOS CallKit bridge
|
|
158
|
+
const uppercaseUUID = this.denormalizeUUID(callUUID);
|
|
159
|
+
console.log('CallKit: Reporting incoming call', {
|
|
160
|
+
callUUID: uppercaseUUID,
|
|
161
|
+
handle,
|
|
162
|
+
displayName,
|
|
163
|
+
});
|
|
164
|
+
const result = await this.bridge.reportIncomingCall(uppercaseUUID, handle, displayName);
|
|
165
|
+
console.log('CallKit: Incoming call reported successfully', result);
|
|
166
|
+
return result.success;
|
|
167
|
+
} catch (error) {
|
|
168
|
+
console.error('CallKit: Failed to report incoming call', error);
|
|
169
|
+
return false;
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
public async answerCall(callUUID: string): Promise<boolean> {
|
|
174
|
+
if (!this.bridge || Platform.OS !== 'ios') {
|
|
175
|
+
console.warn('CallKit: Not available on this platform');
|
|
176
|
+
return false;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
try {
|
|
180
|
+
// Convert to uppercase for iOS CallKit bridge
|
|
181
|
+
const uppercaseUUID = this.denormalizeUUID(callUUID);
|
|
182
|
+
console.log('CallKit: Answering call', { callUUID: uppercaseUUID });
|
|
183
|
+
const result = await this.bridge.answerCall(uppercaseUUID);
|
|
184
|
+
console.log('CallKit: Call answered successfully', result);
|
|
185
|
+
return result.success;
|
|
186
|
+
} catch (error) {
|
|
187
|
+
console.error('CallKit: Failed to answer call', error);
|
|
188
|
+
return false;
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
public async endCall(callUUID: string): Promise<boolean> {
|
|
193
|
+
if (!this.bridge || Platform.OS !== 'ios') {
|
|
194
|
+
console.warn('CallKit: Not available on this platform');
|
|
195
|
+
return false;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
try {
|
|
199
|
+
// Convert to uppercase for iOS CallKit bridge
|
|
200
|
+
const uppercaseUUID = this.denormalizeUUID(callUUID);
|
|
201
|
+
console.log('CallKit: Ending call', { callUUID: uppercaseUUID });
|
|
202
|
+
const result = await this.bridge.endCall(uppercaseUUID);
|
|
203
|
+
console.log('CallKit: Call ended successfully', result);
|
|
204
|
+
return result.success;
|
|
205
|
+
} catch (error) {
|
|
206
|
+
console.error('CallKit: Failed to end call', error);
|
|
207
|
+
return false;
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
public async reportCallConnected(callUUID: string): Promise<boolean> {
|
|
212
|
+
if (!this.bridge || Platform.OS !== 'ios') {
|
|
213
|
+
return false;
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
try {
|
|
217
|
+
// Convert to uppercase for iOS CallKit bridge
|
|
218
|
+
const uppercaseUUID = this.denormalizeUUID(callUUID);
|
|
219
|
+
console.log('CallKit: Reporting call connected', { callUUID: uppercaseUUID });
|
|
220
|
+
const result = await this.bridge.reportCallConnected(uppercaseUUID);
|
|
221
|
+
console.log('CallKit: Call connected reported successfully', result);
|
|
222
|
+
return result.success;
|
|
223
|
+
} catch (error) {
|
|
224
|
+
console.error('CallKit: Failed to report call connected', error);
|
|
225
|
+
return false;
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
public async reportCallEnded(
|
|
230
|
+
callUUID: string,
|
|
231
|
+
reason: CallEndReason = CallEndReason.RemoteEnded
|
|
232
|
+
): Promise<boolean> {
|
|
233
|
+
if (!this.bridge || Platform.OS !== 'ios') {
|
|
234
|
+
return false;
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
try {
|
|
238
|
+
// Convert to uppercase for iOS CallKit bridge
|
|
239
|
+
const uppercaseUUID = this.denormalizeUUID(callUUID);
|
|
240
|
+
console.log('CallKit: Reporting call ended', { callUUID: uppercaseUUID, reason });
|
|
241
|
+
const result = await this.bridge.reportCallEnded(uppercaseUUID, reason);
|
|
242
|
+
console.log('CallKit: Call ended reported successfully', result);
|
|
243
|
+
return result.success;
|
|
244
|
+
} catch (error) {
|
|
245
|
+
console.error('CallKit: Failed to report call ended', error);
|
|
246
|
+
return false;
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
public async updateCall(callUUID: string, displayName: string, handle: string): Promise<boolean> {
|
|
251
|
+
if (!this.bridge || Platform.OS !== 'ios') {
|
|
252
|
+
return false;
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
try {
|
|
256
|
+
// Convert to uppercase for iOS CallKit bridge
|
|
257
|
+
const uppercaseUUID = this.denormalizeUUID(callUUID);
|
|
258
|
+
console.log('CallKit: Updating call', { callUUID: uppercaseUUID, displayName, handle });
|
|
259
|
+
const result = await this.bridge.updateCall(uppercaseUUID, displayName, handle);
|
|
260
|
+
console.log('CallKit: Call updated successfully', result);
|
|
261
|
+
return result.success;
|
|
262
|
+
} catch (error) {
|
|
263
|
+
console.error('CallKit: Failed to update call', error);
|
|
264
|
+
return false;
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
public async getActiveCalls(): Promise<any[]> {
|
|
269
|
+
if (!this.bridge || Platform.OS !== 'ios') {
|
|
270
|
+
return [];
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
try {
|
|
274
|
+
const calls = await this.bridge.getActiveCalls();
|
|
275
|
+
console.log('CallKit: Active calls retrieved', calls);
|
|
276
|
+
return calls;
|
|
277
|
+
} catch (error) {
|
|
278
|
+
console.error('CallKit: Failed to get active calls', error);
|
|
279
|
+
return [];
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
// Event listener management
|
|
284
|
+
|
|
285
|
+
public onStartCall(listener: (event: CallKitEvent) => void): () => void {
|
|
286
|
+
this.listeners.set('startCall', listener);
|
|
287
|
+
return () => this.listeners.delete('startCall');
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
public onAnswerCall(listener: (event: CallKitEvent) => void): () => void {
|
|
291
|
+
this.listeners.set('answerCall', listener);
|
|
292
|
+
return () => this.listeners.delete('answerCall');
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
public onEndCall(listener: (event: CallKitEvent) => void): () => void {
|
|
296
|
+
this.listeners.set('endCall', listener);
|
|
297
|
+
return () => this.listeners.delete('endCall');
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
public onReceivePush(listener: (event: CallKitEvent) => void): () => void {
|
|
301
|
+
this.listeners.set('receivePush', listener);
|
|
302
|
+
return () => this.listeners.delete('receivePush');
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
// Utility methods
|
|
306
|
+
|
|
307
|
+
public generateCallUUID(): string {
|
|
308
|
+
return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function (c) {
|
|
309
|
+
const r = (Math.random() * 16) | 0;
|
|
310
|
+
const v = c === 'x' ? r : (r & 0x3) | 0x8;
|
|
311
|
+
return v.toString(16);
|
|
312
|
+
});
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
public isAvailable(): boolean {
|
|
316
|
+
return Platform.OS === 'ios' && this.bridge !== null;
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
// Export singleton instance
|
|
321
|
+
export const CallKit = new CallKitManager();
|
|
322
|
+
export default CallKit;
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
export { default as CallKit, CallEndReason, type CallKitEvent } from './callkit';
|
|
2
|
+
export { default as callKitCoordinator } from './callkit-coordinator';
|
|
3
|
+
export { useCallKit } from './use-callkit';
|
|
4
|
+
export { useCallKitCoordinator } from '../hooks/use-callkit-coordinator';
|
|
@@ -0,0 +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
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import React, { createContext, useContext } from 'react';
|
|
2
|
+
import { TelnyxVoipClient } from '../telnyx-voip-client';
|
|
3
|
+
import { TelnyxConnectionState } from '../models/connection-state';
|
|
4
|
+
|
|
5
|
+
interface TelnyxVoiceContextValue {
|
|
6
|
+
voipClient: TelnyxVoipClient;
|
|
7
|
+
// Add methods that CallKitHandler expects (these can be no-ops for now)
|
|
8
|
+
connectionState?: TelnyxConnectionState;
|
|
9
|
+
setConnectionState?: (state: TelnyxConnectionState) => void;
|
|
10
|
+
connect?: (payload: any) => Promise<any>;
|
|
11
|
+
client?: any;
|
|
12
|
+
setClient?: (client: any) => void;
|
|
13
|
+
enableAutoReconnect?: (enabled: boolean) => void;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
const TelnyxVoiceContext = createContext<TelnyxVoiceContextValue | null>(null);
|
|
17
|
+
|
|
18
|
+
export const TelnyxVoiceProvider: React.FC<{
|
|
19
|
+
voipClient: TelnyxVoipClient;
|
|
20
|
+
children: React.ReactNode;
|
|
21
|
+
}> = ({ voipClient, children }) => {
|
|
22
|
+
return (
|
|
23
|
+
<TelnyxVoiceContext.Provider value={{ voipClient }}>{children}</TelnyxVoiceContext.Provider>
|
|
24
|
+
);
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
export const useTelnyxVoice = (): TelnyxVoiceContextValue => {
|
|
28
|
+
const context = useContext(TelnyxVoiceContext);
|
|
29
|
+
if (!context) {
|
|
30
|
+
throw new Error('useTelnyxVoice must be used within a TelnyxVoiceProvider');
|
|
31
|
+
}
|
|
32
|
+
return context;
|
|
33
|
+
};
|