@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.
Files changed (73) hide show
  1. package/TelnyxVoiceCommons.podspec +32 -0
  2. package/ios/CallKitBridge.m +44 -0
  3. package/ios/CallKitBridge.swift +879 -0
  4. package/ios/README.md +211 -0
  5. package/ios/VoicePnBridge.m +31 -0
  6. package/ios/VoicePnBridge.swift +87 -0
  7. package/lib/callkit/callkit-coordinator.d.ts +126 -0
  8. package/lib/callkit/callkit-coordinator.js +728 -0
  9. package/lib/callkit/callkit.d.ts +49 -0
  10. package/lib/callkit/callkit.js +262 -0
  11. package/lib/callkit/index.d.ts +4 -0
  12. package/lib/callkit/index.js +15 -0
  13. package/lib/callkit/use-callkit-coordinator.d.ts +21 -0
  14. package/lib/callkit/use-callkit-coordinator.js +53 -0
  15. package/lib/callkit/use-callkit.d.ts +28 -0
  16. package/lib/callkit/use-callkit.js +279 -0
  17. package/lib/context/TelnyxVoiceContext.d.ts +18 -0
  18. package/lib/context/TelnyxVoiceContext.js +18 -0
  19. package/lib/hooks/use-callkit-coordinator.d.ts +13 -0
  20. package/lib/hooks/use-callkit-coordinator.js +48 -0
  21. package/lib/hooks/useAppReadyNotifier.d.ts +9 -0
  22. package/lib/hooks/useAppReadyNotifier.js +25 -0
  23. package/lib/hooks/useAppStateHandler.d.ts +16 -0
  24. package/lib/hooks/useAppStateHandler.js +105 -0
  25. package/lib/index.d.ts +24 -0
  26. package/lib/index.js +66 -0
  27. package/lib/internal/CallKitHandler.d.ts +17 -0
  28. package/lib/internal/CallKitHandler.js +110 -0
  29. package/lib/internal/callkit-manager.d.ts +69 -0
  30. package/lib/internal/callkit-manager.js +326 -0
  31. package/lib/internal/calls/call-state-controller.d.ts +92 -0
  32. package/lib/internal/calls/call-state-controller.js +294 -0
  33. package/lib/internal/session/session-manager.d.ts +87 -0
  34. package/lib/internal/session/session-manager.js +385 -0
  35. package/lib/internal/user-defaults-helpers.d.ts +10 -0
  36. package/lib/internal/user-defaults-helpers.js +69 -0
  37. package/lib/internal/voice-pn-bridge.d.ts +14 -0
  38. package/lib/internal/voice-pn-bridge.js +5 -0
  39. package/lib/models/call-state.d.ts +61 -0
  40. package/lib/models/call-state.js +87 -0
  41. package/lib/models/call.d.ts +145 -0
  42. package/lib/models/call.js +372 -0
  43. package/lib/models/config.d.ts +64 -0
  44. package/lib/models/config.js +92 -0
  45. package/lib/models/connection-state.d.ts +34 -0
  46. package/lib/models/connection-state.js +50 -0
  47. package/lib/telnyx-voice-app.d.ts +48 -0
  48. package/lib/telnyx-voice-app.js +486 -0
  49. package/lib/telnyx-voip-client.d.ts +184 -0
  50. package/lib/telnyx-voip-client.js +386 -0
  51. package/package.json +104 -0
  52. package/src/callkit/callkit-coordinator.ts +846 -0
  53. package/src/callkit/callkit.ts +322 -0
  54. package/src/callkit/index.ts +4 -0
  55. package/src/callkit/use-callkit.ts +345 -0
  56. package/src/context/TelnyxVoiceContext.tsx +33 -0
  57. package/src/hooks/use-callkit-coordinator.ts +60 -0
  58. package/src/hooks/useAppReadyNotifier.ts +25 -0
  59. package/src/hooks/useAppStateHandler.ts +134 -0
  60. package/src/index.ts +56 -0
  61. package/src/internal/CallKitHandler.tsx +149 -0
  62. package/src/internal/callkit-manager.ts +335 -0
  63. package/src/internal/calls/call-state-controller.ts +384 -0
  64. package/src/internal/session/session-manager.ts +467 -0
  65. package/src/internal/user-defaults-helpers.ts +58 -0
  66. package/src/internal/voice-pn-bridge.ts +18 -0
  67. package/src/models/call-state.ts +98 -0
  68. package/src/models/call.ts +388 -0
  69. package/src/models/config.ts +125 -0
  70. package/src/models/connection-state.ts +50 -0
  71. package/src/telnyx-voice-app.tsx +690 -0
  72. package/src/telnyx-voip-client.ts +475 -0
  73. package/src/types/telnyx-sdk.d.ts +79 -0
