@telnyx/react-voice-commons-sdk 0.1.3 → 0.1.4

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.
@@ -1,737 +1,788 @@
1
- import React, { useEffect, useRef, useState, useCallback } from 'react';
2
- import { AppState, AppStateStatus, Platform } from 'react-native';
3
- import { TelnyxVoipClient, createBackgroundTelnyxVoipClient } from './telnyx-voip-client';
4
- import { TelnyxConnectionState } from './models/connection-state';
5
- import { Call } from './models/call';
6
- import { TelnyxVoiceProvider } from './context/TelnyxVoiceContext';
7
-
8
- /**
9
- * Configuration options for TelnyxVoiceApp
10
- */
11
- export interface TelnyxVoiceAppOptions {
12
- /** The TelnyxVoipClient instance to manage */
13
- voipClient: TelnyxVoipClient;
14
-
15
- /** Optional callback when push notification processing starts */
16
- onPushNotificationProcessingStarted?: () => void;
17
-
18
- /** Optional callback when push notification processing completes */
19
- onPushNotificationProcessingCompleted?: () => void;
20
-
21
- /** Optional callback for additional background/foreground handling */
22
- onAppStateChanged?: (state: AppStateStatus) => void;
23
-
24
- /** Whether to enable automatic login/reconnection (default: true) */
25
- enableAutoReconnect?: boolean;
26
-
27
- /** Whether to skip web platform for background detection (default: true) */
28
- skipWebBackgroundDetection?: boolean;
29
-
30
- /** Enable debug logging */
31
- debug?: boolean;
32
- }
33
-
34
- /**
35
- * Props for the TelnyxVoiceApp component
36
- */
37
- export interface TelnyxVoiceAppProps extends TelnyxVoiceAppOptions {
38
- /** The child components to render */
39
- children: React.ReactNode;
40
- }
41
-
42
- /**
43
- * Interface for the TelnyxVoiceApp component with static methods
44
- */
45
- export interface TelnyxVoiceAppComponent extends React.FC<TelnyxVoiceAppProps> {
46
- initializeAndCreate: (options: {
47
- voipClient: TelnyxVoipClient;
48
- children: React.ReactNode;
49
- backgroundMessageHandler?: (message: any) => Promise<void>;
50
- onPushNotificationProcessingStarted?: () => void;
51
- onPushNotificationProcessingCompleted?: () => void;
52
- onAppStateChanged?: (state: AppStateStatus) => void;
53
- enableAutoReconnect?: boolean;
54
- skipWebBackgroundDetection?: boolean;
55
- debug?: boolean;
56
- }) => Promise<React.ComponentType>;
57
-
58
- handleBackgroundPush: (message: any) => Promise<void>;
59
- }
60
-
61
- /**
62
- * A comprehensive wrapper component that handles all Telnyx SDK lifecycle management.
63
- *
64
- * This component automatically handles:
65
- * - Push notification initialization from terminated state
66
- * - Background/foreground lifecycle detection and auto-reconnection
67
- * - Login state management with automatic reconnection
68
- * - CallKit integration preparation
69
- *
70
- * Simply wrap your main app component with this to get full Telnyx functionality:
71
- * ```tsx
72
- * <TelnyxVoiceApp voipClient={myVoipClient}>
73
- * <MyApp />
74
- * </TelnyxVoiceApp>
75
- * ```
76
- */
77
- const TelnyxVoiceAppComponent: React.FC<TelnyxVoiceAppProps> = ({
78
- voipClient,
79
- children,
80
- onPushNotificationProcessingStarted,
81
- onPushNotificationProcessingCompleted,
82
- onAppStateChanged,
83
- enableAutoReconnect = true,
84
- skipWebBackgroundDetection = true,
85
- debug = false,
86
- }) => {
87
- // State management
88
- const [processingPushOnLaunch, setProcessingPushOnLaunch] = useState(false);
89
- const [isHandlingForegroundCall, setIsHandlingForegroundCall] = useState(false);
90
- const [currentConnectionState, setCurrentConnectionState] = useState<TelnyxConnectionState>(
91
- voipClient.currentConnectionState
92
- );
93
-
94
- // Refs for tracking state
95
- const appStateRef = useRef<AppStateStatus>(AppState.currentState);
96
- const backgroundDetectorIgnore = useRef(false);
97
-
98
- // Static background client instance for singleton pattern
99
- const backgroundClientRef = useRef<TelnyxVoipClient | null>(null);
100
-
101
- const log = useCallback(
102
- (message: string, ...args: any[]) => {
103
- if (debug) {
104
- console.log(`[TelnyxVoiceApp] ${message}`, ...args);
105
- }
106
- },
107
- [debug]
108
- );
109
-
110
- // Handle app state changes
111
- const handleAppStateChange = useCallback(
112
- async (nextAppState: AppStateStatus) => {
113
- const previousAppState = appStateRef.current;
114
- appStateRef.current = nextAppState;
115
-
116
- log(`App state changed from ${previousAppState} to ${nextAppState}`);
117
- log(`Background detector ignore flag: ${backgroundDetectorIgnore.current}`);
118
- log(`Handling foreground call: ${isHandlingForegroundCall}`);
119
-
120
- // Call optional user callback first
121
- onAppStateChanged?.(nextAppState);
122
-
123
- // Only handle background disconnection when actually transitioning from active to background
124
- // Don't disconnect on background-to-background transitions (e.g., during CallKit operations)
125
- if (
126
- (nextAppState === 'background' || nextAppState === 'inactive') &&
127
- previousAppState === 'active'
128
- ) {
129
- log(
130
- `App transitioned from ${previousAppState} to ${nextAppState} - handling backgrounding`
131
- );
132
- await handleAppBackgrounded();
133
- } else if (nextAppState === 'background' || nextAppState === 'inactive') {
134
- log(
135
- `App state is ${nextAppState} but was already ${previousAppState} - skipping background handling`
136
- );
137
- }
138
-
139
- // Always check for push notifications when app becomes active (regardless of auto-reconnect setting)
140
- if (nextAppState === 'active' && previousAppState !== 'active') {
141
- log('App became active - checking for push notifications');
142
- await checkForInitialPushNotification(true); // Pass true for fromAppResume
143
- }
144
-
145
- // Only handle auto-reconnection if auto-reconnect is enabled
146
- if (enableAutoReconnect && nextAppState === 'active' && previousAppState !== 'active') {
147
- await handleAppResumed();
148
- }
149
- },
150
- [enableAutoReconnect, onAppStateChanged, isHandlingForegroundCall, log]
151
- );
152
-
153
- // Handle app going to background - disconnect like the old implementation
154
- const handleAppBackgrounded = useCallback(async () => {
155
- // Check if we should ignore background detection (e.g., during active calls)
156
- if (backgroundDetectorIgnore.current || isHandlingForegroundCall) {
157
- log(
158
- 'Background detector ignore flag set or handling foreground call - skipping disconnection'
159
- );
160
- return;
161
- }
162
-
163
- // Check if there are any active calls that should prevent disconnection
164
- const activeCalls = voipClient.currentCalls;
165
- const hasOngoingCall =
166
- activeCalls.length > 0 &&
167
- activeCalls.some(
168
- (call) =>
169
- call.currentState === 'ACTIVE' ||
170
- call.currentState === 'HELD' ||
171
- call.currentState === 'RINGING' ||
172
- call.currentState === 'CONNECTING'
173
- );
174
-
175
- // Also check if there's an incoming call from push notification being processed
176
- let isCallFromPush = false;
177
- if (Platform.OS === 'ios') {
178
- try {
179
- const { callKitCoordinator } = require('./callkit/callkit-coordinator');
180
- isCallFromPush = callKitCoordinator.getIsCallFromPush();
181
- } catch (e) {
182
- log('Error checking isCallFromPush:', e);
183
- }
184
- }
185
-
186
- if (hasOngoingCall || isCallFromPush) {
187
- log('Active calls or push call detected - skipping background disconnection', {
188
- callCount: activeCalls.length,
189
- hasOngoingCall,
190
- isCallFromPush,
191
- callStates: activeCalls.map((call) => ({
192
- callId: call.callId,
193
- currentState: call.currentState,
194
- destination: call.destination,
195
- })),
196
- });
197
- return;
198
- }
199
-
200
- log('App backgrounded - disconnecting (matching old BackgroundDetector behavior)');
201
-
202
- try {
203
- // Always disconnect when backgrounded (matches old implementation)
204
- await voipClient.logout();
205
- log('Successfully disconnected on background');
206
- } catch (e) {
207
- log('Error disconnecting on background:', e);
208
- }
209
- }, [voipClient, isHandlingForegroundCall, log]);
210
-
211
- // Handle app resuming from background
212
- const handleAppResumed = useCallback(async () => {
213
- log('App resumed - checking reconnection needs');
214
-
215
- // IMPORTANT: Check for push notifications first when resuming from background
216
- // This handles the case where the user accepted a call while the app was backgrounded
217
- await checkForInitialPushNotification(true); // Pass true for fromAppResume
218
-
219
- // If we're ignoring (e.g., from push call) or handling foreground call, don't auto-reconnect
220
- if (backgroundDetectorIgnore.current || isHandlingForegroundCall) {
221
- log(
222
- 'Background detector ignore flag set or handling foreground call - skipping reconnection'
223
- );
224
- return;
225
- }
226
-
227
- // iOS-specific: If push notification handling just initiated a connection,
228
- // skip auto-reconnection to prevent double login
229
- if (Platform.OS === 'ios') {
230
- // Check if connection state changed after push processing
231
- const connectionStateAfterPush = voipClient.currentConnectionState;
232
- if (
233
- connectionStateAfterPush === TelnyxConnectionState.CONNECTING ||
234
- connectionStateAfterPush === TelnyxConnectionState.CONNECTED
235
- ) {
236
- log(
237
- `iOS: Push handling initiated connection (${connectionStateAfterPush}), skipping auto-reconnection`
238
- );
239
- return;
240
- }
241
- }
242
-
243
- // Check current connection state and reconnect if needed
244
- const currentState = voipClient.currentConnectionState;
245
- log(`Current connection state: ${currentState}`);
246
-
247
- // If we're not connected and have stored credentials, attempt reconnection
248
- if (currentState !== TelnyxConnectionState.CONNECTED) {
249
- await attemptAutoReconnection();
250
- }
251
- }, [voipClient, isHandlingForegroundCall, log]);
252
-
253
- // Attempt to reconnect using stored credentials
254
- const attemptAutoReconnection = useCallback(async () => {
255
- try {
256
- log('Attempting auto-reconnection...');
257
-
258
- // Try to get stored config and reconnect
259
- const success = await voipClient.loginFromStoredConfig();
260
- log(`Auto-reconnection ${success ? 'successful' : 'failed'}`);
261
-
262
- // If auto-reconnection fails, redirect to login screen
263
- if (!success) {
264
- log('Auto-reconnection failed - redirecting to login screen');
265
- }
266
- } catch (e) {
267
- log('Auto-reconnection error:', e);
268
- // On error, also redirect to login
269
- log('Auto-reconnection error - redirecting to login screen');
270
- }
271
- }, [voipClient, log]);
272
-
273
- // Check for initial push notification action when app launches
274
- const checkForInitialPushNotification = useCallback(
275
- async (fromAppResume: boolean = false) => {
276
- log(`checkForInitialPushNotification called${fromAppResume ? ' (from app resume)' : ''}`);
277
-
278
- if (processingPushOnLaunch && !fromAppResume) {
279
- log('Already processing push, returning early');
280
- return;
281
- }
282
-
283
- // Only set the flag if this is not from app resume to allow resume processing
284
- if (!fromAppResume) {
285
- setProcessingPushOnLaunch(true);
286
- }
287
- onPushNotificationProcessingStarted?.();
288
-
289
- try {
290
- let pushData: Record<string, any> | null = null;
291
-
292
- // Try to get push data from the native layer using our VoicePnBridge
293
- try {
294
- // Import the native bridge module dynamically
295
- const { NativeModules } = require('react-native');
296
- const VoicePnBridge = NativeModules.VoicePnBridge;
297
-
298
- if (VoicePnBridge) {
299
- log('Checking for pending push actions via VoicePnBridge');
300
-
301
- // First check for pending call actions (notification button taps like hangup/answer)
302
- const pendingCallAction = await VoicePnBridge.getPendingCallAction();
303
- log('Raw pending call action response:', pendingCallAction);
304
-
305
- if (pendingCallAction && pendingCallAction.action != null) {
306
- log('Found pending call action:', pendingCallAction);
307
-
308
- // Handle call actions directly
309
- if (pendingCallAction.action === 'hangup' && pendingCallAction.callId) {
310
- log(
311
- 'Processing hangup action from notification for call:',
312
- pendingCallAction.callId
313
- );
314
-
315
- // Find and hangup the call
316
- const activeCall = voipClient.currentActiveCall;
317
- if (activeCall && activeCall.callId === pendingCallAction.callId) {
318
- log('Hanging up active call from notification action');
319
- try {
320
- await activeCall.hangup();
321
- log('Call hung up successfully from notification action');
322
- } catch (error) {
323
- log('Error hanging up call from notification action:', error);
324
- }
325
- } else {
326
- log('No matching active call found for hangup action');
327
- }
328
-
329
- // Clear the pending action
330
- await VoicePnBridge.clearPendingCallAction();
331
- return; // Don't process as push data
332
- }
333
- }
334
-
335
- // Then check for regular push notification data
336
- const pendingAction = await VoicePnBridge.getPendingPushAction();
337
- log('Raw pending action response:', pendingAction);
338
-
339
- if (pendingAction && pendingAction.action != null && pendingAction.metadata != null) {
340
- log('Found pending push action:', pendingAction);
341
-
342
- // Parse the metadata if it's a string
343
- let metadata = pendingAction.metadata;
344
- try {
345
- // First try parsing as JSON
346
- metadata = JSON.parse(metadata);
347
- log('Parsed metadata as JSON:', metadata);
348
- } catch (e) {
349
- log('JSON parse failed, trying Android key-value format');
350
- }
351
-
352
- // Create push data structure that matches what the VoIP client expects
353
- pushData = {
354
- action: pendingAction.action,
355
- metadata: metadata,
356
- from_notification: true,
357
- };
358
-
359
- // Clear the pending action so it doesn't get processed again
360
- await VoicePnBridge.clearPendingPushAction();
361
- log('Cleared pending push action after retrieval');
362
- } else {
363
- log('No pending push actions found');
364
- }
365
- } else {
366
- log('VoicePnBridge not available - this is expected on iOS');
367
- }
368
- } catch (bridgeError) {
369
- log('Error accessing VoicePnBridge:', bridgeError);
370
- }
371
-
372
- // Process the push notification if found
373
- if (pushData) {
374
- log('Processing initial push notification...');
375
-
376
- // Check if we're already connected and handling a push - prevent duplicate processing
377
- const isConnected = voipClient.currentConnectionState === TelnyxConnectionState.CONNECTED;
378
- if (isConnected) {
379
- log('SKIPPING - Already connected, preventing duplicate processing');
380
- // Clear the stored data since we're already handling it
381
- // TODO: Implement clearPushMetaData
382
- return;
383
- }
384
-
385
- // Set flags to prevent auto-reconnection during push call
386
- setIsHandlingForegroundCall(true);
387
- backgroundDetectorIgnore.current = true;
388
- log(`Background detector ignore set to: true at ${new Date().toISOString()}`);
389
- log(`Foreground call handling flag set to: true at ${new Date().toISOString()}`);
390
-
391
- // Dispose any existing background client to prevent conflicts
392
- disposeBackgroundClient();
393
-
394
- // Handle the push notification using platform-specific approach
395
- if (Platform.OS === 'ios') {
396
- // On iOS, coordinate with CallKit by notifying the coordinator about the push
397
- const { callKitCoordinator } = require('./callkit/callkit-coordinator');
398
-
399
- // Extract call_id from nested metadata structure to use as CallKit UUID
400
- const callId = pushData.metadata?.metadata?.call_id;
401
- if (callId) {
402
- log('Notifying CallKit coordinator about push notification:', callId);
403
- await callKitCoordinator.handleCallKitPushReceived(callId, {
404
- callData: { source: 'push_notification' },
405
- pushData: pushData,
406
- });
407
- } else {
408
- log('No call_id found in push data, falling back to direct handling');
409
- await voipClient.handlePushNotification(pushData);
410
- }
411
- } else {
412
- // On other platforms, handle push notification directly
413
- await voipClient.handlePushNotification(pushData);
414
- }
415
-
416
- log('Initial push notification processed');
417
- log('Cleared stored push data to prevent duplicate processing');
418
-
419
- // Note: isHandlingForegroundCall will be reset when calls.length becomes 0
420
- // This prevents premature disconnection during CallKit answer flow
421
- } else {
422
- log('No initial push data found');
423
- }
424
- } catch (e) {
425
- log('Error processing initial push notification:', e);
426
- // Reset flags on error
427
- setIsHandlingForegroundCall(false);
428
- } finally {
429
- // Always reset the processing flag - it should not remain stuck
430
- setProcessingPushOnLaunch(false);
431
- onPushNotificationProcessingCompleted?.();
432
- }
433
- },
434
- [
435
- processingPushOnLaunch,
436
- voipClient,
437
- onPushNotificationProcessingStarted,
438
- onPushNotificationProcessingCompleted,
439
- log,
440
- ]
441
- );
442
-
443
- // Dispose background client instance when no longer needed
444
- const disposeBackgroundClient = useCallback(() => {
445
- if (backgroundClientRef.current) {
446
- log('Disposing background client instance');
447
- backgroundClientRef.current.dispose();
448
- backgroundClientRef.current = null;
449
- }
450
- }, [log]);
451
-
452
- // Create background client for push notification handling
453
- const createBackgroundClient = useCallback((): TelnyxVoipClient => {
454
- log('Creating background client instance');
455
-
456
- const backgroundClient = createBackgroundTelnyxVoipClient({
457
- debug,
458
- });
459
-
460
- return backgroundClient;
461
- }, [debug, log]);
462
-
463
- // Setup effect
464
- useEffect(() => {
465
- // Listen to connection state changes
466
- const connectionStateSubscription = voipClient.connectionState$.subscribe((state) => {
467
- setCurrentConnectionState(state);
468
-
469
- // Just log connection changes, let the app handle navigation
470
- log(`Connection state changed to: ${state}`);
471
- });
472
-
473
- // Listen to call changes to reset flags when no active calls
474
- const callsSubscription = voipClient.calls$.subscribe((calls) => {
475
- // Check if we should reset flags - only reset if:
476
- // 1. No active WebRTC calls AND
477
- // 2. No CallKit operations in progress (to prevent disconnection during CallKit answer flow)
478
- const hasActiveWebRTCCalls = calls.length > 0;
479
- let hasCallKitProcessing = false;
480
-
481
- // Check CallKit processing calls only on iOS
482
- if (Platform.OS === 'ios') {
483
- try {
484
- const { callKitCoordinator } = require('./callkit/callkit-coordinator');
485
- hasCallKitProcessing = callKitCoordinator.hasProcessingCalls();
486
- log(`CallKit processing check: hasProcessingCalls=${hasCallKitProcessing}`);
487
- } catch (e) {
488
- log('Error checking CallKit processing calls:', e);
489
- }
490
- }
491
-
492
- log(
493
- `Flag reset check: WebRTC calls=${calls.length}, CallKit processing=${hasCallKitProcessing}, isHandlingForegroundCall=${isHandlingForegroundCall}, backgroundDetectorIgnore=${backgroundDetectorIgnore.current}`
494
- );
495
-
496
- if (
497
- !hasActiveWebRTCCalls &&
498
- !hasCallKitProcessing &&
499
- (isHandlingForegroundCall || backgroundDetectorIgnore.current)
500
- ) {
501
- log(
502
- `No active calls and no CallKit processing - resetting ignore flags at ${new Date().toISOString()}`
503
- );
504
- setIsHandlingForegroundCall(false);
505
- backgroundDetectorIgnore.current = false;
506
- } else if (!hasActiveWebRTCCalls && hasCallKitProcessing) {
507
- log(
508
- `No WebRTC calls but CallKit operations in progress - keeping ignore flags active at ${new Date().toISOString()}`
509
- );
510
- } else if (hasActiveWebRTCCalls) {
511
- log(`WebRTC calls active - keeping ignore flags active at ${new Date().toISOString()}`);
512
- }
513
-
514
- // Also reset processingPushOnLaunch if no calls are active
515
- // This ensures the flag doesn't get stuck after call ends
516
- if (calls.length === 0 && processingPushOnLaunch) {
517
- log('No active calls - resetting processing push flag');
518
- setProcessingPushOnLaunch(false);
519
- }
520
- });
521
-
522
- // Listen for immediate call action events from notification buttons (Android only)
523
- let callActionSubscription: any = null;
524
- if (Platform.OS === 'android') {
525
- try {
526
- const { VoicePnBridge } = require('./internal/voice-pn-bridge');
527
- callActionSubscription = VoicePnBridge.addCallActionListener((event) => {
528
- log(`Received immediate call action: ${event.action} for callId: ${event.callId}`);
529
-
530
- // Handle immediate call actions (mainly for ending active calls from notification)
531
- if (
532
- event.action === 'hangup' ||
533
- event.action === 'endCall' ||
534
- event.action === 'reject'
535
- ) {
536
- log(`Processing immediate end call action for callId: ${event.callId}`);
537
-
538
- // Find the call by ID and end it
539
- const targetCall = voipClient.currentCalls.find((call) => call.callId === event.callId);
540
- if (targetCall) {
541
- log(`Found active call ${event.callId}, ending it immediately`);
542
- targetCall.hangup().catch((error) => {
543
- log(`Error ending call ${event.callId}:`, error);
544
- });
545
- } else {
546
- log(`No active call found with ID ${event.callId}`);
547
- }
548
- }
549
- });
550
-
551
- log('Call action listener registered for immediate notification handling');
552
- } catch (e) {
553
- log('Error setting up call action listener (VoicePnBridge not available):', e);
554
- }
555
- }
556
-
557
- // Add app state listener if not skipping web background detection or not on web
558
- // AND if app state management is enabled in the client options
559
- let appStateSubscription: any = null;
560
- if (
561
- (!skipWebBackgroundDetection || Platform.OS !== 'web') &&
562
- voipClient.options.enableAppStateManagement
563
- ) {
564
- appStateSubscription = AppState.addEventListener('change', handleAppStateChange);
565
- }
566
-
567
- // Handle initial push notification if app was launched from terminated state
568
- // Only check if we're not already processing to prevent infinite loops
569
- const timeoutId = setTimeout(() => {
570
- if (!processingPushOnLaunch) {
571
- checkForInitialPushNotification();
572
- }
573
- }, 100);
574
-
575
- // Cleanup function
576
- return () => {
577
- connectionStateSubscription.unsubscribe();
578
- callsSubscription.unsubscribe();
579
- if (callActionSubscription) {
580
- try {
581
- const { VoicePnBridge } = require('./internal/voice-pn-bridge');
582
- VoicePnBridge.removeCallActionListener(callActionSubscription);
583
- log('Call action listener removed');
584
- } catch (e) {
585
- log('Error removing call action listener:', e);
586
- }
587
- }
588
- if (appStateSubscription) {
589
- appStateSubscription.remove();
590
- }
591
- clearTimeout(timeoutId);
592
- // Clean up background client instance
593
- disposeBackgroundClient();
594
- };
595
- }, [
596
- voipClient,
597
- handleAppStateChange,
598
- disposeBackgroundClient,
599
- skipWebBackgroundDetection,
600
- isHandlingForegroundCall,
601
- log,
602
- ]);
603
-
604
- // Simply return the children wrapped in context provider - all lifecycle management is handled internally
605
- return <TelnyxVoiceProvider voipClient={voipClient}>{children}</TelnyxVoiceProvider>;
606
- };
607
-
608
- /**
609
- * Static factory method that handles all common SDK initialization boilerplate.
610
- *
611
- * This is the recommended way to initialize the Telnyx Voice SDK in your app.
612
- * It ensures that push notifications, background handlers, and other dependencies are
613
- * set up correctly before the app runs.
614
- *
615
- * This method:
616
- * - Initializes push notification handling
617
- * - Registers the background push notification handler
618
- * - Returns a fully configured TelnyxVoiceApp component to be used in your app
619
- *
620
- * ## Usage:
621
- * ```tsx
622
- * const App = TelnyxVoiceApp.initializeAndCreate({
623
- * voipClient: myVoipClient,
624
- * backgroundHandler: backgroundHandler,
625
- * children: <MyApp />,
626
- * });
627
- * ```
628
- */
629
- const initializeAndCreate = async (options: {
630
- /** The TelnyxVoipClient instance that will be managed by this component */
631
- voipClient: TelnyxVoipClient;
632
-
633
- /** The main component of your application */
634
- children: React.ReactNode;
635
-
636
- /** The function that will handle background push notifications */
637
- backgroundMessageHandler?: (message: any) => Promise<void>;
638
-
639
- /** Optional callback when push notification processing starts */
640
- onPushNotificationProcessingStarted?: () => void;
641
-
642
- /** Optional callback when push notification processing completes */
643
- onPushNotificationProcessingCompleted?: () => void;
644
-
645
- /** Optional callback for app state changes */
646
- onAppStateChanged?: (state: AppStateStatus) => void;
647
-
648
- /** Whether to enable automatic reconnection (default: true) */
649
- enableAutoReconnect?: boolean;
650
-
651
- /** Whether to skip web background detection (default: true) */
652
- skipWebBackgroundDetection?: boolean;
653
-
654
- /** Enable debug logging */
655
- debug?: boolean;
656
- }): Promise<React.ComponentType> => {
657
- const {
658
- voipClient,
659
- children,
660
- backgroundMessageHandler,
661
- onPushNotificationProcessingStarted,
662
- onPushNotificationProcessingCompleted,
663
- onAppStateChanged,
664
- enableAutoReconnect = true,
665
- skipWebBackgroundDetection = true,
666
- debug = false,
667
- } = options;
668
-
669
- // Initialize push notification handling for Android
670
- if (Platform.OS === 'android') {
671
- // TODO: Initialize Firebase or other push notification service
672
- if (debug) {
673
- console.log('[TelnyxVoiceApp] Android push notification initialization needed');
674
- }
675
- }
676
-
677
- // Register background message handler if provided
678
- if (backgroundMessageHandler) {
679
- // TODO: Register the background message handler with the push notification service
680
- if (debug) {
681
- console.log('[TelnyxVoiceApp] Background message handler registration needed');
682
- }
683
- }
684
-
685
- if (debug) {
686
- console.log('[TelnyxVoiceApp] SDK initialization complete');
687
- }
688
-
689
- // Return a component that renders TelnyxVoiceApp with the provided options
690
- return () => (
691
- <TelnyxVoiceApp
692
- voipClient={voipClient}
693
- onPushNotificationProcessingStarted={onPushNotificationProcessingStarted}
694
- onPushNotificationProcessingCompleted={onPushNotificationProcessingCompleted}
695
- onAppStateChanged={onAppStateChanged}
696
- enableAutoReconnect={enableAutoReconnect}
697
- skipWebBackgroundDetection={skipWebBackgroundDetection}
698
- debug={debug}
699
- >
700
- {children}
701
- </TelnyxVoiceApp>
702
- );
703
- };
704
-
705
- /**
706
- * Handles background push notifications in the background isolate.
707
- * This should be called from your background message handler.
708
- */
709
- const handleBackgroundPush = async (message: any): Promise<void> => {
710
- console.log('[TelnyxVoiceApp] Background push received:', message);
711
-
712
- try {
713
- // TODO: Initialize push notification service in isolate if needed
714
-
715
- // Use singleton pattern for background client to prevent multiple instances
716
- let backgroundClient = createBackgroundTelnyxVoipClient({
717
- debug: true,
718
- });
719
-
720
- await backgroundClient.handlePushNotification(message);
721
-
722
- console.log('[TelnyxVoiceApp] Background push processed successfully');
723
-
724
- // Clean up the background client
725
- backgroundClient.dispose();
726
- } catch (e) {
727
- console.log('[TelnyxVoiceApp] Error processing background push:', e);
728
- }
729
- };
730
-
731
- // Create the component with static methods
732
- export const TelnyxVoiceApp: TelnyxVoiceAppComponent = Object.assign(TelnyxVoiceAppComponent, {
733
- initializeAndCreate,
734
- handleBackgroundPush,
735
- });
736
-
737
- export default TelnyxVoiceApp;
1
+ import React, { useEffect, useRef, useState, useCallback } from 'react';
2
+ import { AppState, AppStateStatus, Platform } from 'react-native';
3
+ import { TelnyxVoipClient, createBackgroundTelnyxVoipClient } from './telnyx-voip-client';
4
+ import { TelnyxConnectionState } from './models/connection-state';
5
+ import { Call } from './models/call';
6
+ import { TelnyxVoiceProvider } from './context/TelnyxVoiceContext';
7
+
8
+ /**
9
+ * Configuration options for TelnyxVoiceApp
10
+ */
11
+ export interface TelnyxVoiceAppOptions {
12
+ /** The TelnyxVoipClient instance to manage */
13
+ voipClient: TelnyxVoipClient;
14
+
15
+ /** Optional callback when push notification processing starts */
16
+ onPushNotificationProcessingStarted?: () => void;
17
+
18
+ /** Optional callback when push notification processing completes */
19
+ onPushNotificationProcessingCompleted?: () => void;
20
+
21
+ /** Optional callback for additional background/foreground handling */
22
+ onAppStateChanged?: (state: AppStateStatus) => void;
23
+
24
+ /** Whether to enable automatic login/reconnection (default: true) */
25
+ enableAutoReconnect?: boolean;
26
+
27
+ /** Whether to skip web platform for background detection (default: true) */
28
+ skipWebBackgroundDetection?: boolean;
29
+
30
+ /** Enable debug logging */
31
+ debug?: boolean;
32
+ }
33
+
34
+ /**
35
+ * Props for the TelnyxVoiceApp component
36
+ */
37
+ export interface TelnyxVoiceAppProps extends TelnyxVoiceAppOptions {
38
+ /** The child components to render */
39
+ children: React.ReactNode;
40
+ }
41
+
42
+ /**
43
+ * Interface for the TelnyxVoiceApp component with static methods
44
+ */
45
+ export interface TelnyxVoiceAppComponent extends React.FC<TelnyxVoiceAppProps> {
46
+ initializeAndCreate: (options: {
47
+ voipClient: TelnyxVoipClient;
48
+ children: React.ReactNode;
49
+ backgroundMessageHandler?: (message: any) => Promise<void>;
50
+ onPushNotificationProcessingStarted?: () => void;
51
+ onPushNotificationProcessingCompleted?: () => void;
52
+ onAppStateChanged?: (state: AppStateStatus) => void;
53
+ enableAutoReconnect?: boolean;
54
+ skipWebBackgroundDetection?: boolean;
55
+ debug?: boolean;
56
+ }) => Promise<React.ComponentType>;
57
+
58
+ handleBackgroundPush: (message: any) => Promise<void>;
59
+ }
60
+
61
+ /**
62
+ * A comprehensive wrapper component that handles all Telnyx SDK lifecycle management.
63
+ *
64
+ * This component automatically handles:
65
+ * - Push notification initialization from terminated state
66
+ * - Background/foreground lifecycle detection and auto-reconnection
67
+ * - Login state management with automatic reconnection
68
+ * - CallKit integration preparation
69
+ *
70
+ * Simply wrap your main app component with this to get full Telnyx functionality:
71
+ * ```tsx
72
+ * <TelnyxVoiceApp voipClient={myVoipClient}>
73
+ * <MyApp />
74
+ * </TelnyxVoiceApp>
75
+ * ```
76
+ */
77
+ const TelnyxVoiceAppComponent: React.FC<TelnyxVoiceAppProps> = ({
78
+ voipClient,
79
+ children,
80
+ onPushNotificationProcessingStarted,
81
+ onPushNotificationProcessingCompleted,
82
+ onAppStateChanged,
83
+ enableAutoReconnect = true,
84
+ skipWebBackgroundDetection = true,
85
+ debug = false,
86
+ }) => {
87
+ // State management
88
+ const [processingPushOnLaunch, setProcessingPushOnLaunch] = useState(false);
89
+ const [isHandlingForegroundCall, setIsHandlingForegroundCall] = useState(false);
90
+ const [currentConnectionState, setCurrentConnectionState] = useState<TelnyxConnectionState>(
91
+ voipClient.currentConnectionState
92
+ );
93
+
94
+ // Refs for tracking state
95
+ const appStateRef = useRef<AppStateStatus>(AppState.currentState);
96
+ const backgroundDetectorIgnore = useRef(false);
97
+
98
+ // Static background client instance for singleton pattern
99
+ const backgroundClientRef = useRef<TelnyxVoipClient | null>(null);
100
+
101
+ const log = useCallback(
102
+ (message: string, ...args: any[]) => {
103
+ if (debug) {
104
+ console.log(`[TelnyxVoiceApp] ${message}`, ...args);
105
+ }
106
+ },
107
+ [debug]
108
+ );
109
+
110
+ // Handle app state changes
111
+ const handleAppStateChange = useCallback(
112
+ async (nextAppState: AppStateStatus) => {
113
+ const previousAppState = appStateRef.current;
114
+ appStateRef.current = nextAppState;
115
+
116
+ log(`App state changed from ${previousAppState} to ${nextAppState}`);
117
+ log(`Background detector ignore flag: ${backgroundDetectorIgnore.current}`);
118
+ log(`Handling foreground call: ${isHandlingForegroundCall}`);
119
+
120
+ // Call optional user callback first
121
+ onAppStateChanged?.(nextAppState);
122
+
123
+ // Only handle background disconnection when actually transitioning from active to background
124
+ // Don't disconnect on background-to-background transitions (e.g., during CallKit operations)
125
+ if (
126
+ (nextAppState === 'background' || nextAppState === 'inactive') &&
127
+ previousAppState === 'active'
128
+ ) {
129
+ log(
130
+ `App transitioned from ${previousAppState} to ${nextAppState} - handling backgrounding`
131
+ );
132
+ await handleAppBackgrounded();
133
+ } else if (nextAppState === 'background' || nextAppState === 'inactive') {
134
+ log(
135
+ `App state is ${nextAppState} but was already ${previousAppState} - skipping background handling`
136
+ );
137
+ }
138
+
139
+ // Always check for push notifications when app becomes active (regardless of auto-reconnect setting)
140
+ if (nextAppState === 'active' && previousAppState !== 'active') {
141
+ log('App became active - checking for push notifications');
142
+ await checkForInitialPushNotification(true); // Pass true for fromAppResume
143
+ }
144
+
145
+ // Only handle auto-reconnection if auto-reconnect is enabled
146
+ if (enableAutoReconnect && nextAppState === 'active' && previousAppState !== 'active') {
147
+ await handleAppResumed();
148
+ }
149
+ },
150
+ [enableAutoReconnect, onAppStateChanged, isHandlingForegroundCall, log]
151
+ );
152
+
153
+ // Handle app going to background - disconnect like the old implementation
154
+ const handleAppBackgrounded = useCallback(async () => {
155
+ // Check if we should ignore background detection (e.g., during active calls)
156
+ if (backgroundDetectorIgnore.current || isHandlingForegroundCall) {
157
+ log(
158
+ 'Background detector ignore flag set or handling foreground call - skipping disconnection'
159
+ );
160
+ return;
161
+ }
162
+
163
+ // Check if there are any active calls that should prevent disconnection
164
+ const activeCalls = voipClient.currentCalls;
165
+ const hasOngoingCall =
166
+ activeCalls.length > 0 &&
167
+ activeCalls.some(
168
+ (call) =>
169
+ call.currentState === 'ACTIVE' ||
170
+ call.currentState === 'HELD' ||
171
+ call.currentState === 'RINGING' ||
172
+ call.currentState === 'CONNECTING'
173
+ );
174
+
175
+ // Also check if there's an incoming call from push notification being processed
176
+ let isCallFromPush = false;
177
+ if (Platform.OS === 'ios') {
178
+ try {
179
+ const { callKitCoordinator } = require('./callkit/callkit-coordinator');
180
+ isCallFromPush = callKitCoordinator.getIsCallFromPush();
181
+ } catch (e) {
182
+ log('Error checking isCallFromPush:', e);
183
+ }
184
+ }
185
+
186
+ if (hasOngoingCall || isCallFromPush) {
187
+ log('Active calls or push call detected - skipping background disconnection', {
188
+ callCount: activeCalls.length,
189
+ hasOngoingCall,
190
+ isCallFromPush,
191
+ callStates: activeCalls.map((call) => ({
192
+ callId: call.callId,
193
+ currentState: call.currentState,
194
+ destination: call.destination,
195
+ })),
196
+ });
197
+ return;
198
+ }
199
+
200
+ log('App backgrounded - disconnecting (matching old BackgroundDetector behavior)');
201
+
202
+ try {
203
+ // Always disconnect when backgrounded (matches old implementation)
204
+ await voipClient.logout();
205
+ log('Successfully disconnected on background');
206
+ } catch (e) {
207
+ log('Error disconnecting on background:', e);
208
+ }
209
+ }, [voipClient, isHandlingForegroundCall, log]);
210
+
211
+ // Handle app resuming from background
212
+ const handleAppResumed = useCallback(async () => {
213
+ log('App resumed - checking reconnection needs');
214
+
215
+ // IMPORTANT: Check for push notifications first when resuming from background
216
+ // This handles the case where the user accepted a call while the app was backgrounded
217
+ await checkForInitialPushNotification(true); // Pass true for fromAppResume
218
+
219
+ // If we're ignoring (e.g., from push call) or handling foreground call, don't auto-reconnect
220
+ if (backgroundDetectorIgnore.current || isHandlingForegroundCall) {
221
+ log(
222
+ 'Background detector ignore flag set or handling foreground call - skipping reconnection'
223
+ );
224
+ return;
225
+ }
226
+
227
+ // iOS-specific: If push notification handling just initiated a connection,
228
+ // skip auto-reconnection to prevent double login
229
+ if (Platform.OS === 'ios') {
230
+ // Check if connection state changed after push processing
231
+ const connectionStateAfterPush = voipClient.currentConnectionState;
232
+ if (
233
+ connectionStateAfterPush === TelnyxConnectionState.CONNECTING ||
234
+ connectionStateAfterPush === TelnyxConnectionState.CONNECTED
235
+ ) {
236
+ log(
237
+ `iOS: Push handling initiated connection (${connectionStateAfterPush}), skipping auto-reconnection`
238
+ );
239
+ return;
240
+ }
241
+ }
242
+
243
+ // Check current connection state and reconnect if needed
244
+ const currentState = voipClient.currentConnectionState;
245
+ log(`Current connection state: ${currentState}`);
246
+
247
+ // If we're not connected and have stored credentials, attempt reconnection
248
+ if (currentState !== TelnyxConnectionState.CONNECTED) {
249
+ await attemptAutoReconnection();
250
+ }
251
+ }, [voipClient, isHandlingForegroundCall, log]);
252
+
253
+ // Attempt to reconnect using stored credentials
254
+ const attemptAutoReconnection = useCallback(async () => {
255
+ try {
256
+ log('Attempting auto-reconnection...');
257
+
258
+ // Try to get stored config and reconnect
259
+ const success = await voipClient.loginFromStoredConfig();
260
+ log(`Auto-reconnection ${success ? 'successful' : 'failed'}`);
261
+
262
+ // If auto-reconnection fails, redirect to login screen
263
+ if (!success) {
264
+ log('Auto-reconnection failed - redirecting to login screen');
265
+ }
266
+ } catch (e) {
267
+ log('Auto-reconnection error:', e);
268
+ // On error, also redirect to login
269
+ log('Auto-reconnection error - redirecting to login screen');
270
+ }
271
+ }, [voipClient, log]);
272
+
273
+ // Check for initial push notification action when app launches
274
+ const checkForInitialPushNotification = useCallback(
275
+ async (fromAppResume: boolean = false) => {
276
+ log(`checkForInitialPushNotification called${fromAppResume ? ' (from app resume)' : ''}`);
277
+
278
+ if (processingPushOnLaunch && !fromAppResume) {
279
+ log('Already processing push, returning early');
280
+ return;
281
+ }
282
+
283
+ // Only set the flag if this is not from app resume to allow resume processing
284
+ if (!fromAppResume) {
285
+ setProcessingPushOnLaunch(true);
286
+ }
287
+ onPushNotificationProcessingStarted?.();
288
+
289
+ try {
290
+ let pushData: Record<string, any> | null = null;
291
+
292
+ // Try to get push data from the native layer using platform-specific methods
293
+ if (Platform.OS === 'android') {
294
+ try {
295
+ // Import the native bridge module dynamically
296
+ const { NativeModules } = require('react-native');
297
+ const VoicePnBridge = NativeModules.VoicePnBridge;
298
+
299
+ if (VoicePnBridge) {
300
+ log('Checking for pending push actions via VoicePnBridge');
301
+
302
+ // First check for pending call actions (notification button taps like hangup/answer)
303
+ const pendingCallAction = await VoicePnBridge.getPendingCallAction();
304
+ log('Raw pending call action response:', pendingCallAction);
305
+
306
+ if (pendingCallAction && pendingCallAction.action != null) {
307
+ log('Found pending call action:', pendingCallAction);
308
+
309
+ // Handle call actions directly
310
+ if (pendingCallAction.action === 'hangup' && pendingCallAction.callId) {
311
+ log(
312
+ 'Processing hangup action from notification for call:',
313
+ pendingCallAction.callId
314
+ );
315
+
316
+ // Find and hangup the call
317
+ const activeCall = voipClient.currentActiveCall;
318
+ if (activeCall && activeCall.callId === pendingCallAction.callId) {
319
+ log('Hanging up active call from notification action');
320
+ try {
321
+ await activeCall.hangup();
322
+ log('Call hung up successfully from notification action');
323
+ } catch (error) {
324
+ log('Error hanging up call from notification action:', error);
325
+ }
326
+ } else {
327
+ log('No matching active call found for hangup action');
328
+ }
329
+
330
+ // Clear the pending action
331
+ await VoicePnBridge.clearPendingCallAction();
332
+ return; // Don't process as push data
333
+ }
334
+ }
335
+
336
+ // Then check for regular push notification data
337
+ const pendingAction = await VoicePnBridge.getPendingPushAction();
338
+ log('Raw pending action response:', pendingAction);
339
+
340
+ if (pendingAction && pendingAction.action != null && pendingAction.metadata != null) {
341
+ log('Found pending push action:', pendingAction);
342
+
343
+ // Parse the metadata if it's a string
344
+ let metadata = pendingAction.metadata;
345
+ try {
346
+ // First try parsing as JSON
347
+ metadata = JSON.parse(metadata);
348
+ log('Parsed metadata as JSON:', metadata);
349
+ } catch (e) {
350
+ log('JSON parse failed, trying Android key-value format');
351
+ }
352
+
353
+ // Create push data structure that matches what the VoIP client expects
354
+ pushData = {
355
+ action: pendingAction.action,
356
+ metadata: metadata,
357
+ from_notification: true,
358
+ };
359
+
360
+ // Clear the pending action so it doesn't get processed again
361
+ await VoicePnBridge.clearPendingPushAction();
362
+ log('Cleared pending push action after retrieval');
363
+ } else {
364
+ log('No pending push actions found');
365
+ }
366
+ } else {
367
+ log('VoicePnBridge not available on Android');
368
+ }
369
+ } catch (bridgeError) {
370
+ log('Error accessing VoicePnBridge on Android:', bridgeError);
371
+ }
372
+ } else if (Platform.OS === 'ios') {
373
+ try {
374
+ // Import the native bridge module dynamically (same as Android)
375
+ const { NativeModules } = require('react-native');
376
+ const VoicePnBridge = NativeModules.VoicePnBridge;
377
+
378
+ if (VoicePnBridge) {
379
+ log('Checking for pending VoIP push data via iOS VoicePnBridge');
380
+
381
+ // Check for VoIP push notification data stored by the native push handler
382
+ const pendingVoipPushJson = await VoicePnBridge.getPendingVoipPush();
383
+ log('Raw pending VoIP push response:', pendingVoipPushJson);
384
+
385
+ if (pendingVoipPushJson) {
386
+ try {
387
+ const pendingVoipPush = JSON.parse(pendingVoipPushJson);
388
+ const voipPayload = pendingVoipPush?.payload;
389
+
390
+ if (voipPayload && voipPayload.metadata) {
391
+ log('Found pending VoIP push data:', voipPayload);
392
+
393
+ // Create push data structure that matches what the VoIP client expects
394
+ pushData = {
395
+ action: 'incoming_call',
396
+ metadata: voipPayload.metadata,
397
+ from_notification: true,
398
+ };
399
+
400
+ // Clear the pending VoIP push data so it doesn't get processed again
401
+ await VoicePnBridge.clearPendingVoipPush();
402
+ log('Cleared pending VoIP push data after retrieval');
403
+ } else {
404
+ log('Invalid VoIP push data structure');
405
+ }
406
+ } catch (parseError) {
407
+ log('Error parsing VoIP push JSON:', parseError);
408
+ }
409
+ } else {
410
+ log('No pending VoIP push data found');
411
+ }
412
+ } else {
413
+ log('VoicePnBridge not available on iOS');
414
+ }
415
+ } catch (bridgeError) {
416
+ log('Error accessing VoicePnBridge on iOS:', bridgeError);
417
+ }
418
+ } else {
419
+ log('Push data check skipped - unsupported platform');
420
+ }
421
+
422
+ // Process the push notification if found
423
+ if (pushData) {
424
+ log('Processing initial push notification...');
425
+
426
+ // Check if we're already connected and handling a push - prevent duplicate processing
427
+ const isConnected = voipClient.currentConnectionState === TelnyxConnectionState.CONNECTED;
428
+ if (isConnected) {
429
+ log('SKIPPING - Already connected, preventing duplicate processing');
430
+ // Clear the stored data since we're already handling it
431
+ // TODO: Implement clearPushMetaData
432
+ return;
433
+ }
434
+
435
+ // Set flags to prevent auto-reconnection during push call
436
+ setIsHandlingForegroundCall(true);
437
+ backgroundDetectorIgnore.current = true;
438
+ log(`Background detector ignore set to: true at ${new Date().toISOString()}`);
439
+ log(`Foreground call handling flag set to: true at ${new Date().toISOString()}`);
440
+
441
+ // Dispose any existing background client to prevent conflicts
442
+ disposeBackgroundClient();
443
+
444
+ // Handle the push notification using platform-specific approach
445
+ if (Platform.OS === 'ios') {
446
+ // On iOS, coordinate with CallKit by notifying the coordinator about the push
447
+ const { callKitCoordinator } = require('./callkit/callkit-coordinator');
448
+
449
+ // Extract call_id from nested metadata structure to use as CallKit UUID
450
+ const callId = pushData.metadata?.metadata?.call_id;
451
+ if (callId) {
452
+ log('Notifying CallKit coordinator about push notification:', callId);
453
+ await callKitCoordinator.handleCallKitPushReceived(callId, {
454
+ callData: { source: 'push_notification' },
455
+ pushData: pushData,
456
+ });
457
+ } else {
458
+ log('No call_id found in push data, falling back to direct handling');
459
+ await voipClient.handlePushNotification(pushData);
460
+ }
461
+ } else {
462
+ // On other platforms, handle push notification directly
463
+ await voipClient.handlePushNotification(pushData);
464
+ }
465
+
466
+ log('Initial push notification processed');
467
+ log('Cleared stored push data to prevent duplicate processing');
468
+
469
+ // Note: isHandlingForegroundCall will be reset when calls.length becomes 0
470
+ // This prevents premature disconnection during CallKit answer flow
471
+ }
472
+ else {
473
+ log('No initial push data found');
474
+ }
475
+ } catch (e) {
476
+ log('Error processing initial push notification:', e);
477
+ // Reset flags on error
478
+ setIsHandlingForegroundCall(false);
479
+ } finally {
480
+ // Always reset the processing flag - it should not remain stuck
481
+ setProcessingPushOnLaunch(false);
482
+ onPushNotificationProcessingCompleted?.();
483
+ }
484
+ },
485
+ [
486
+ processingPushOnLaunch,
487
+ voipClient,
488
+ onPushNotificationProcessingStarted,
489
+ onPushNotificationProcessingCompleted,
490
+ log,
491
+ ]
492
+ );
493
+
494
+ // Dispose background client instance when no longer needed
495
+ const disposeBackgroundClient = useCallback(() => {
496
+ if (backgroundClientRef.current) {
497
+ log('Disposing background client instance');
498
+ backgroundClientRef.current.dispose();
499
+ backgroundClientRef.current = null;
500
+ }
501
+ }, [log]);
502
+
503
+ // Create background client for push notification handling
504
+ const createBackgroundClient = useCallback((): TelnyxVoipClient => {
505
+ log('Creating background client instance');
506
+
507
+ const backgroundClient = createBackgroundTelnyxVoipClient({
508
+ debug,
509
+ });
510
+
511
+ return backgroundClient;
512
+ }, [debug, log]);
513
+
514
+ // Setup effect
515
+ useEffect(() => {
516
+ // Listen to connection state changes
517
+ const connectionStateSubscription = voipClient.connectionState$.subscribe((state) => {
518
+ setCurrentConnectionState(state);
519
+
520
+ // Just log connection changes, let the app handle navigation
521
+ log(`Connection state changed to: ${state}`);
522
+ });
523
+
524
+ // Listen to call changes to reset flags when no active calls
525
+ const callsSubscription = voipClient.calls$.subscribe((calls) => {
526
+ // Check if we should reset flags - only reset if:
527
+ // 1. No active WebRTC calls AND
528
+ // 2. No CallKit operations in progress (to prevent disconnection during CallKit answer flow)
529
+ const hasActiveWebRTCCalls = calls.length > 0;
530
+ let hasCallKitProcessing = false;
531
+
532
+ // Check CallKit processing calls only on iOS
533
+ if (Platform.OS === 'ios') {
534
+ try {
535
+ const { callKitCoordinator } = require('./callkit/callkit-coordinator');
536
+ hasCallKitProcessing = callKitCoordinator.hasProcessingCalls();
537
+ log(`CallKit processing check: hasProcessingCalls=${hasCallKitProcessing}`);
538
+ } catch (e) {
539
+ log('Error checking CallKit processing calls:', e);
540
+ }
541
+ }
542
+
543
+ log(
544
+ `Flag reset check: WebRTC calls=${calls.length}, CallKit processing=${hasCallKitProcessing}, isHandlingForegroundCall=${isHandlingForegroundCall}, backgroundDetectorIgnore=${backgroundDetectorIgnore.current}`
545
+ );
546
+
547
+ if (
548
+ !hasActiveWebRTCCalls &&
549
+ !hasCallKitProcessing &&
550
+ (isHandlingForegroundCall || backgroundDetectorIgnore.current)
551
+ ) {
552
+ log(
553
+ `No active calls and no CallKit processing - resetting ignore flags at ${new Date().toISOString()}`
554
+ );
555
+ setIsHandlingForegroundCall(false);
556
+ backgroundDetectorIgnore.current = false;
557
+ } else if (!hasActiveWebRTCCalls && hasCallKitProcessing) {
558
+ log(
559
+ `No WebRTC calls but CallKit operations in progress - keeping ignore flags active at ${new Date().toISOString()}`
560
+ );
561
+ } else if (hasActiveWebRTCCalls) {
562
+ log(`WebRTC calls active - keeping ignore flags active at ${new Date().toISOString()}`);
563
+ }
564
+
565
+ // Also reset processingPushOnLaunch if no calls are active
566
+ // This ensures the flag doesn't get stuck after call ends
567
+ if (calls.length === 0 && processingPushOnLaunch) {
568
+ log('No active calls - resetting processing push flag');
569
+ setProcessingPushOnLaunch(false);
570
+ }
571
+ });
572
+
573
+ // Listen for immediate call action events from notification buttons (Android only)
574
+ let callActionSubscription: any = null;
575
+ if (Platform.OS === 'android') {
576
+ try {
577
+ const { VoicePnBridge } = require('./internal/voice-pn-bridge');
578
+ callActionSubscription = VoicePnBridge.addCallActionListener((event) => {
579
+ log(`Received immediate call action: ${event.action} for callId: ${event.callId}`);
580
+
581
+ // Handle immediate call actions (mainly for ending active calls from notification)
582
+ if (
583
+ event.action === 'hangup' ||
584
+ event.action === 'endCall' ||
585
+ event.action === 'reject'
586
+ ) {
587
+ log(`Processing immediate end call action for callId: ${event.callId}`);
588
+
589
+ // Find the call by ID and end it
590
+ const targetCall = voipClient.currentCalls.find((call) => call.callId === event.callId);
591
+ if (targetCall) {
592
+ log(`Found active call ${event.callId}, ending it immediately`);
593
+ targetCall.hangup().catch((error) => {
594
+ log(`Error ending call ${event.callId}:`, error);
595
+ });
596
+ } else {
597
+ log(`No active call found with ID ${event.callId}`);
598
+ }
599
+ }
600
+ });
601
+
602
+ log('Call action listener registered for immediate notification handling');
603
+ } catch (e) {
604
+ log('Error setting up call action listener (VoicePnBridge not available):', e);
605
+ }
606
+ }
607
+
608
+ // Add app state listener if not skipping web background detection or not on web
609
+ // AND if app state management is enabled in the client options
610
+ let appStateSubscription: any = null;
611
+ if (
612
+ (!skipWebBackgroundDetection || Platform.OS !== 'web') &&
613
+ voipClient.options.enableAppStateManagement
614
+ ) {
615
+ appStateSubscription = AppState.addEventListener('change', handleAppStateChange);
616
+ }
617
+
618
+ // Handle initial push notification if app was launched from terminated state
619
+ // Only check if we're not already processing to prevent infinite loops
620
+ const timeoutId = setTimeout(() => {
621
+ if (!processingPushOnLaunch) {
622
+ checkForInitialPushNotification();
623
+ }
624
+ }, 100);
625
+
626
+ // Cleanup function
627
+ return () => {
628
+ connectionStateSubscription.unsubscribe();
629
+ callsSubscription.unsubscribe();
630
+ if (callActionSubscription) {
631
+ try {
632
+ const { VoicePnBridge } = require('./internal/voice-pn-bridge');
633
+ VoicePnBridge.removeCallActionListener(callActionSubscription);
634
+ log('Call action listener removed');
635
+ } catch (e) {
636
+ log('Error removing call action listener:', e);
637
+ }
638
+ }
639
+ if (appStateSubscription) {
640
+ appStateSubscription.remove();
641
+ }
642
+ clearTimeout(timeoutId);
643
+ // Clean up background client instance
644
+ disposeBackgroundClient();
645
+ };
646
+ }, [
647
+ voipClient,
648
+ handleAppStateChange,
649
+ disposeBackgroundClient,
650
+ skipWebBackgroundDetection,
651
+ isHandlingForegroundCall,
652
+ log,
653
+ ]);
654
+
655
+ // Simply return the children wrapped in context provider - all lifecycle management is handled internally
656
+ return <TelnyxVoiceProvider voipClient={voipClient}>{children}</TelnyxVoiceProvider>;
657
+ };
658
+
659
+ /**
660
+ * Static factory method that handles all common SDK initialization boilerplate.
661
+ *
662
+ * This is the recommended way to initialize the Telnyx Voice SDK in your app.
663
+ * It ensures that push notifications, background handlers, and other dependencies are
664
+ * set up correctly before the app runs.
665
+ *
666
+ * This method:
667
+ * - Initializes push notification handling
668
+ * - Registers the background push notification handler
669
+ * - Returns a fully configured TelnyxVoiceApp component to be used in your app
670
+ *
671
+ * ## Usage:
672
+ * ```tsx
673
+ * const App = TelnyxVoiceApp.initializeAndCreate({
674
+ * voipClient: myVoipClient,
675
+ * backgroundHandler: backgroundHandler,
676
+ * children: <MyApp />,
677
+ * });
678
+ * ```
679
+ */
680
+ const initializeAndCreate = async (options: {
681
+ /** The TelnyxVoipClient instance that will be managed by this component */
682
+ voipClient: TelnyxVoipClient;
683
+
684
+ /** The main component of your application */
685
+ children: React.ReactNode;
686
+
687
+ /** The function that will handle background push notifications */
688
+ backgroundMessageHandler?: (message: any) => Promise<void>;
689
+
690
+ /** Optional callback when push notification processing starts */
691
+ onPushNotificationProcessingStarted?: () => void;
692
+
693
+ /** Optional callback when push notification processing completes */
694
+ onPushNotificationProcessingCompleted?: () => void;
695
+
696
+ /** Optional callback for app state changes */
697
+ onAppStateChanged?: (state: AppStateStatus) => void;
698
+
699
+ /** Whether to enable automatic reconnection (default: true) */
700
+ enableAutoReconnect?: boolean;
701
+
702
+ /** Whether to skip web background detection (default: true) */
703
+ skipWebBackgroundDetection?: boolean;
704
+
705
+ /** Enable debug logging */
706
+ debug?: boolean;
707
+ }): Promise<React.ComponentType> => {
708
+ const {
709
+ voipClient,
710
+ children,
711
+ backgroundMessageHandler,
712
+ onPushNotificationProcessingStarted,
713
+ onPushNotificationProcessingCompleted,
714
+ onAppStateChanged,
715
+ enableAutoReconnect = true,
716
+ skipWebBackgroundDetection = true,
717
+ debug = false,
718
+ } = options;
719
+
720
+ // Initialize push notification handling for Android
721
+ if (Platform.OS === 'android') {
722
+ // TODO: Initialize Firebase or other push notification service
723
+ if (debug) {
724
+ console.log('[TelnyxVoiceApp] Android push notification initialization needed');
725
+ }
726
+ }
727
+
728
+ // Register background message handler if provided
729
+ if (backgroundMessageHandler) {
730
+ // TODO: Register the background message handler with the push notification service
731
+ if (debug) {
732
+ console.log('[TelnyxVoiceApp] Background message handler registration needed');
733
+ }
734
+ }
735
+
736
+ if (debug) {
737
+ console.log('[TelnyxVoiceApp] SDK initialization complete');
738
+ }
739
+
740
+ // Return a component that renders TelnyxVoiceApp with the provided options
741
+ return () => (
742
+ <TelnyxVoiceApp
743
+ voipClient={voipClient}
744
+ onPushNotificationProcessingStarted={onPushNotificationProcessingStarted}
745
+ onPushNotificationProcessingCompleted={onPushNotificationProcessingCompleted}
746
+ onAppStateChanged={onAppStateChanged}
747
+ enableAutoReconnect={enableAutoReconnect}
748
+ skipWebBackgroundDetection={skipWebBackgroundDetection}
749
+ debug={debug}
750
+ >
751
+ {children}
752
+ </TelnyxVoiceApp>
753
+ );
754
+ };
755
+
756
+ /**
757
+ * Handles background push notifications in the background isolate.
758
+ * This should be called from your background message handler.
759
+ */
760
+ const handleBackgroundPush = async (message: any): Promise<void> => {
761
+ console.log('[TelnyxVoiceApp] Background push received:', message);
762
+
763
+ try {
764
+ // TODO: Initialize push notification service in isolate if needed
765
+
766
+ // Use singleton pattern for background client to prevent multiple instances
767
+ let backgroundClient = createBackgroundTelnyxVoipClient({
768
+ debug: true,
769
+ });
770
+
771
+ await backgroundClient.handlePushNotification(message);
772
+
773
+ console.log('[TelnyxVoiceApp] Background push processed successfully');
774
+
775
+ // Clean up the background client
776
+ backgroundClient.dispose();
777
+ } catch (e) {
778
+ console.log('[TelnyxVoiceApp] Error processing background push:', e);
779
+ }
780
+ };
781
+
782
+ // Create the component with static methods
783
+ export const TelnyxVoiceApp: TelnyxVoiceAppComponent = Object.assign(TelnyxVoiceAppComponent, {
784
+ initializeAndCreate,
785
+ handleBackgroundPush,
786
+ });
787
+
788
+ export default TelnyxVoiceApp;