@telnyx/react-voice-commons-sdk 0.1.2 → 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 (58) hide show
  1. package/CHANGELOG.md +42 -0
  2. package/TelnyxVoiceCommons.podspec +31 -31
  3. package/ios/CallKitBridge.m +43 -43
  4. package/ios/CallKitBridge.swift +874 -879
  5. package/ios/VoicePnBridge.m +30 -30
  6. package/ios/VoicePnBridge.swift +86 -86
  7. package/lib/callkit/callkit-coordinator.d.ts +110 -117
  8. package/lib/callkit/callkit-coordinator.js +664 -727
  9. package/lib/callkit/callkit.d.ts +41 -41
  10. package/lib/callkit/callkit.js +252 -242
  11. package/lib/callkit/index.js +15 -47
  12. package/lib/callkit/use-callkit.d.ts +19 -19
  13. package/lib/callkit/use-callkit.js +270 -310
  14. package/lib/context/TelnyxVoiceContext.d.ts +9 -9
  15. package/lib/context/TelnyxVoiceContext.js +10 -13
  16. package/lib/hooks/use-callkit-coordinator.d.ts +9 -17
  17. package/lib/hooks/use-callkit-coordinator.js +45 -50
  18. package/lib/hooks/useAppReadyNotifier.js +13 -15
  19. package/lib/hooks/useAppStateHandler.d.ts +6 -11
  20. package/lib/hooks/useAppStateHandler.js +95 -110
  21. package/lib/hooks/useNetworkStateHandler.d.ts +0 -0
  22. package/lib/hooks/useNetworkStateHandler.js +0 -0
  23. package/lib/index.d.ts +3 -21
  24. package/lib/index.js +50 -201
  25. package/lib/internal/CallKitHandler.d.ts +6 -6
  26. package/lib/internal/CallKitHandler.js +96 -104
  27. package/lib/internal/callkit-manager.d.ts +57 -57
  28. package/lib/internal/callkit-manager.js +299 -316
  29. package/lib/internal/calls/call-state-controller.d.ts +73 -86
  30. package/lib/internal/calls/call-state-controller.js +263 -307
  31. package/lib/internal/session/session-manager.d.ts +71 -75
  32. package/lib/internal/session/session-manager.js +360 -424
  33. package/lib/internal/user-defaults-helpers.js +49 -39
  34. package/lib/internal/voice-pn-bridge.d.ts +114 -12
  35. package/lib/internal/voice-pn-bridge.js +212 -5
  36. package/lib/models/call-state.d.ts +46 -44
  37. package/lib/models/call-state.js +70 -68
  38. package/lib/models/call.d.ts +161 -133
  39. package/lib/models/call.js +454 -382
  40. package/lib/models/config.d.ts +11 -18
  41. package/lib/models/config.js +37 -35
  42. package/lib/models/connection-state.d.ts +10 -10
  43. package/lib/models/connection-state.js +16 -16
  44. package/lib/telnyx-voice-app.d.ts +28 -28
  45. package/lib/telnyx-voice-app.js +463 -481
  46. package/lib/telnyx-voip-client.d.ts +167 -167
  47. package/lib/telnyx-voip-client.js +385 -390
  48. package/package.json +11 -4
  49. package/src/callkit/callkit-coordinator.ts +18 -34
  50. package/src/hooks/useNetworkStateHandler.ts +0 -0
  51. package/src/internal/calls/call-state-controller.ts +81 -58
  52. package/src/internal/session/session-manager.ts +42 -26
  53. package/src/internal/voice-pn-bridge.ts +250 -2
  54. package/src/models/call-state.ts +8 -1
  55. package/src/models/call.ts +119 -5
  56. package/src/telnyx-voice-app.tsx +87 -40
  57. package/src/telnyx-voip-client.ts +15 -3
  58. package/src/types/telnyx-sdk.d.ts +16 -2
@@ -1,12 +1,12 @@
1
- 'use strict';
2
- Object.defineProperty(exports, '__esModule', { value: true });
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.TelnyxVoiceApp = void 0;
4
- const jsx_runtime_1 = require('react/jsx-runtime');
5
- const react_1 = require('react');
6
- const react_native_1 = require('react-native');
7
- const telnyx_voip_client_1 = require('./telnyx-voip-client');
8
- const connection_state_1 = require('./models/connection-state');
9
- const TelnyxVoiceContext_1 = require('./context/TelnyxVoiceContext');
4
+ const jsx_runtime_1 = require("react/jsx-runtime");
5
+ const react_1 = require("react");
6
+ const react_native_1 = require("react-native");
7
+ const telnyx_voip_client_1 = require("./telnyx-voip-client");
8
+ const connection_state_1 = require("./models/connection-state");
9
+ const TelnyxVoiceContext_1 = require("./context/TelnyxVoiceContext");
10
10
  /**
11
11
  * A comprehensive wrapper component that handles all Telnyx SDK lifecycle management.
12
12
  *
@@ -23,435 +23,436 @@ const TelnyxVoiceContext_1 = require('./context/TelnyxVoiceContext');
23
23
  * </TelnyxVoiceApp>
24
24
  * ```
25
25
  */