@@ -0,0 +1,388 @@
1
+ import { BehaviorSubject, Observable } from 'rxjs';
2
+ import { distinctUntilChanged, map } from 'rxjs/operators';
3
+ import { TelnyxCallState, CallStateHelpers } from './call-state';
4
+ import { Call as TelnyxCall } from '@telnyx/react-native-voice-sdk';
5
+ import { Platform } from 'react-native';
6
+
7
+ /**
8
+ * Represents a call with reactive state streams.
9
+ *
10
+ * This class wraps the underlying Telnyx Call object and provides
11
+ * reactive streams for all call state changes, making it easy to
12
+ * integrate with any state management solution.
13
+ */
14
+ export class Call {
15
+ private readonly _callState = new BehaviorSubject<TelnyxCallState>(TelnyxCallState.RINGING);
16
+ private readonly _isMuted = new BehaviorSubject<boolean>(false);
17
+ private readonly _isHeld = new BehaviorSubject<boolean>(false);
18
+ private readonly _duration = new BehaviorSubject<number>(0);
19
+
20
+ private _durationTimer?: NodeJS.Timeout;
21
+ private _startTime?: Date;
22
+
23
+ constructor(
24
+ private readonly _telnyxCall: TelnyxCall,
25
+ private readonly _callId: string,
26
+ private readonly _destination: string,
27
+ private readonly _isIncoming: boolean
28
+ ) {
29
+ this._setupCallListeners();
30
+ }
31
+
32
+ /**
33
+ * Unique identifier for this call
34
+ */
35
+ get callId(): string {
36
+ return this._callId;
37
+ }
38
+
39
+ /**
40
+ * The destination number or SIP URI
41
+ */
42
+ get destination(): string {
43
+ return this._destination;
44
+ }
45
+
46
+ /**
47
+ * Whether this is an incoming call
48
+ */
49
+ get isIncoming(): boolean {
50
+ return this._isIncoming;
51
+ }
52
+
53
+ /**
54
+ * Whether this is an outgoing call
55
+ */
56
+ get isOutgoing(): boolean {
57
+ return !this._isIncoming;
58
+ }
59
+
60
+ /**
61
+ * Current call state (synchronous access)
62
+ */
63
+ get currentState(): TelnyxCallState {
64
+ return this._callState.value;
65
+ }
66
+
67
+ /**
68
+ * Current mute state (synchronous access)
69
+ */
70
+ get currentIsMuted(): boolean {
71
+ return this._isMuted.value;
72
+ }
73
+
74
+ /**
75
+ * Current hold state (synchronous access)
76
+ */
77
+ get currentIsHeld(): boolean {
78
+ return this._isHeld.value;
79
+ }
80
+
81
+ /**
82
+ * Current call duration in seconds (synchronous access)
83
+ */
84
+ get currentDuration(): number {
85
+ return this._duration.value;
86
+ }
87
+
88
+ /**
89
+ * Get the underlying Telnyx Call object (for internal use)
90
+ * @internal
91
+ */
92
+ get telnyxCall(): TelnyxCall {
93
+ return this._telnyxCall;
94
+ }
95
+
96
+ /**
97
+ * Observable stream of call state changes
98
+ */
99
+ get callState$(): Observable<TelnyxCallState> {
100
+ return this._callState.asObservable().pipe(distinctUntilChanged());
101
+ }
102
+
103
+ /**
104
+ * Observable stream of mute state changes
105
+ */
106
+ get isMuted$(): Observable<boolean> {
107
+ return this._isMuted.asObservable().pipe(distinctUntilChanged());
108
+ }
109
+
110
+ /**
111
+ * Observable stream of hold state changes
112
+ */
113
+ get isHeld$(): Observable<boolean> {
114
+ return this._isHeld.asObservable().pipe(distinctUntilChanged());
115
+ }
116
+
117
+ /**
118
+ * Observable stream of call duration changes (in seconds)
119
+ */
120
+ get duration$(): Observable<number> {
121
+ return this._duration.asObservable().pipe(distinctUntilChanged());
122
+ }
123
+
124
+ /**
125
+ * Observable that emits true when the call can be answered
126
+ */
127
+ get canAnswer$(): Observable<boolean> {
128
+ return this.callState$.pipe(
129
+ map((state) => CallStateHelpers.canAnswer(state)),
130
+ distinctUntilChanged()
131
+ );
132
+ }
133
+
134
+ /**
135
+ * Observable that emits true when the call can be hung up
136
+ */
137
+ get canHangup$(): Observable<boolean> {
138
+ return this.callState$.pipe(
139
+ map((state) => CallStateHelpers.canHangup(state)),
140
+ distinctUntilChanged()
141
+ );
142
+ }
143
+
144
+ /**
145
+ * Observable that emits true when the call can be put on hold
146
+ */
147
+ get canHold$(): Observable<boolean> {
148
+ return this.callState$.pipe(
149
+ map((state) => CallStateHelpers.canHold(state)),
150
+ distinctUntilChanged()
151
+ );
152
+ }
153
+
154
+ /**
155
+ * Observable that emits true when the call can be resumed from hold
156
+ */
157
+ get canResume$(): Observable<boolean> {
158
+ return this.callState$.pipe(
159
+ map((state) => CallStateHelpers.canResume(state)),
160
+ distinctUntilChanged()
161
+ );
162
+ }
163
+
164
+ /**
165
+ * Answer the incoming call
166
+ */
167
+ async answer(): Promise<void> {
168
+ if (!CallStateHelpers.canAnswer(this.currentState)) {
169
+ throw new Error(`Cannot answer call in state: ${this.currentState}`);
170
+ }
171
+
172
+ try {
173
+ // On iOS, use CallKit coordinator for proper audio session handling
174
+ if (Platform.OS === 'ios') {
175
+ const { callKitCoordinator } = await import('../callkit/callkit-coordinator');
176
+ if (callKitCoordinator.isAvailable()) {
177
+ console.log('Call: Using CallKit coordinator to answer call (iOS)');
178
+ await callKitCoordinator.answerCallFromUI(this._telnyxCall);
179
+ return;
180
+ }
181
+ }
182
+
183
+ // Fallback for Android or when CallKit is not available
184
+ console.log('Call: Setting state to CONNECTING before answering');
185
+ this._callState.next(TelnyxCallState.CONNECTING);
186
+
187
+ await this._telnyxCall.answer();
188
+ } catch (error) {
189
+ console.error('Failed to answer call:', error);
190
+ throw error;
191
+ }
192
+ }
193
+
194
+ /**
195
+ * Hang up the call
196
+ */
197
+ async hangup(): Promise<void> {
198
+ if (!CallStateHelpers.canHangup(this.currentState)) {
199
+ throw new Error(`Cannot hang up call in state: ${this.currentState}`);
200
+ }
201
+
202
+ try {
203
+ // On iOS, use CallKit coordinator for proper CallKit cleanup
204
+ if (Platform.OS === 'ios') {
205
+ const { callKitCoordinator } = await import('../callkit/callkit-coordinator');
206
+ if (callKitCoordinator.isAvailable()) {
207
+ console.log('Call: Using CallKit coordinator to end call (iOS)');
208
+ await callKitCoordinator.endCallFromUI(this._telnyxCall);
209
+ return;
210
+ }
211
+ }
212
+
213
+ // Fallback for Android or when CallKit is not available
214
+ await this._telnyxCall.hangup();
215
+ } catch (error) {
216
+ console.error('Failed to hang up call:', error);
217
+ throw error;
218
+ }
219
+ }
220
+
221
+ /**
222
+ * Put the call on hold
223
+ */
224
+ async hold(): Promise<void> {
225
+ if (!CallStateHelpers.canHold(this.currentState)) {
226
+ throw new Error(`Cannot hold call in state: ${this.currentState}`);
227
+ }
228
+
229
+ try {
230
+ await this._telnyxCall.hold();
231
+ } catch (error) {
232
+ console.error('Failed to hold call:', error);
233
+ throw error;
234
+ }
235
+ }
236
+
237
+ /**
238
+ * Resume the call from hold
239
+ */
240
+ async resume(): Promise<void> {
241
+ if (!CallStateHelpers.canResume(this.currentState)) {
242
+ throw new Error(`Cannot resume call in state: ${this.currentState}`);
243
+ }
244
+
245
+ try {
246
+ await this._telnyxCall.unhold();
247
+ } catch (error) {
248
+ console.error('Failed to resume call:', error);
249
+ throw error;
250
+ }
251
+ }
252
+
253
+ /**
254
+ * Mute the call
255
+ */
256
+ async mute(): Promise<void> {
257
+ if (!CallStateHelpers.canToggleMute(this.currentState)) {
258
+ throw new Error(`Cannot mute call in state: ${this.currentState}`);
259
+ }
260
+
261
+ try {
262
+ this._telnyxCall.mute();
263
+ this._isMuted.next(true);
264
+ } catch (error) {
265
+ console.error('Failed to mute call:', error);
266
+ throw error;
267
+ }
268
+ }
269
+
270
+ /**
271
+ * Unmute the call
272
+ */
273
+ async unmute(): Promise<void> {
274
+ if (!CallStateHelpers.canToggleMute(this.currentState)) {
275
+ throw new Error(`Cannot unmute call in state: ${this.currentState}`);
276
+ }
277
+
278
+ try {
279
+ this._telnyxCall.unmute();
280
+ this._isMuted.next(false);
281
+ } catch (error) {
282
+ console.error('Failed to unmute call:', error);
283
+ throw error;
284
+ }
285
+ }
286
+
287
+ /**
288
+ * Toggle mute state
289
+ */
290
+ async toggleMute(): Promise<void> {
291
+ if (this.currentIsMuted) {
292
+ await this.unmute();
293
+ } else {
294
+ await this.mute();
295
+ }
296
+ }
297
+
298
+ /**
299
+ * Set the call to connecting state (used for push notification calls when answered via CallKit)
300
+ * @internal
301
+ */
302
+ setConnecting(): void {
303
+ console.log('Call: Setting state to CONNECTING for push notification answer');
304
+ this._callState.next(TelnyxCallState.CONNECTING);
305
+ }
306
+
307
+ /**
308
+ * Clean up resources when the call is disposed
309
+ */
310
+ dispose(): void {
311
+ this._stopDurationTimer();
312
+ this._callState.complete();
313
+ this._isMuted.complete();
314
+ this._isHeld.complete();
315
+ this._duration.complete();
316
+ }
317
+
318
+ /**
319
+ * Set up listeners for the underlying Telnyx call
320
+ */
321
+ private _setupCallListeners(): void {
322
+ // Map Telnyx call states to our simplified states
323
+ this._telnyxCall.on('telnyx.call.state', (call: any, state: any) => {
324
+ const telnyxState = this._mapToTelnyxCallState(state);
325
+ this._callState.next(telnyxState);
326
+
327
+ // Start duration timer when call becomes active
328
+ if (telnyxState === TelnyxCallState.ACTIVE && !this._startTime) {
329
+ this._startDurationTimer();
330
+ }
331
+
332
+ // Stop duration timer when call ends
333
+ if (CallStateHelpers.isTerminated(telnyxState)) {
334
+ this._stopDurationTimer();
335
+ }
336
+ });
337
+ }
338
+
339
+ /**
340
+ * Map Telnyx SDK call states to our simplified call states
341
+ */
342
+ private _mapToTelnyxCallState(telnyxState: any): TelnyxCallState {
343
+ // This mapping will depend on the actual Telnyx SDK call states
344
+ // For now, using a basic mapping - this should be updated based on actual SDK
345
+ switch (telnyxState) {
346
+ case 'ringing':
347
+ case 'new':
348
+ return TelnyxCallState.RINGING;
349
+ case 'active':
350
+ case 'answered':
351
+ return TelnyxCallState.ACTIVE;
352
+ case 'held':
353
+ return TelnyxCallState.HELD;
354
+ case 'ended':
355
+ case 'hangup':
356
+ return TelnyxCallState.ENDED;
357
+ case 'failed':
358
+ case 'rejected':
359
+ return TelnyxCallState.FAILED;
360
+ default:
361
+ console.warn(`Unknown call state: ${telnyxState}`);
362
+ return TelnyxCallState.RINGING;
363
+ }
364
+ }
365
+
366
+ /**
367
+ * Start the duration timer
368
+ */
369
+ private _startDurationTimer(): void {
370
+ this._startTime = new Date();
371
+ this._durationTimer = setInterval(() => {
372
+ if (this._startTime) {
373
+ const duration = Math.floor((Date.now() - this._startTime.getTime()) / 1000);
374
+ this._duration.next(duration);
375
+ }
376
+ }, 1000);
377
+ }
378
+
379
+ /**
380
+ * Stop the duration timer
381
+ */
382
+ private _stopDurationTimer(): void {
383
+ if (this._durationTimer) {
384
+ clearInterval(this._durationTimer);
385
+ this._durationTimer = undefined;
386
+ }
387
+ }
388
+ }
@@ -0,0 +1,125 @@
1
+ /**
2
+ * Configuration for credential-based authentication
3
+ */
4
+ export interface CredentialConfig {
5
+ type: 'credential';
6
+ sipUser: string;
7
+ sipPassword: string;
8
+ debug?: boolean;
9
+ pushNotificationDeviceToken?: string;
10
+ }
11
+
12
+ /**
13
+ * Configuration for token-based authentication
14
+ */
15
+ export interface TokenConfig {
16
+ type: 'token';
17
+ token: string;
18
+ debug?: boolean;
19
+ pushNotificationDeviceToken?: string;
20
+ }
21
+
22
+ /**
23
+ * Union type for all supported authentication configurations
24
+ */
25
+ export type Config = CredentialConfig | TokenConfig;
26
+
27
+ /**
28
+ * Type guard to check if config is credential-based
29
+ */
30
+ export function isCredentialConfig(config: Config): config is CredentialConfig {
31
+ return config.type === 'credential';
32
+ }
33
+
34
+ /**
35
+ * Type guard to check if config is token-based
36
+ */
37
+ export function isTokenConfig(config: Config): config is TokenConfig {
38
+ return config.type === 'token';
39
+ }
40
+
41
+ /**
42
+ * Validates a credential configuration
43
+ */
44
+ export function validateCredentialConfig(config: CredentialConfig): string[] {
45
+ const errors: string[] = [];
46
+
47
+ if (!config.sipUser || config.sipUser.trim() === '') {
48
+ errors.push('sipUser is required');
49
+ }
50
+
51
+ if (!config.sipPassword || config.sipPassword.trim() === '') {
52
+ errors.push('sipPassword is required');
53
+ }
54
+
55
+ return errors;
56
+ }
57
+
58
+ /**
59
+ * Validates a token configuration
60
+ */
61
+ export function validateTokenConfig(config: TokenConfig): string[] {
62
+ const errors: string[] = [];
63
+
64
+ if (!config.token || config.token.trim() === '') {
65
+ errors.push('token is required');
66
+ }
67
+
68
+ return errors;
69
+ }
70
+
71
+ /**
72
+ * Validates any configuration
73
+ */
74
+ export function validateConfig(config: Config): string[] {
75
+ if (isCredentialConfig(config)) {
76
+ return validateCredentialConfig(config);
77
+ } else if (isTokenConfig(config)) {
78
+ return validateTokenConfig(config);
79
+ } else {
80
+ return ['Invalid configuration type'];
81
+ }
82
+ }
83
+
84
+ /**
85
+ * Creates a credential configuration
86
+ *
87
+ * @param sipUser - SIP username for authentication
88
+ * @param sipPassword - SIP password for authentication
89
+ * @param options - Optional configuration settings
90
+ * @param options.debug - Enable debug logging (sets SDK logLevel to 'debug')
91
+ * @param options.pushNotificationDeviceToken - Device token for push notifications
92
+ * @returns Complete credential configuration object
93
+ */
94
+ export function createCredentialConfig(
95
+ sipUser: string,
96
+ sipPassword: string,
97
+ options?: Partial<Omit<CredentialConfig, 'type' | 'sipUser' | 'sipPassword'>>
98
+ ): CredentialConfig {
99
+ return {
100
+ type: 'credential',
101
+ sipUser,
102
+ sipPassword,
103
+ ...options,
104
+ };
105
+ }
106
+
107
+ /**
108
+ * Creates a token-based configuration
109
+ *
110
+ * @param sipToken - JWT token for authentication
111
+ * @param options - Optional configuration settings
112
+ * @param options.debug - Enable debug logging (sets SDK logLevel to 'debug')
113
+ * @param options.pushNotificationDeviceToken - Device token for push notifications
114
+ * @returns Complete token configuration object
115
+ */
116
+ export function createTokenConfig(
117
+ sipToken: string,
118
+ options?: Partial<Omit<TokenConfig, 'type' | 'sipToken'>>
119
+ ): TokenConfig {
120
+ return {
121
+ type: 'token',
122
+ token: sipToken,
123
+ ...options,
124
+ };
125
+ }
@@ -0,0 +1,50 @@
1
+ /**
2
+ * Represents the connection state to the Telnyx platform.
3
+ *
4
+ * This enum provides a simplified view of the connection status,
5
+ * abstracting away the complexity of the underlying WebSocket states.
6
+ */
7
+ export enum TelnyxConnectionState {
8
+ /** Initial state before any connection attempt */
9
+ DISCONNECTED = 'DISCONNECTED',
10
+
11
+ /** Attempting to establish connection */
12
+ CONNECTING = 'CONNECTING',
13
+
14
+ /** Successfully connected and authenticated */
15
+ CONNECTED = 'CONNECTED',
16
+
17
+ /** Connection lost, attempting to reconnect */
18
+ RECONNECTING = 'RECONNECTING',
19
+
20
+ /** Connection failed or authentication error */
21
+ ERROR = 'ERROR',
22
+ }
23
+
24
+ /**
25
+ * Type guard to check if a value is a valid TelnyxConnectionState
26
+ */
27
+ export function isTelnyxConnectionState(value: any): value is TelnyxConnectionState {
28
+ return Object.values(TelnyxConnectionState).includes(value);
29
+ }
30
+
31
+ /**
32
+ * Helper function to determine if the connection state allows making calls
33
+ */
34
+ export function canMakeCalls(state: TelnyxConnectionState): boolean {
35
+ return state === TelnyxConnectionState.CONNECTED;
36
+ }
37
+
38
+ /**
39
+ * Helper function to determine if the connection state indicates an active connection
40
+ */
41
+ export function isConnected(state: TelnyxConnectionState): boolean {
42
+ return state === TelnyxConnectionState.CONNECTED;
43
+ }
44
+
45
+ /**
46
+ * Helper function to determine if the connection is in a transitional state
47
+ */
48
+ export function isTransitioning(state: TelnyxConnectionState): boolean {
49
+ return state === TelnyxConnectionState.CONNECTING || state === TelnyxConnectionState.RECONNECTING;
50
+ }