@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,60 @@
1
+ import { useCallback } from 'react';
2
+ import { Call } from '@telnyx/react-native-voice-sdk';
3
+ import callKitCoordinator from '../callkit/callkit-coordinator';
4
+ import { TelnyxVoipClient } from '../telnyx-voip-client';
5
+
6
+ export function useCallKitCoordinator() {
7
+ const reportIncomingCall = useCallback(
8
+ async (call: Call, callerName: string, callerNumber: string): Promise<string | null> => {
9
+ return callKitCoordinator.reportIncomingCall(call, callerName, callerNumber);
10
+ },
11
+ []
12
+ );
13
+
14
+ const startOutgoingCall = useCallback(
15
+ async (call: Call, destinationNumber: string, displayName?: string): Promise<string | null> => {
16
+ return callKitCoordinator.startOutgoingCall(call, destinationNumber, displayName);
17
+ },
18
+ []
19
+ );
20
+
21
+ const answerCallFromUI = useCallback(async (call: Call): Promise<boolean> => {
22
+ return callKitCoordinator.answerCallFromUI(call);
23
+ }, []);
24
+
25
+ const endCallFromUI = useCallback(async (call: Call): Promise<boolean> => {
26
+ return callKitCoordinator.endCallFromUI(call);
27
+ }, []);
28
+
29
+ const getCallKitUUID = useCallback((call: Call): string | null => {
30
+ return callKitCoordinator.getCallKitUUID(call);
31
+ }, []);
32
+
33
+ const getWebRTCCall = useCallback((callKitUUID: string): Call | null => {
34
+ return callKitCoordinator.getWebRTCCall(callKitUUID);
35
+ }, []);
36
+
37
+ const linkExistingCallKitCall = useCallback((call: Call, callKitUUID: string): void => {
38
+ callKitCoordinator.linkExistingCallKitCall(call, callKitUUID);
39
+ }, []);
40
+
41
+ const isAvailable = useCallback((): boolean => {
42
+ return callKitCoordinator.isAvailable();
43
+ }, []);
44
+
45
+ const setVoipClient = useCallback((voipClient: TelnyxVoipClient): void => {
46
+ callKitCoordinator.setVoipClient(voipClient);
47
+ }, []);
48
+
49
+ return {
50
+ reportIncomingCall,
51
+ startOutgoingCall,
52
+ answerCallFromUI,
53
+ endCallFromUI,
54
+ getCallKitUUID,
55
+ getWebRTCCall,
56
+ linkExistingCallKitCall,
57
+ isAvailable,
58
+ setVoipClient,
59
+ };
60
+ }
@@ -0,0 +1,25 @@
1
+ import { useEffect } from 'react';
2
+ import { Platform } from 'react-native';
3
+
4
+ /**
5
+ * Hook to notify the native side when React Native is ready
6
+ * Call this hook when your main screen/login screen is visible and ready
7
+ * This is automatically called by TelnyxVoiceApp, but can be used manually if needed
8
+ *
9
+ * Note: With the native VoicePnManager integration, this notification is now
10
+ * handled automatically by the native Android services, so this hook is simplified.
11
+ */
12
+ export const useAppReadyNotifier = () => {
13
+ useEffect(() => {
14
+ // Only notify on Android since this is Android-specific functionality
15
+ if (Platform.OS === 'android') {
16
+ console.log(
17
+ 'useAppReadyNotifier: React Native is ready (native services handle FCM automatically)'
18
+ );
19
+
20
+ // Note: The VoicePnManager in the native Android code automatically handles
21
+ // push notification state when the Firebase messaging service receives notifications.
22
+ // No JavaScript module communication is needed anymore.
23
+ }
24
+ }, []);
25
+ };
@@ -0,0 +1,134 @@
1
+ import { useEffect, useRef } from 'react';
2
+ import { AppState, AppStateStatus } from 'react-native';
3
+ import { router } from 'expo-router';
4
+ import AsyncStorage from '@react-native-async-storage/async-storage';
5
+ import { TelnyxVoipClient } from '../telnyx-voip-client';
6
+ import { TelnyxConnectionState } from '../models/connection-state';
7
+ import { TelnyxCallState, CallStateHelpers } from '../models/call-state';
8
+
9
+ interface UseAppStateHandlerOptions {
10
+ voipClient: TelnyxVoipClient;
11
+ disconnectOnBackground?: boolean;
12
+ navigateToLoginOnDisconnect?: boolean;
13
+ debug?: boolean;
14
+ }
15
+
16
+ /**
17
+ * Hook to handle app state changes for VoIP behavior
18
+ * When app goes to background without an active call, disconnect socket and redirect to login
19
+ */
20
+ export const useAppStateHandler = ({
21
+ voipClient,
22
+ disconnectOnBackground = true,
23
+ navigateToLoginOnDisconnect = true,
24
+ debug = false,
25
+ }: UseAppStateHandlerOptions) => {
26
+ const appState = useRef(AppState.currentState);
27
+ const log = debug ? console.log : () => {};
28
+
29
+ useEffect(() => {
30
+ const handleAppStateChange = async (nextAppState: AppStateStatus) => {
31
+ log('AppStateHandler: App state changed from', appState.current, 'to', nextAppState);
32
+
33
+ // When app goes to background
34
+ if (appState.current.match(/active/) && nextAppState === 'background') {
35
+ log('AppStateHandler: App went to background, checking for active calls...');
36
+
37
+ const connectionState = voipClient.currentConnectionState;
38
+ if (connectionState !== TelnyxConnectionState.CONNECTED) {
39
+ log('AppStateHandler: Not connected, skipping background handling');
40
+ appState.current = nextAppState;
41
+ return;
42
+ }
43
+
44
+ // Check if there's an active call (including ringing calls)
45
+ const activeCalls = voipClient.currentCalls;
46
+ const hasActiveCall =
47
+ activeCalls.length > 0 &&
48
+ activeCalls.some(
49
+ (call) =>
50
+ CallStateHelpers.isActive(call.currentState) ||
51
+ call.currentState === TelnyxCallState.RINGING
52
+ );
53
+
54
+ log('AppStateHandler: Active call check:', {
55
+ callCount: activeCalls.length,
56
+ hasActiveCall,
57
+ callStates: activeCalls.map((call) => ({
58
+ callId: call.callId,
59
+ currentState: call.currentState,
60
+ destination: call.destination,
61
+ isIncoming: call.isIncoming,
62
+ })),
63
+ });
64
+
65
+ if (!hasActiveCall && disconnectOnBackground) {
66
+ // Check if there's a push notification call in progress
67
+ const isPushNotificationInProgress = await AsyncStorage.getItem(
68
+ '@push_notification_payload'
69
+ );
70
+
71
+ if (isPushNotificationInProgress) {
72
+ log('AppStateHandler: Push notification call in progress, keeping socket connected');
73
+ // Wait a bit longer before allowing disconnection to give time for WebRTC call to establish
74
+ setTimeout(async () => {
75
+ const stillInProgress = await AsyncStorage.getItem('@push_notification_payload');
76
+ if (!stillInProgress) {
77
+ log('AppStateHandler: Push notification call completed, now disconnecting socket');
78
+ await voipClient.logout();
79
+ if (navigateToLoginOnDisconnect) {
80
+ router.replace('/');
81
+ }
82
+ }
83
+ }, 5000); // Wait 5 seconds
84
+ appState.current = nextAppState;
85
+ return;
86
+ }
87
+
88
+ log('AppStateHandler: No active call detected, disconnecting socket...');
89
+
90
+ try {
91
+ // Disconnect the socket with background reason
92
+ await voipClient.logout();
93
+
94
+ log('AppStateHandler: Socket disconnected successfully');
95
+
96
+ // Navigate to login screen
97
+ if (navigateToLoginOnDisconnect) {
98
+ // Use a small delay to ensure the disconnect completes
99
+ setTimeout(() => {
100
+ log('AppStateHandler: Navigating to login screen');
101
+ router.replace('/');
102
+ }, 100);
103
+ }
104
+ } catch (error) {
105
+ console.error('AppStateHandler: Error during background disconnect:', error);
106
+ }
107
+ } else {
108
+ log(
109
+ 'AppStateHandler: Active call detected or disconnect disabled, keeping socket connected'
110
+ );
111
+ }
112
+ }
113
+
114
+ // When app comes to foreground
115
+ if (appState.current.match(/background/) && nextAppState === 'active') {
116
+ log('AppStateHandler: App came to foreground');
117
+ // User will need to manually login again when returning from background
118
+ // Auto-login only happens for push notification calls
119
+ }
120
+
121
+ appState.current = nextAppState;
122
+ };
123
+
124
+ const subscription = AppState.addEventListener('change', handleAppStateChange);
125
+
126
+ return () => {
127
+ subscription?.remove();
128
+ };
129
+ }, [voipClient, disconnectOnBackground, navigateToLoginOnDisconnect, log]);
130
+
131
+ return {
132
+ currentAppState: appState.current,
133
+ };
134
+ };
package/src/index.ts ADDED
@@ -0,0 +1,56 @@
1
+ /**
2
+ * @telnyx/react-voice-commons-sdk
3
+ *
4
+ * A high-level, state-agnostic, drop-in module for the Telnyx React Native SDK.
5
+ *
6
+ * This library provides a simplified interface for integrating Telnyx WebRTC
7
+ * capabilities into React Native applications. It handles session management,
8
+ * call state transitions, push notification processing, and native call UI
9
+ * integration.
10
+ */
11
+
12
+ // Main client
13
+ export {
14
+ TelnyxVoipClient,
15
+ createTelnyxVoipClient,
16
+ createBackgroundTelnyxVoipClient,
17
+ } from './telnyx-voip-client';
18
+ export type { TelnyxVoipClientOptions } from './telnyx-voip-client';
19
+
20
+ // TelnyxVoiceApp component
21
+ export { TelnyxVoiceApp } from './telnyx-voice-app';
22
+ export type { TelnyxVoiceAppOptions, TelnyxVoiceAppProps } from './telnyx-voice-app';
23
+
24
+ // Hooks
25
+ export { useAppStateHandler } from './hooks/useAppStateHandler';
26
+ export { useTelnyxVoice } from './context/TelnyxVoiceContext';
27
+
28
+ // Models
29
+ export { Call } from './models/call';
30
+ export {
31
+ TelnyxConnectionState,
32
+ isTelnyxConnectionState,
33
+ canMakeCalls,
34
+ isConnected,
35
+ isTransitioning,
36
+ } from './models/connection-state';
37
+ export { TelnyxCallState, isTelnyxCallState, CallStateHelpers } from './models/call-state';
38
+ export {
39
+ isCredentialConfig,
40
+ isTokenConfig,
41
+ validateConfig,
42
+ validateCredentialConfig,
43
+ validateTokenConfig,
44
+ createCredentialConfig,
45
+ createTokenConfig,
46
+ } from './models/config';
47
+ export type { Config, CredentialConfig, TokenConfig } from './models/config';
48
+
49
+ // Re-export useful types from the underlying SDK
50
+ export type { Call as TelnyxCall } from '@telnyx/react-native-voice-sdk';
51
+
52
+ // Export CallKit functionality
53
+ export * from './callkit';
54
+
55
+ // Export hooks
56
+ export { useAppReadyNotifier } from './hooks/useAppReadyNotifier';
@@ -0,0 +1,149 @@
1
+ import React, { useEffect, useRef } from 'react';
2
+ import { Platform, DeviceEventEmitter } from 'react-native';
3
+ import { useRouter } from 'expo-router';
4
+ import AsyncStorage from '@react-native-async-storage/async-storage';
5
+ import { useTelnyxVoice } from '../context/TelnyxVoiceContext';
6
+ import { callKitCoordinator } from '../callkit';
7
+
8
+ // Global flag to ensure only one CallKitHandler is active
9
+ let isCallKitHandlerActive = false;
10
+
11
+ interface CallData {
12
+ type: string;
13
+ callUUID: string;
14
+ payload?: any;
15
+ callData?: any;
16
+ action: string;
17
+ }
18
+
19
+ interface CallKitHandlerProps {
20
+ /** Callback when user needs to login from push notification */
21
+ onLoginRequired?: (pushPayload: any) => void;
22
+ /** Callback when call is answered and user should navigate to dialer */
23
+ onNavigateToDialer?: () => void;
24
+ /** Callback when call ends and user should navigate back */
25
+ onNavigateBack?: () => void;
26
+ }
27
+
28
+ /**
29
+ * Internal CallKit handler for iOS push notifications
30
+ * This component is automatically included in TelnyxVoiceApp
31
+ *
32
+ * @internal - Users should not use this component directly
33
+ */
34
+ export const CallKitHandler: React.FC<CallKitHandlerProps> = ({
35
+ onLoginRequired,
36
+ onNavigateToDialer,
37
+ onNavigateBack,
38
+ }) => {
39
+ const router = useRouter();
40
+ const { voipClient } = useTelnyxVoice();
41
+
42
+ // Store active calls by CallKit UUID for coordination
43
+ const activeCallsRef = useRef<Map<string, any>>(new Map());
44
+
45
+ useEffect(() => {
46
+ if (Platform.OS !== 'ios') return;
47
+
48
+ if (isCallKitHandlerActive) {
49
+ console.log('CallKitHandler: Another instance is already active, skipping setup');
50
+ return;
51
+ }
52
+
53
+ isCallKitHandlerActive = true;
54
+ console.log('CallKitHandler: Setting up DeviceEventEmitter listeners (singleton instance)...');
55
+
56
+ DeviceEventEmitter.removeAllListeners('incomingVoIPCall');
57
+ DeviceEventEmitter.removeAllListeners('callKitAction');
58
+
59
+ const incomingCallListener = DeviceEventEmitter.addListener(
60
+ 'incomingVoIPCall',
61
+ async (eventData: CallData) => {
62
+ console.log('🔥 CallKitHandler: Incoming VoIP call received:', eventData);
63
+ if (eventData.action === 'connect_webrtc') {
64
+ await handleIncomingCall(eventData);
65
+ }
66
+ }
67
+ );
68
+
69
+ const callKitActionListener = DeviceEventEmitter.addListener(
70
+ 'callKitAction',
71
+ async (eventData: CallData) => {
72
+ console.log('🔥 CallKitHandler: CallKit action received:', eventData);
73
+ switch (eventData.action) {
74
+ case 'answer':
75
+ await handleAnswerCall(eventData);
76
+ break;
77
+ case 'end':
78
+ await handleEndCall(eventData);
79
+ break;
80
+ }
81
+ }
82
+ );
83
+
84
+ console.log('CallKitHandler: DeviceEventEmitter listeners set up successfully');
85
+
86
+ return () => {
87
+ console.log('CallKitHandler: Cleaning up DeviceEventEmitter listeners');
88
+ incomingCallListener.remove();
89
+ callKitActionListener.remove();
90
+ isCallKitHandlerActive = false;
91
+ };
92
+ }, []);
93
+
94
+ const handleIncomingCall = async (eventData: CallData) => {
95
+ console.log('CallKitHandler: Handling incoming call', {
96
+ callUUID: eventData.callUUID,
97
+ hasClient: !!voipClient,
98
+ });
99
+
100
+ // Store the push notification payload
101
+ await AsyncStorage.setItem('@push_notification_payload', JSON.stringify(eventData.payload));
102
+
103
+ // Mark this call as being processed
104
+ activeCallsRef.current.set(eventData.callUUID, {
105
+ processing: true,
106
+ timestamp: Date.now(),
107
+ });
108
+
109
+ // Trigger login required callback if provided
110
+ if (onLoginRequired) {
111
+ onLoginRequired(eventData.payload);
112
+ }
113
+ };
114
+
115
+ const handleAnswerCall = async (eventData: CallData) => {
116
+ console.log('CallKitHandler: User answered call via CallKit', {
117
+ callUUID: eventData.callUUID,
118
+ isTrackedCall: activeCallsRef.current.has(eventData.callUUID),
119
+ });
120
+
121
+ // Navigate to dialer after answering
122
+ if (onNavigateToDialer) {
123
+ onNavigateToDialer();
124
+ } else {
125
+ router.replace('/dialer');
126
+ }
127
+ };
128
+
129
+ const handleEndCall = async (eventData: CallData) => {
130
+ console.log('CallKitHandler: User ended call via CallKit', {
131
+ callUUID: eventData.callUUID,
132
+ isTrackedCall: activeCallsRef.current.has(eventData.callUUID),
133
+ });
134
+
135
+ // Clean up our local tracking info
136
+ activeCallsRef.current.delete(eventData.callUUID);
137
+ await AsyncStorage.removeItem('@push_notification_payload');
138
+
139
+ // Navigate back after call ends
140
+ if (onNavigateBack) {
141
+ onNavigateBack();
142
+ } else {
143
+ router.replace('/dialer');
144
+ }
145
+ };
146
+
147
+ // This component doesn't render anything, it just handles events
148
+ return null;
149
+ };