26
- const TelnyxVoiceAppComponent = ({
27
- voipClient,
28
- children,
29
- onPushNotificationProcessingStarted,
30
- onPushNotificationProcessingCompleted,
31
- onAppStateChanged,
32
- enableAutoReconnect = true,
33
- skipWebBackgroundDetection = true,
34
- debug = false,
35
- }) => {
36
- // State management
37
- const [processingPushOnLaunch, setProcessingPushOnLaunch] = (0, react_1.useState)(false);
38
- const [isHandlingForegroundCall, setIsHandlingForegroundCall] = (0, react_1.useState)(false);
39
- const [currentConnectionState, setCurrentConnectionState] = (0, react_1.useState)(
40
- voipClient.currentConnectionState
41
- );
42
- // Refs for tracking state
43
- const appStateRef = (0, react_1.useRef)(react_native_1.AppState.currentState);
44
- const backgroundDetectorIgnore = (0, react_1.useRef)(false);
45
- // Static background client instance for singleton pattern
46
- const backgroundClientRef = (0, react_1.useRef)(null);
47
- const log = (0, react_1.useCallback)(
48
- (message, ...args) => {
49
- if (debug) {
50
- console.log(`[TelnyxVoiceApp] ${message}`, ...args);
51
- }
52
- },
53
- [debug]
54
- );
55
- // Handle app state changes
56
- const handleAppStateChange = (0, react_1.useCallback)(
57
- async (nextAppState) => {
58
- const previousAppState = appStateRef.current;
59
- appStateRef.current = nextAppState;
60
- log(`App state changed from ${previousAppState} to ${nextAppState}`);
61
- log(`Background detector ignore flag: ${backgroundDetectorIgnore.current}`);
62
- log(`Handling foreground call: ${isHandlingForegroundCall}`);
63
- // Call optional user callback first
64
- onAppStateChanged?.(nextAppState);
65
- // Only handle background disconnection when actually transitioning from active to background
66
- // Don't disconnect on background-to-background transitions (e.g., during CallKit operations)
67
- if (
68
- (nextAppState === 'background' || nextAppState === 'inactive') &&
69
- previousAppState === 'active'
70
- ) {
71
- log(
72
- `App transitioned from ${previousAppState} to ${nextAppState} - handling backgrounding`
73
- );
74
- await handleAppBackgrounded();
75
- } else if (nextAppState === 'background' || nextAppState === 'inactive') {
76
- log(
77
- `App state is ${nextAppState} but was already ${previousAppState} - skipping background handling`
78
- );
79
- }
80
- // Always check for push notifications when app becomes active (regardless of auto-reconnect setting)
81
- if (nextAppState === 'active' && previousAppState !== 'active') {
82
- log('App became active - checking for push notifications');
26
+ const TelnyxVoiceAppComponent = ({ voipClient, children, onPushNotificationProcessingStarted, onPushNotificationProcessingCompleted, onAppStateChanged, enableAutoReconnect = true, skipWebBackgroundDetection = true, debug = false, }) => {
27
+ // State management
28
+ const [processingPushOnLaunch, setProcessingPushOnLaunch] = (0, react_1.useState)(false);
29
+ const [isHandlingForegroundCall, setIsHandlingForegroundCall] = (0, react_1.useState)(false);
30
+ const [currentConnectionState, setCurrentConnectionState] = (0, react_1.useState)(voipClient.currentConnectionState);
31
+ // Refs for tracking state
32
+ const appStateRef = (0, react_1.useRef)(react_native_1.AppState.currentState);
33
+ const backgroundDetectorIgnore = (0, react_1.useRef)(false);
34
+ // Static background client instance for singleton pattern
35
+ const backgroundClientRef = (0, react_1.useRef)(null);
36
+ const log = (0, react_1.useCallback)((message, ...args) => {
37
+ if (debug) {
38
+ console.log(`[TelnyxVoiceApp] ${message}`, ...args);
39
+ }
40
+ }, [debug]);
41
+ // Handle app state changes
42
+ const handleAppStateChange = (0, react_1.useCallback)(async (nextAppState) => {
43
+ const previousAppState = appStateRef.current;
44
+ appStateRef.current = nextAppState;
45
+ log(`App state changed from ${previousAppState} to ${nextAppState}`);
46
+ log(`Background detector ignore flag: ${backgroundDetectorIgnore.current}`);
47
+ log(`Handling foreground call: ${isHandlingForegroundCall}`);
48
+ // Call optional user callback first
49
+ onAppStateChanged?.(nextAppState);
50
+ // Only handle background disconnection when actually transitioning from active to background
51
+ // Don't disconnect on background-to-background transitions (e.g., during CallKit operations)
52
+ if ((nextAppState === 'background' || nextAppState === 'inactive') &&
53
+ previousAppState === 'active') {
54
+ log(`App transitioned from ${previousAppState} to ${nextAppState} - handling backgrounding`);
55
+ await handleAppBackgrounded();
56
+ }
57
+ else if (nextAppState === 'background' || nextAppState === 'inactive') {
58
+ log(`App state is ${nextAppState} but was already ${previousAppState} - skipping background handling`);
59
+ }
60
+ // Always check for push notifications when app becomes active (regardless of auto-reconnect setting)
61
+ if (nextAppState === 'active' && previousAppState !== 'active') {
62
+ log('App became active - checking for push notifications');
63
+ await checkForInitialPushNotification(true); // Pass true for fromAppResume
64
+ }
65
+ // Only handle auto-reconnection if auto-reconnect is enabled
66
+ if (enableAutoReconnect && nextAppState === 'active' && previousAppState !== 'active') {
67
+ await handleAppResumed();
68
+ }
69
+ }, [enableAutoReconnect, onAppStateChanged, isHandlingForegroundCall, log]);
70
+ // Handle app going to background - disconnect like the old implementation
71
+ const handleAppBackgrounded = (0, react_1.useCallback)(async () => {
72
+ // Check if we should ignore background detection (e.g., during active calls)
73
+ if (backgroundDetectorIgnore.current || isHandlingForegroundCall) {
74
+ log('Background detector ignore flag set or handling foreground call - skipping disconnection');
75
+ return;
76
+ }
77
+ // Check if there are any active calls that should prevent disconnection
78
+ const activeCalls = voipClient.currentCalls;
79
+ const hasOngoingCall = activeCalls.length > 0 &&
80
+ activeCalls.some((call) => call.currentState === 'ACTIVE' ||
81
+ call.currentState === 'HELD' ||
82
+ call.currentState === 'RINGING' ||
83
+ call.currentState === 'CONNECTING');
84
+ // Also check if there's an incoming call from push notification being processed
85
+ let isCallFromPush = false;
86
+ if (react_native_1.Platform.OS === 'ios') {
87
+ try {
88
+ const { callKitCoordinator } = require('./callkit/callkit-coordinator');
89
+ isCallFromPush = callKitCoordinator.getIsCallFromPush();
90
+ }
91
+ catch (e) {
92
+ log('Error checking isCallFromPush:', e);
93
+ }
94
+ }
95
+ if (hasOngoingCall || isCallFromPush) {
96
+ log('Active calls or push call detected - skipping background disconnection', {
97
+ callCount: activeCalls.length,
98
+ hasOngoingCall,
99
+ isCallFromPush,
100
+ callStates: activeCalls.map((call) => ({
101
+ callId: call.callId,
102
+ currentState: call.currentState,
103
+ destination: call.destination,
104
+ })),
105
+ });
106
+ return;
107
+ }
108
+ log('App backgrounded - disconnecting (matching old BackgroundDetector behavior)');
109
+ try {
110
+ // Always disconnect when backgrounded (matches old implementation)
111
+ await voipClient.logout();
112
+ log('Successfully disconnected on background');
113
+ }
114
+ catch (e) {
115
+ log('Error disconnecting on background:', e);
116
+ }
117
+ }, [voipClient, isHandlingForegroundCall, log]);
118
+ // Handle app resuming from background
119
+ const handleAppResumed = (0, react_1.useCallback)(async () => {
120
+ log('App resumed - checking reconnection needs');
121
+ // IMPORTANT: Check for push notifications first when resuming from background
122
+ // This handles the case where the user accepted a call while the app was backgrounded
83
123
  await checkForInitialPushNotification(true); // Pass true for fromAppResume
84
- }
85
- // Only handle auto-reconnection if auto-reconnect is enabled
86
- if (enableAutoReconnect && nextAppState === 'active' && previousAppState !== 'active') {
87
- await handleAppResumed();
88
- }
89
- },
90
- [enableAutoReconnect, onAppStateChanged, isHandlingForegroundCall, log]
91
- );
92
- // Handle app going to background - disconnect like the old implementation
93
- const handleAppBackgrounded = (0, react_1.useCallback)(async () => {
94
- // Check if we should ignore background detection (e.g., during active calls)
95
- if (backgroundDetectorIgnore.current || isHandlingForegroundCall) {
96
- log(
97
- 'Background detector ignore flag set or handling foreground call - skipping disconnection'
98
- );
99
- return;
100
- }
101
- // Check if there are any active calls that should prevent disconnection
102
- const activeCalls = voipClient.currentCalls;
103
- const hasOngoingCall =
104
- activeCalls.length > 0 &&
105
- activeCalls.some(
106
- (call) =>
107
- call.currentState === 'ACTIVE' ||
108
- call.currentState === 'HELD' ||
109
- call.currentState === 'RINGING' ||
110
- call.currentState === 'CONNECTING'
111
- );
112
- // Also check if there's an incoming call from push notification being processed
113
- let isCallFromPush = false;
114
- if (react_native_1.Platform.OS === 'ios') {
115
- try {
116
- const { callKitCoordinator } = require('./callkit/callkit-coordinator');
117
- isCallFromPush = callKitCoordinator.getIsCallFromPush();
118
- } catch (e) {
119
- log('Error checking isCallFromPush:', e);
120
- }
121
- }
122
- if (hasOngoingCall || isCallFromPush) {
123
- log('Active calls or push call detected - skipping background disconnection', {
124
- callCount: activeCalls.length,
125
- hasOngoingCall,
126
- isCallFromPush,
127
- callStates: activeCalls.map((call) => ({
128
- callId: call.callId,
129
- currentState: call.currentState,
130
- destination: call.destination,
131
- })),
132
- });
133
- return;
134
- }
135
- log('App backgrounded - disconnecting (matching old BackgroundDetector behavior)');
136
- try {
137
- // Always disconnect when backgrounded (matches old implementation)
138
- await voipClient.logout();
139
- log('Successfully disconnected on background');
140
- } catch (e) {
141
- log('Error disconnecting on background:', e);
142
- }
143
- }, [voipClient, isHandlingForegroundCall, log]);
144
- // Handle app resuming from background
145
- const handleAppResumed = (0, react_1.useCallback)(async () => {
146
- log('App resumed - checking reconnection needs');
147
- // IMPORTANT: Check for push notifications first when resuming from background
148
- // This handles the case where the user accepted a call while the app was backgrounded
149
- await checkForInitialPushNotification(true); // Pass true for fromAppResume
150
- // If we're ignoring (e.g., from push call) or handling foreground call, don't auto-reconnect
151
- if (backgroundDetectorIgnore.current || isHandlingForegroundCall) {
152
- log(
153
- 'Background detector ignore flag set or handling foreground call - skipping reconnection'
154
- );
155
- return;
156
- }
157
- // iOS-specific: If push notification handling just initiated a connection,
158
- // skip auto-reconnection to prevent double login
159
- if (react_native_1.Platform.OS === 'ios') {
160
- // Check if connection state changed after push processing
161
- const connectionStateAfterPush = voipClient.currentConnectionState;
162
- if (
163
- connectionStateAfterPush === connection_state_1.TelnyxConnectionState.CONNECTING ||
164
- connectionStateAfterPush === connection_state_1.TelnyxConnectionState.CONNECTED
165
- ) {
166
- log(
167
- `iOS: Push handling initiated connection (${connectionStateAfterPush}), skipping auto-reconnection`
168
- );
169
- return;
170
- }
171
- }
172
- // Check current connection state and reconnect if needed
173
- const currentState = voipClient.currentConnectionState;
174
- log(`Current connection state: ${currentState}`);
175
- // If we're not connected and have stored credentials, attempt reconnection
176
- if (currentState !== connection_state_1.TelnyxConnectionState.CONNECTED) {
177
- await attemptAutoReconnection();
178
- }
179
- }, [voipClient, isHandlingForegroundCall, log]);
180
- // Attempt to reconnect using stored credentials
181
- const attemptAutoReconnection = (0, react_1.useCallback)(async () => {
182
- try {
183
- log('Attempting auto-reconnection...');
184
- // Try to get stored config and reconnect
185
- const success = await voipClient.loginFromStoredConfig();
186
- log(`Auto-reconnection ${success ? 'successful' : 'failed'}`);
187
- // If auto-reconnection fails, redirect to login screen
188
- if (!success) {
189
- log('Auto-reconnection failed - redirecting to login screen');
190
- // Import router dynamically to avoid circular dependency issues
191
- const { router } = require('expo-router');
192
- // Small delay to ensure state is settled
193
- setTimeout(() => {
194
- router.replace('/');
195
- }, 100);
196
- }
197
- } catch (e) {
198
- log('Auto-reconnection error:', e);
199
- // On error, also redirect to login
200
- log('Auto-reconnection error - redirecting to login screen');
201
- const { router } = require('expo-router');
202
- setTimeout(() => {
203
- router.replace('/');
204
- }, 100);
205
- }
206
- }, [voipClient, log]);
207
- // Check for initial push notification when app launches
208
- const checkForInitialPushNotification = (0, react_1.useCallback)(
209
- async (fromAppResume = false) => {
210
- log(`checkForInitialPushNotification called${fromAppResume ? ' (from app resume)' : ''}`);
211
- if (processingPushOnLaunch && !fromAppResume) {
212
- log('Already processing push, returning early');
213
- return;
214
- }
215
- // Only set the flag if this is not from app resume to allow resume processing
216
- if (!fromAppResume) {
217
- setProcessingPushOnLaunch(true);
218
- }
219
- onPushNotificationProcessingStarted?.();
220
- try {
221
- let pushData = null;
222
- // Try to get push data from the native layer using our VoicePnBridge
124
+ // If we're ignoring (e.g., from push call) or handling foreground call, don't auto-reconnect
125
+ if (backgroundDetectorIgnore.current || isHandlingForegroundCall) {
126
+ log('Background detector ignore flag set or handling foreground call - skipping reconnection');
127
+ return;
128
+ }
129
+ // iOS-specific: If push notification handling just initiated a connection,
130
+ // skip auto-reconnection to prevent double login
131
+ if (react_native_1.Platform.OS === 'ios') {
132
+ // Check if connection state changed after push processing
133
+ const connectionStateAfterPush = voipClient.currentConnectionState;
134
+ if (connectionStateAfterPush === connection_state_1.TelnyxConnectionState.CONNECTING ||
135
+ connectionStateAfterPush === connection_state_1.TelnyxConnectionState.CONNECTED) {
136
+ log(`iOS: Push handling initiated connection (${connectionStateAfterPush}), skipping auto-reconnection`);
137
+ return;
138
+ }
139
+ }
140
+ // Check current connection state and reconnect if needed
141
+ const currentState = voipClient.currentConnectionState;
142
+ log(`Current connection state: ${currentState}`);
143
+ // If we're not connected and have stored credentials, attempt reconnection
144
+ if (currentState !== connection_state_1.TelnyxConnectionState.CONNECTED) {
145
+ await attemptAutoReconnection();
146
+ }
147
+ }, [voipClient, isHandlingForegroundCall, log]);
148
+ // Attempt to reconnect using stored credentials
149
+ const attemptAutoReconnection = (0, react_1.useCallback)(async () => {
223
150
  try {
224
- // Import the native bridge module dynamically
225
- const { NativeModules } = require('react-native');
226
- const VoicePnBridge = NativeModules.VoicePnBridge;
227
- if (VoicePnBridge) {
228
- log('Checking for pending push actions via VoicePnBridge');
229
- const pendingAction = await VoicePnBridge.getPendingPushAction();
230
- log('Raw pending action response:', pendingAction);
231
- if (pendingAction && pendingAction.action != null && pendingAction.metadata != null) {
232
- log('Found pending push action:', pendingAction);
233
- // Parse the metadata if it's a string
234
- let metadata = pendingAction.metadata;
235
- if (typeof metadata === 'string') {
236
- try {
237
- // First try parsing as JSON
238
- metadata = JSON.parse(metadata);
239
- log('Parsed metadata as JSON:', metadata);
240
- } catch (e) {
241
- // If JSON parsing fails, try parsing Android key-value format
242
- // Format: "{call_id=value, action=value}"
243
- log('JSON parse failed, trying Android key-value format');
244
- try {
245
- const cleanedString = metadata.replace(/[{}]/g, '').trim();
246
- const pairs = cleanedString.split(',').map((pair) => pair.trim());
247
- const parsed = {};
248
- for (const pair of pairs) {
249
- const [key, value] = pair.split('=').map((s) => s.trim());
250
- if (key && value) {
251
- parsed[key] = value;
252
- }
151
+ log('Attempting auto-reconnection...');
152
+ // Try to get stored config and reconnect
153
+ const success = await voipClient.loginFromStoredConfig();
154
+ log(`Auto-reconnection ${success ? 'successful' : 'failed'}`);
155
+ // If auto-reconnection fails, redirect to login screen
156
+ if (!success) {
157
+ log('Auto-reconnection failed - redirecting to login screen');
158
+ }
159
+ }
160
+ catch (e) {
161
+ log('Auto-reconnection error:', e);
162
+ // On error, also redirect to login
163
+ log('Auto-reconnection error - redirecting to login screen');
164
+ }
165
+ }, [voipClient, log]);
166
+ // Check for initial push notification action when app launches
167
+ const checkForInitialPushNotification = (0, react_1.useCallback)(async (fromAppResume = false) => {
168
+ log(`checkForInitialPushNotification called${fromAppResume ? ' (from app resume)' : ''}`);
169
+ if (processingPushOnLaunch && !fromAppResume) {
170
+ log('Already processing push, returning early');
171
+ return;
172
+ }
173
+ // Only set the flag if this is not from app resume to allow resume processing
174
+ if (!fromAppResume) {
175
+ setProcessingPushOnLaunch(true);
176
+ }
177
+ onPushNotificationProcessingStarted?.();
178
+ try {
179
+ let pushData = null;
180
+ // Try to get push data from the native layer using our VoicePnBridge
181
+ try {
182
+ // Import the native bridge module dynamically
183
+ const { NativeModules } = require('react-native');
184
+ const VoicePnBridge = NativeModules.VoicePnBridge;
185
+ if (VoicePnBridge) {
186
+ log('Checking for pending push actions via VoicePnBridge');
187
+ // First check for pending call actions (notification button taps like hangup/answer)
188
+ const pendingCallAction = await VoicePnBridge.getPendingCallAction();
189
+ log('Raw pending call action response:', pendingCallAction);
190
+ if (pendingCallAction && pendingCallAction.action != null) {
191
+ log('Found pending call action:', pendingCallAction);
192
+ // Handle call actions directly
193
+ if (pendingCallAction.action === 'hangup' && pendingCallAction.callId) {
194
+ log('Processing hangup action from notification for call:', pendingCallAction.callId);
195
+ // Find and hangup the call
196
+ const activeCall = voipClient.currentActiveCall;
197
+ if (activeCall && activeCall.callId === pendingCallAction.callId) {
198
+ log('Hanging up active call from notification action');
199
+ try {
200
+ await activeCall.hangup();
201
+ log('Call hung up successfully from notification action');
202
+ }
203
+ catch (error) {
204
+ log('Error hanging up call from notification action:', error);
205
+ }
206
+ }
207
+ else {
208
+ log('No matching active call found for hangup action');
209
+ }
210
+ // Clear the pending action
211
+ await VoicePnBridge.clearPendingCallAction();
212
+ return; // Don't process as push data
213
+ }
214
+ }
215
+ // Then check for regular push notification data
216
+ const pendingAction = await VoicePnBridge.getPendingPushAction();
217
+ log('Raw pending action response:', pendingAction);
218
+ if (pendingAction && pendingAction.action != null && pendingAction.metadata != null) {
219
+ log('Found pending push action:', pendingAction);
220
+ // Parse the metadata if it's a string
221
+ let metadata = pendingAction.metadata;
222
+ try {
223
+ // First try parsing as JSON
224
+ metadata = JSON.parse(metadata);
225
+ log('Parsed metadata as JSON:', metadata);
226
+ }
227
+ catch (e) {
228
+ log('JSON parse failed, trying Android key-value format');
229
+ }
230
+ // Create push data structure that matches what the VoIP client expects
231
+ pushData = {
232
+ action: pendingAction.action,
233
+ metadata: metadata,
234
+ from_notification: true,
235
+ };
236
+ // Clear the pending action so it doesn't get processed again
237
+ await VoicePnBridge.clearPendingPushAction();
238
+ log('Cleared pending push action after retrieval');
239
+ }
240
+ else {
241
+ log('No pending push actions found');
253
242
  }
254
- metadata = parsed;
255
- log('Parsed metadata as Android key-value format:', metadata);
256
- } catch (parseError) {
257
- log('Failed to parse metadata in any format, using as-is:', parseError);
258
- }
259
243
  }
260
- }
261
- // Create push data structure that matches what the VoIP client expects
262
- pushData = {
263
- action: pendingAction.action,
264
- metadata: metadata,
265
- from_notification: true,
266
- };
267
- // Clear the pending action so it doesn't get processed again
268
- await VoicePnBridge.clearPendingPushAction();
269
- log('Cleared pending push action after retrieval');
270
- } else {
271
- log('No pending push actions found');
244
+ else {
245
+ log('VoicePnBridge not available - this is expected on iOS');
246
+ }
247
+ }
248
+ catch (bridgeError) {
249
+ log('Error accessing VoicePnBridge:', bridgeError);
250
+ }
251
+ // Process the push notification if found
252
+ if (pushData) {
253
+ log('Processing initial push notification...');
254
+ // Check if we're already connected and handling a push - prevent duplicate processing
255
+ const isConnected = voipClient.currentConnectionState === connection_state_1.TelnyxConnectionState.CONNECTED;
256
+ if (isConnected) {
257
+ log('SKIPPING - Already connected, preventing duplicate processing');
258
+ // Clear the stored data since we're already handling it
259
+ // TODO: Implement clearPushMetaData
260
+ return;
261
+ }
262
+ // Set flags to prevent auto-reconnection during push call
263
+ setIsHandlingForegroundCall(true);
264
+ backgroundDetectorIgnore.current = true;
265
+ log(`Background detector ignore set to: true at ${new Date().toISOString()}`);
266
+ log(`Foreground call handling flag set to: true at ${new Date().toISOString()}`);
267
+ // Dispose any existing background client to prevent conflicts
268
+ disposeBackgroundClient();
269
+ // Handle the push notification using platform-specific approach
270
+ if (react_native_1.Platform.OS === 'ios') {
271
+ // On iOS, coordinate with CallKit by notifying the coordinator about the push
272
+ const { callKitCoordinator } = require('./callkit/callkit-coordinator');
273
+ // Extract call_id from nested metadata structure to use as CallKit UUID
274
+ const callId = pushData.metadata?.metadata?.call_id;
275
+ if (callId) {
276
+ log('Notifying CallKit coordinator about push notification:', callId);
277
+ await callKitCoordinator.handleCallKitPushReceived(callId, {
278
+ callData: { source: 'push_notification' },
279
+ pushData: pushData,
280
+ });
281
+ }
282
+ else {
283
+ log('No call_id found in push data, falling back to direct handling');
284
+ await voipClient.handlePushNotification(pushData);
285
+ }
286
+ }
287
+ else {
288
+ // On other platforms, handle push notification directly
289
+ await voipClient.handlePushNotification(pushData);
290
+ }
291
+ log('Initial push notification processed');
292
+ log('Cleared stored push data to prevent duplicate processing');
293
+ // Note: isHandlingForegroundCall will be reset when calls.length becomes 0
294
+ // This prevents premature disconnection during CallKit answer flow
295
+ }
296
+ else {
297
+ log('No initial push data found');
272
298
  }
273
- } else {
274
- log('VoicePnBridge not available - this is expected on iOS');
275
- }
276
- } catch (bridgeError) {
277
- log('Error accessing VoicePnBridge:', bridgeError);
278
299
  }
279
- // Process the push notification if found
280
- if (pushData) {
281
- log('Processing initial push notification...');
282
- // Check if we're already connected and handling a push - prevent duplicate processing
283
- const isConnected =
284
- voipClient.currentConnectionState ===
285
- connection_state_1.TelnyxConnectionState.CONNECTED;
286
- if (isConnected) {
287
- log('SKIPPING - Already connected, preventing duplicate processing');
288
- // Clear the stored data since we're already handling it
289
- // TODO: Implement clearPushMetaData
290
- return;
291
- }
292
- // Set flags to prevent auto-reconnection during push call
293
- setIsHandlingForegroundCall(true);
294
- backgroundDetectorIgnore.current = true;
295
- log(`Background detector ignore set to: true at ${new Date().toISOString()}`);
296
- log(`Foreground call handling flag set to: true at ${new Date().toISOString()}`);
297
- // Dispose any existing background client to prevent conflicts
298
- disposeBackgroundClient();
299
- // Handle the push notification using platform-specific approach
300
- if (react_native_1.Platform.OS === 'ios') {
301
- // On iOS, coordinate with CallKit by notifying the coordinator about the push
302
- const { callKitCoordinator } = require('./callkit/callkit-coordinator');
303
- // Extract call_id from nested metadata structure to use as CallKit UUID
304
- const callId = pushData.metadata?.metadata?.call_id;
305
- if (callId) {
306
- log('Notifying CallKit coordinator about push notification:', callId);
307
- await callKitCoordinator.handleCallKitPushReceived(callId, {
308
- callData: { source: 'push_notification' },
309
- pushData: pushData,
310
- });
311
- } else {
312
- log('No call_id found in push data, falling back to direct handling');
313
- await voipClient.handlePushNotification(pushData);
300
+ catch (e) {
301
+ log('Error processing initial push notification:', e);
302
+ // Reset flags on error
303
+ setIsHandlingForegroundCall(false);
304
+ }
305
+ finally {
306
+ // Always reset the processing flag - it should not remain stuck
307
+ setProcessingPushOnLaunch(false);
308
+ onPushNotificationProcessingCompleted?.();
309
+ }
310
+ }, [
311
+ processingPushOnLaunch,
312
+ voipClient,
313
+ onPushNotificationProcessingStarted,
314
+ onPushNotificationProcessingCompleted,
315
+ log,
316
+ ]);
317
+ // Dispose background client instance when no longer needed
318
+ const disposeBackgroundClient = (0, react_1.useCallback)(() => {
319
+ if (backgroundClientRef.current) {
320
+ log('Disposing background client instance');
321
+ backgroundClientRef.current.dispose();
322
+ backgroundClientRef.current = null;
323
+ }
324
+ }, [log]);
325
+ // Create background client for push notification handling
326
+ const createBackgroundClient = (0, react_1.useCallback)(() => {
327
+ log('Creating background client instance');
328
+ const backgroundClient = (0, telnyx_voip_client_1.createBackgroundTelnyxVoipClient)({
329
+ debug,
330
+ });
331
+ return backgroundClient;
332
+ }, [debug, log]);
333
+ // Setup effect
334
+ (0, react_1.useEffect)(() => {
335
+ // Listen to connection state changes
336
+ const connectionStateSubscription = voipClient.connectionState$.subscribe((state) => {
337
+ setCurrentConnectionState(state);
338
+ // Just log connection changes, let the app handle navigation
339
+ log(`Connection state changed to: ${state}`);
340
+ });
341
+ // Listen to call changes to reset flags when no active calls
342
+ const callsSubscription = voipClient.calls$.subscribe((calls) => {
343
+ // Check if we should reset flags - only reset if:
344
+ // 1. No active WebRTC calls AND
345
+ // 2. No CallKit operations in progress (to prevent disconnection during CallKit answer flow)
346
+ const hasActiveWebRTCCalls = calls.length > 0;
347
+ let hasCallKitProcessing = false;
348
+ // Check CallKit processing calls only on iOS
349
+ if (react_native_1.Platform.OS === 'ios') {
350
+ try {
351
+ const { callKitCoordinator } = require('./callkit/callkit-coordinator');
352
+ hasCallKitProcessing = callKitCoordinator.hasProcessingCalls();
353
+ log(`CallKit processing check: hasProcessingCalls=${hasCallKitProcessing}`);
354
+ }
355
+ catch (e) {
356
+ log('Error checking CallKit processing calls:', e);
357
+ }
358
+ }
359
+ log(`Flag reset check: WebRTC calls=${calls.length}, CallKit processing=${hasCallKitProcessing}, isHandlingForegroundCall=${isHandlingForegroundCall}, backgroundDetectorIgnore=${backgroundDetectorIgnore.current}`);
360
+ if (!hasActiveWebRTCCalls &&
361
+ !hasCallKitProcessing &&
362
+ (isHandlingForegroundCall || backgroundDetectorIgnore.current)) {
363
+ log(`No active calls and no CallKit processing - resetting ignore flags at ${new Date().toISOString()}`);
364
+ setIsHandlingForegroundCall(false);
365
+ backgroundDetectorIgnore.current = false;
366
+ }
367
+ else if (!hasActiveWebRTCCalls && hasCallKitProcessing) {
368
+ log(`No WebRTC calls but CallKit operations in progress - keeping ignore flags active at ${new Date().toISOString()}`);
369
+ }
370
+ else if (hasActiveWebRTCCalls) {
371
+ log(`WebRTC calls active - keeping ignore flags active at ${new Date().toISOString()}`);
372
+ }
373
+ // Also reset processingPushOnLaunch if no calls are active
374
+ // This ensures the flag doesn't get stuck after call ends
375
+ if (calls.length === 0 && processingPushOnLaunch) {
376
+ log('No active calls - resetting processing push flag');
377
+ setProcessingPushOnLaunch(false);
378
+ }
379
+ });
380
+ // Listen for immediate call action events from notification buttons (Android only)
381
+ let callActionSubscription = null;
382
+ if (react_native_1.Platform.OS === 'android') {
383
+ try {
384
+ const { VoicePnBridge } = require('./internal/voice-pn-bridge');
385
+ callActionSubscription = VoicePnBridge.addCallActionListener((event) => {
386
+ log(`Received immediate call action: ${event.action} for callId: ${event.callId}`);
387
+ // Handle immediate call actions (mainly for ending active calls from notification)
388
+ if (event.action === 'hangup' ||
389
+ event.action === 'endCall' ||
390
+ event.action === 'reject') {
391
+ log(`Processing immediate end call action for callId: ${event.callId}`);
392
+ // Find the call by ID and end it
393
+ const targetCall = voipClient.currentCalls.find((call) => call.callId === event.callId);
394
+ if (targetCall) {
395
+ log(`Found active call ${event.callId}, ending it immediately`);
396
+ targetCall.hangup().catch((error) => {
397
+ log(`Error ending call ${event.callId}:`, error);
398
+ });
399
+ }
400
+ else {
401
+ log(`No active call found with ID ${event.callId}`);
402
+ }
403
+ }
404
+ });
405
+ log('Call action listener registered for immediate notification handling');
406
+ }
407
+ catch (e) {
408
+ log('Error setting up call action listener (VoicePnBridge not available):', e);
314
409
  }
315
- } else {
316
- // On other platforms, handle push notification directly
317
- await voipClient.handlePushNotification(pushData);
318
- }
319
- log('Initial push notification processed');
320
- log('Cleared stored push data to prevent duplicate processing');
321
- // Note: isHandlingForegroundCall will be reset when calls.length becomes 0
322
- // This prevents premature disconnection during CallKit answer flow
323
- } else {
324
- log('No initial push data found');
325
410
  }
326
- } catch (e) {
327
- log('Error processing initial push notification:', e);
328
- // Reset flags on error
329
- setIsHandlingForegroundCall(false);
330
- } finally {
331
- // Always reset the processing flag - it should not remain stuck
332
- setProcessingPushOnLaunch(false);
333
- onPushNotificationProcessingCompleted?.();
334
- }
335
- },
336
- [
337
- processingPushOnLaunch,
338
- voipClient,
339
- onPushNotificationProcessingStarted,
340
- onPushNotificationProcessingCompleted,
341
- log,
342
- ]
343
- );
344
- // Dispose background client instance when no longer needed
345
- const disposeBackgroundClient = (0, react_1.useCallback)(() => {
346
- if (backgroundClientRef.current) {
347
- log('Disposing background client instance');
348
- backgroundClientRef.current.dispose();
349
- backgroundClientRef.current = null;
350
- }
351
- }, [log]);
352
- // Create background client for push notification handling
353
- const createBackgroundClient = (0, react_1.useCallback)(() => {
354
- log('Creating background client instance');
355
- const backgroundClient = (0, telnyx_voip_client_1.createBackgroundTelnyxVoipClient)({
356
- debug,
357
- });
358
- return backgroundClient;
359
- }, [debug, log]);
360
- // Setup effect
361
- (0, react_1.useEffect)(() => {
362
- // Listen to connection state changes
363
- const connectionStateSubscription = voipClient.connectionState$.subscribe((state) => {
364
- setCurrentConnectionState(state);
365
- // Just log connection changes, let the app handle navigation
366
- log(`Connection state changed to: ${state}`);
367
- });
368
- // Listen to call changes to reset flags when no active calls
369
- const callsSubscription = voipClient.calls$.subscribe((calls) => {
370
- // Check if we should reset flags - only reset if:
371
- // 1. No active WebRTC calls AND
372
- // 2. No CallKit operations in progress (to prevent disconnection during CallKit answer flow)
373
- const hasActiveWebRTCCalls = calls.length > 0;
374
- let hasCallKitProcessing = false;
375
- // Check CallKit processing calls only on iOS
376
- if (react_native_1.Platform.OS === 'ios') {
377
- try {
378
- const { callKitCoordinator } = require('./callkit/callkit-coordinator');
379
- hasCallKitProcessing = callKitCoordinator.hasProcessingCalls();
380
- log(`CallKit processing check: hasProcessingCalls=${hasCallKitProcessing}`);
381
- } catch (e) {
382
- log('Error checking CallKit processing calls:', e);
411
+ // Add app state listener if not skipping web background detection or not on web
412
+ // AND if app state management is enabled in the client options
413
+ let appStateSubscription = null;
414
+ if ((!skipWebBackgroundDetection || react_native_1.Platform.OS !== 'web') &&
415
+ voipClient.options.enableAppStateManagement) {
416
+ appStateSubscription = react_native_1.AppState.addEventListener('change', handleAppStateChange);
383
417
  }
384
- }
385
- log(
386
- `Flag reset check: WebRTC calls=${calls.length}, CallKit processing=${hasCallKitProcessing}, isHandlingForegroundCall=${isHandlingForegroundCall}, backgroundDetectorIgnore=${backgroundDetectorIgnore.current}`
387
- );
388
- if (
389
- !hasActiveWebRTCCalls &&
390
- !hasCallKitProcessing &&
391
- (isHandlingForegroundCall || backgroundDetectorIgnore.current)
392
- ) {
393
- log(
394
- `No active calls and no CallKit processing - resetting ignore flags at ${new Date().toISOString()}`
395
- );
396
- setIsHandlingForegroundCall(false);
397
- backgroundDetectorIgnore.current = false;
398
- } else if (!hasActiveWebRTCCalls && hasCallKitProcessing) {
399
- log(
400
- `No WebRTC calls but CallKit operations in progress - keeping ignore flags active at ${new Date().toISOString()}`
401
- );
402
- } else if (hasActiveWebRTCCalls) {
403
- log(`WebRTC calls active - keeping ignore flags active at ${new Date().toISOString()}`);
404
- }
405
- // Also reset processingPushOnLaunch if no calls are active
406
- // This ensures the flag doesn't get stuck after call ends
407
- if (calls.length === 0 && processingPushOnLaunch) {
408
- log('No active calls - resetting processing push flag');
409
- setProcessingPushOnLaunch(false);
410
- }
411
- });
412
- // Add app state listener if not skipping web background detection or not on web
413
- // AND if app state management is enabled in the client options
414
- let appStateSubscription = null;
415
- if (
416
- (!skipWebBackgroundDetection || react_native_1.Platform.OS !== 'web') &&
417
- voipClient.options.enableAppStateManagement
418
- ) {
419
- appStateSubscription = react_native_1.AppState.addEventListener(
420
- 'change',
421
- handleAppStateChange
422
- );
423
- }
424
- // Handle initial push notification if app was launched from terminated state
425
- // Only check if we're not already processing to prevent infinite loops
426
- const timeoutId = setTimeout(() => {
427
- if (!processingPushOnLaunch) {
428
- checkForInitialPushNotification();
429
- }
430
- }, 100);
431
- // Cleanup function
432
- return () => {
433
- connectionStateSubscription.unsubscribe();
434
- callsSubscription.unsubscribe();
435
- if (appStateSubscription) {
436
- appStateSubscription.remove();
437
- }
438
- clearTimeout(timeoutId);
439
- // Clean up background client instance
440
- disposeBackgroundClient();
441
- };
442
- }, [
443
- voipClient,
444
- handleAppStateChange,
445
- disposeBackgroundClient,
446
- skipWebBackgroundDetection,
447
- isHandlingForegroundCall,
448
- log,
449
- ]);
450
- // Simply return the children wrapped in context provider - all lifecycle management is handled internally
451
- return (0, jsx_runtime_1.jsx)(TelnyxVoiceContext_1.TelnyxVoiceProvider, {
452
- voipClient: voipClient,
453
- children: children,
454
- });
418
+ // Handle initial push notification if app was launched from terminated state
419
+ // Only check if we're not already processing to prevent infinite loops
420
+ const timeoutId = setTimeout(() => {
421
+ if (!processingPushOnLaunch) {
422
+ checkForInitialPushNotification();
423
+ }
424
+ }, 100);
425
+ // Cleanup function
426
+ return () => {
427
+ connectionStateSubscription.unsubscribe();
428
+ callsSubscription.unsubscribe();
429
+ if (callActionSubscription) {
430
+ try {
431
+ const { VoicePnBridge } = require('./internal/voice-pn-bridge');
432
+ VoicePnBridge.removeCallActionListener(callActionSubscription);
433
+ log('Call action listener removed');
434
+ }
435
+ catch (e) {
436
+ log('Error removing call action listener:', e);
437
+ }
438
+ }
439
+ if (appStateSubscription) {
440
+ appStateSubscription.remove();
441
+ }
442
+ clearTimeout(timeoutId);
443
+ // Clean up background client instance
444
+ disposeBackgroundClient();
445
+ };
446
+ }, [
447
+ voipClient,
448
+ handleAppStateChange,
449
+ disposeBackgroundClient,
450
+ skipWebBackgroundDetection,
451
+ isHandlingForegroundCall,
452
+ log,
453
+ ]);
454
+ // Simply return the children wrapped in context provider - all lifecycle management is handled internally
455
+ return (0, jsx_runtime_1.jsx)(TelnyxVoiceContext_1.TelnyxVoiceProvider, { voipClient: voipClient, children: children });
455
456
  };
456
457
  /**
457
458
  * Static factory method that handles all common SDK initialization boilerplate.
@@ -475,70 +476,51 @@ const TelnyxVoiceAppComponent = ({
475
476
  * ```
476
477
  */
477
478
  const initializeAndCreate = async (options) => {
478
- const {
479
- voipClient,
480
- children,
481
- backgroundMessageHandler,
482
- onPushNotificationProcessingStarted,
483
- onPushNotificationProcessingCompleted,
484
- onAppStateChanged,
485
- enableAutoReconnect = true,
486
- skipWebBackgroundDetection = true,
487
- debug = false,
488
- } = options;
489
- // Initialize push notification handling for Android
490
- if (react_native_1.Platform.OS === 'android') {
491
- // TODO: Initialize Firebase or other push notification service
492
- if (debug) {
493
- console.log('[TelnyxVoiceApp] Android push notification initialization needed');
479
+ const { voipClient, children, backgroundMessageHandler, onPushNotificationProcessingStarted, onPushNotificationProcessingCompleted, onAppStateChanged, enableAutoReconnect = true, skipWebBackgroundDetection = true, debug = false, } = options;
480
+ // Initialize push notification handling for Android
481
+ if (react_native_1.Platform.OS === 'android') {
482
+ // TODO: Initialize Firebase or other push notification service
483
+ if (debug) {
484
+ console.log('[TelnyxVoiceApp] Android push notification initialization needed');
485
+ }
486
+ }
487
+ // Register background message handler if provided
488
+ if (backgroundMessageHandler) {
489
+ // TODO: Register the background message handler with the push notification service
490
+ if (debug) {
491
+ console.log('[TelnyxVoiceApp] Background message handler registration needed');
492
+ }
494
493
  }
495
- }
496
- // Register background message handler if provided
497
- if (backgroundMessageHandler) {
498
- // TODO: Register the background message handler with the push notification service
499
494
  if (debug) {
500
- console.log('[TelnyxVoiceApp] Background message handler registration needed');
495
+ console.log('[TelnyxVoiceApp] SDK initialization complete');
501
496
  }
502
- }
503
- if (debug) {
504
- console.log('[TelnyxVoiceApp] SDK initialization complete');
505
- }
506
- // Return a component that renders TelnyxVoiceApp with the provided options
507
- return () =>
508
- (0, jsx_runtime_1.jsx)(exports.TelnyxVoiceApp, {
509
- voipClient: voipClient,
510
- onPushNotificationProcessingStarted: onPushNotificationProcessingStarted,
511
- onPushNotificationProcessingCompleted: onPushNotificationProcessingCompleted,
512
- onAppStateChanged: onAppStateChanged,
513
- enableAutoReconnect: enableAutoReconnect,
514
- skipWebBackgroundDetection: skipWebBackgroundDetection,
515
- debug: debug,
516
- children: children,
517
- });
497
+ // Return a component that renders TelnyxVoiceApp with the provided options
498
+ return () => ((0, jsx_runtime_1.jsx)(exports.TelnyxVoiceApp, { voipClient: voipClient, onPushNotificationProcessingStarted: onPushNotificationProcessingStarted, onPushNotificationProcessingCompleted: onPushNotificationProcessingCompleted, onAppStateChanged: onAppStateChanged, enableAutoReconnect: enableAutoReconnect, skipWebBackgroundDetection: skipWebBackgroundDetection, debug: debug, children: children }));
518
499
  };
519
500
  /**
520
501
  * Handles background push notifications in the background isolate.
521
502
  * This should be called from your background message handler.
522
503
  */
523
504
  const handleBackgroundPush = async (message) => {
524
- console.log('[TelnyxVoiceApp] Background push received:', message);
525
- try {
526
- // TODO: Initialize push notification service in isolate if needed
527
- // Use singleton pattern for background client to prevent multiple instances
528
- let backgroundClient = (0, telnyx_voip_client_1.createBackgroundTelnyxVoipClient)({
529
- debug: true,
530
- });
531
- await backgroundClient.handlePushNotification(message);
532
- console.log('[TelnyxVoiceApp] Background push processed successfully');
533
- // Clean up the background client
534
- backgroundClient.dispose();
535
- } catch (e) {
536
- console.log('[TelnyxVoiceApp] Error processing background push:', e);
537
- }
505
+ console.log('[TelnyxVoiceApp] Background push received:', message);
506
+ try {
507
+ // TODO: Initialize push notification service in isolate if needed
508
+ // Use singleton pattern for background client to prevent multiple instances
509
+ let backgroundClient = (0, telnyx_voip_client_1.createBackgroundTelnyxVoipClient)({
510
+ debug: true,
511
+ });
512
+ await backgroundClient.handlePushNotification(message);
513
+ console.log('[TelnyxVoiceApp] Background push processed successfully');
514
+ // Clean up the background client
515
+ backgroundClient.dispose();
516
+ }
517
+ catch (e) {
518
+ console.log('[TelnyxVoiceApp] Error processing background push:', e);
519
+ }
538
520
  };
539
521
  // Create the component with static methods
540
522
  exports.TelnyxVoiceApp = Object.assign(TelnyxVoiceAppComponent, {
541
- initializeAndCreate,
542
- handleBackgroundPush,
523
+ initializeAndCreate,
524
+ handleBackgroundPush,
543
525
  });
544
526
  exports.default = exports.TelnyxVoiceApp;