@telnyx/react-voice-commons-sdk 0.1.7 → 0.1.8-beta.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +35 -5
- package/lib/callkit/callkit-coordinator.js +31 -1
- package/lib/telnyx-voice-app.js +17 -5
- package/package.json +1 -1
- package/src/callkit/callkit-coordinator.ts +34 -1
- package/src/telnyx-voice-app.tsx +16 -5
package/README.md
CHANGED
|
@@ -32,15 +32,30 @@ The `@telnyx/react-voice-commons-sdk` library provides:
|
|
|
32
32
|
Integrate the library using the `TelnyxVoiceApp` component for automatic lifecycle management:
|
|
33
33
|
|
|
34
34
|
```tsx
|
|
35
|
-
import {
|
|
35
|
+
import {
|
|
36
|
+
TelnyxVoiceApp,
|
|
37
|
+
TelnyxVoipClient,
|
|
38
|
+
createTelnyxVoipClient,
|
|
39
|
+
} from '@telnyx/react-voice-commons-sdk';
|
|
36
40
|
|
|
37
|
-
// Create the VoIP client instance
|
|
41
|
+
// Create the VoIP client instance (singleton — safe to call inside a component body)
|
|
38
42
|
const voipClient = createTelnyxVoipClient({
|
|
39
43
|
enableAppStateManagement: true, // Optional: Enable automatic app state management (default: true)
|
|
40
44
|
debug: true, // Optional: Enable debug logging
|
|
41
45
|
});
|
|
42
46
|
|
|
43
47
|
export default function App() {
|
|
48
|
+
// Skip auto-login if the app was launched from a push notification —
|
|
49
|
+
// the SDK handles login internally via the push notification flow.
|
|
50
|
+
React.useEffect(() => {
|
|
51
|
+
TelnyxVoipClient.isLaunchedFromPushNotification().then((isFromPush) => {
|
|
52
|
+
if (!isFromPush) {
|
|
53
|
+
// Safe to auto-login
|
|
54
|
+
voipClient.loginFromStoredConfig();
|
|
55
|
+
}
|
|
56
|
+
});
|
|
57
|
+
}, []);
|
|
58
|
+
|
|
44
59
|
return (
|
|
45
60
|
<TelnyxVoiceApp voipClient={voipClient} enableAutoReconnect={false} debug={true}>
|
|
46
61
|
<YourAppContent />
|
|
@@ -54,10 +69,16 @@ export default function App() {
|
|
|
54
69
|
### 1. VoIP Client Configuration
|
|
55
70
|
|
|
56
71
|
```tsx
|
|
72
|
+
// createTelnyxVoipClient is a singleton — repeated calls return the same instance.
|
|
73
|
+
// This makes it safe to call inside a React component body without re-creating on every render.
|
|
57
74
|
const voipClient = createTelnyxVoipClient({
|
|
58
75
|
enableAppStateManagement: true, // Optional: Enable automatic app state management (default: true)
|
|
59
76
|
debug: true, // Optional: Enable debug logging
|
|
60
77
|
});
|
|
78
|
+
|
|
79
|
+
// If you need to tear down and recreate the client (e.g., on logout):
|
|
80
|
+
import { destroyTelnyxVoipClient } from '@telnyx/react-voice-commons-sdk';
|
|
81
|
+
destroyTelnyxVoipClient(); // Disposes the singleton; next createTelnyxVoipClient() call creates a fresh instance
|
|
61
82
|
```
|
|
62
83
|
|
|
63
84
|
**Configuration Options Explained:**
|
|
@@ -73,6 +94,7 @@ The `TelnyxVoiceApp` component handles:
|
|
|
73
94
|
- Push notification processing from terminated state
|
|
74
95
|
- Login state management with automatic reconnection
|
|
75
96
|
- Background client management for push notifications
|
|
97
|
+
- **Automatic CallKit coordinator wiring** — the `voipClient` is set on the CallKit coordinator on mount, so you don't need to call `setVoipClient()` manually
|
|
76
98
|
|
|
77
99
|
### 3. Reactive State Management
|
|
78
100
|
|
|
@@ -416,9 +438,18 @@ npx expo run:ios
|
|
|
416
438
|
|
|
417
439
|
### Common Integration Issues
|
|
418
440
|
|
|
419
|
-
### Double Login
|
|
441
|
+
### Double Login on Cold-Start
|
|
442
|
+
|
|
443
|
+
When the app is launched from a push notification, the SDK handles login internally. If your app also auto-logs in on mount, both will race and the push flow breaks. Use `isLaunchedFromPushNotification()` to guard your auto-login:
|
|
444
|
+
|
|
445
|
+
```tsx
|
|
446
|
+
const isFromPush = await TelnyxVoipClient.isLaunchedFromPushNotification();
|
|
447
|
+
if (!isFromPush) {
|
|
448
|
+
voipClient.loginFromStoredConfig();
|
|
449
|
+
}
|
|
450
|
+
```
|
|
420
451
|
|
|
421
|
-
|
|
452
|
+
Also ensure you're not calling login methods manually when using `TelnyxVoiceApp` with auto-reconnection enabled.
|
|
422
453
|
|
|
423
454
|
### Background Disconnection
|
|
424
455
|
|
|
@@ -457,7 +488,6 @@ useEffect(() => {
|
|
|
457
488
|
const subscription = voipClient.connectionState$.subscribe(handleStateChange);
|
|
458
489
|
return () => subscription.unsubscribe();
|
|
459
490
|
}, []);
|
|
460
|
-
}, []);
|
|
461
491
|
```
|
|
462
492
|
|
|
463
493
|
## Documentation
|
|
@@ -303,10 +303,21 @@ class CallKitCoordinator {
|
|
|
303
303
|
} else {
|
|
304
304
|
console.log('CallKitCoordinator: Outgoing call, skipping answer and CONNECTING state');
|
|
305
305
|
}
|
|
306
|
+
// Clear push data now that answer action is fulfilled
|
|
307
|
+
try {
|
|
308
|
+
await voice_pn_bridge_1.VoicePnBridge.clearPendingVoipPush();
|
|
309
|
+
console.log('CallKitCoordinator: Cleared pending VoIP push after answer fulfilled');
|
|
310
|
+
} catch (clearErr) {
|
|
311
|
+
console.error('CallKitCoordinator: Error clearing push data after answer:', clearErr);
|
|
312
|
+
}
|
|
306
313
|
} catch (error) {
|
|
307
314
|
console.error('CallKitCoordinator: Error processing CallKit answer', error);
|
|
308
315
|
await callkit_1.default.reportCallEnded(callKitUUID, callkit_1.CallEndReason.Failed);
|
|
309
316
|
this.cleanupCall(callKitUUID);
|
|
317
|
+
// Clear push data even on error to prevent stale state
|
|
318
|
+
try {
|
|
319
|
+
await voice_pn_bridge_1.VoicePnBridge.clearPendingVoipPush();
|
|
320
|
+
} catch (_) {}
|
|
310
321
|
} finally {
|
|
311
322
|
this.processingCalls.delete(callKitUUID);
|
|
312
323
|
}
|
|
@@ -352,6 +363,11 @@ class CallKitCoordinator {
|
|
|
352
363
|
} finally {
|
|
353
364
|
this.processingCalls.delete(callKitUUID);
|
|
354
365
|
this.cleanupCall(callKitUUID);
|
|
366
|
+
// Clear push data now that end action is fulfilled
|
|
367
|
+
try {
|
|
368
|
+
await voice_pn_bridge_1.VoicePnBridge.clearPendingVoipPush();
|
|
369
|
+
console.log('CallKitCoordinator: Cleared pending VoIP push after end fulfilled');
|
|
370
|
+
} catch (_) {}
|
|
355
371
|
// Check if app is in background and no more calls - disconnect client
|
|
356
372
|
await this.checkBackgroundDisconnection();
|
|
357
373
|
}
|
|
@@ -506,6 +522,11 @@ class CallKitCoordinator {
|
|
|
506
522
|
// Set the pending push action to be handled when app comes to foreground
|
|
507
523
|
await voice_pn_bridge_1.VoicePnBridge.setPendingPushAction(pushAction, pushMetadata);
|
|
508
524
|
console.log('CallKitCoordinator: ✅ Set pending push action');
|
|
525
|
+
// Clear push data now that push notification answer is handled
|
|
526
|
+
try {
|
|
527
|
+
await voice_pn_bridge_1.VoicePnBridge.clearPendingVoipPush();
|
|
528
|
+
console.log('CallKitCoordinator: Cleared pending VoIP push after push answer handled');
|
|
529
|
+
} catch (_) {}
|
|
509
530
|
return;
|
|
510
531
|
}
|
|
511
532
|
// For other platforms (shouldn't happen on iOS)
|
|
@@ -533,6 +554,11 @@ class CallKitCoordinator {
|
|
|
533
554
|
this.voipClient.queueEndFromCallKit();
|
|
534
555
|
// Clean up push notification state
|
|
535
556
|
await this.cleanupPushNotificationState();
|
|
557
|
+
// Clear push data now that rejection is handled
|
|
558
|
+
try {
|
|
559
|
+
await voice_pn_bridge_1.VoicePnBridge.clearPendingVoipPush();
|
|
560
|
+
console.log('CallKitCoordinator: Cleared pending VoIP push after rejection handled');
|
|
561
|
+
} catch (_) {}
|
|
536
562
|
console.log('CallKitCoordinator: 🎯 Push notification rejection handling complete');
|
|
537
563
|
return;
|
|
538
564
|
}
|
|
@@ -738,7 +764,11 @@ class CallKitCoordinator {
|
|
|
738
764
|
* This helps prevent premature flag resets during CallKit operations
|
|
739
765
|
*/
|
|
740
766
|
hasProcessingCalls() {
|
|
741
|
-
return this
|
|
767
|
+
// Also return true when isCallFromPush is set — this prevents the
|
|
768
|
+
// calls$ subscription in TelnyxVoiceApp from resetting protection flags
|
|
769
|
+
// (isHandlingForegroundCall, backgroundDetectorIgnore) before the WebRTC
|
|
770
|
+
// call arrives during push notification handling.
|
|
771
|
+
return this.processingCalls.size > 0 || this.isCallFromPush;
|
|
742
772
|
}
|
|
743
773
|
/**
|
|
744
774
|
* Check if there's currently a call from push notification being processed
|
package/lib/telnyx-voice-app.js
CHANGED
|
@@ -274,8 +274,14 @@ const TelnyxVoiceAppComponent = ({
|
|
|
274
274
|
return null;
|
|
275
275
|
}
|
|
276
276
|
log('Found pending VoIP push data:', voipPayload);
|
|
277
|
-
|
|
278
|
-
|
|
277
|
+
// Do NOT clear push data here. Let it persist until the answer/end action
|
|
278
|
+
// is fulfilled in the CallKit coordinator. This prevents a race condition in
|
|
279
|
+
// Expo apps where the RN bridge mounts immediately on push notification —
|
|
280
|
+
// the push data would be consumed and cleared before the user answers,
|
|
281
|
+
// leaving the coordinator with nothing to work with.
|
|
282
|
+
// For non-Expo apps (RN mounts after answer), the coordinator's
|
|
283
|
+
// handlePushNotificationAnswer/Reject clears the data before
|
|
284
|
+
// checkForInitialPushNotification ever runs, so no loop occurs.
|
|
279
285
|
return { action: 'incoming_call', metadata: voipPayload.metadata, from_notification: true };
|
|
280
286
|
} catch (parseError) {
|
|
281
287
|
log('Error parsing VoIP push JSON:', parseError);
|
|
@@ -315,11 +321,17 @@ const TelnyxVoiceAppComponent = ({
|
|
|
315
321
|
return;
|
|
316
322
|
}
|
|
317
323
|
log('Processing initial push notification...');
|
|
318
|
-
// Prevent duplicate processing if already connected
|
|
324
|
+
// Prevent duplicate processing if already connected or connecting.
|
|
325
|
+
// Since push data is no longer cleared on read, this guard prevents
|
|
326
|
+
// re-processing when checkForInitialPushNotification fires again on app resume.
|
|
319
327
|
if (
|
|
320
|
-
voipClient.currentConnectionState ===
|
|
328
|
+
voipClient.currentConnectionState ===
|
|
329
|
+
connection_state_1.TelnyxConnectionState.CONNECTED ||
|
|
330
|
+
voipClient.currentConnectionState === connection_state_1.TelnyxConnectionState.CONNECTING
|
|
321
331
|
) {
|
|
322
|
-
log(
|
|
332
|
+
log(
|
|
333
|
+
`SKIPPING - Already ${voipClient.currentConnectionState}, preventing duplicate processing`
|
|
334
|
+
);
|
|
323
335
|
return;
|
|
324
336
|
}
|
|
325
337
|
// Set flags to prevent auto-reconnection during push call
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@telnyx/react-voice-commons-sdk",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.8-beta.0",
|
|
4
4
|
"description": "A high-level, state-agnostic, drop-in module for the Telnyx React Native SDK that simplifies WebRTC voice calling integration",
|
|
5
5
|
"main": "lib/index.js",
|
|
6
6
|
"module": "lib/index.js",
|
|
@@ -306,10 +306,21 @@ class CallKitCoordinator {
|
|
|
306
306
|
} else {
|
|
307
307
|
console.log('CallKitCoordinator: Outgoing call, skipping answer and CONNECTING state');
|
|
308
308
|
}
|
|
309
|
+
// Clear push data now that answer action is fulfilled
|
|
310
|
+
try {
|
|
311
|
+
await VoicePnBridge.clearPendingVoipPush();
|
|
312
|
+
console.log('CallKitCoordinator: Cleared pending VoIP push after answer fulfilled');
|
|
313
|
+
} catch (clearErr) {
|
|
314
|
+
console.error('CallKitCoordinator: Error clearing push data after answer:', clearErr);
|
|
315
|
+
}
|
|
309
316
|
} catch (error) {
|
|
310
317
|
console.error('CallKitCoordinator: Error processing CallKit answer', error);
|
|
311
318
|
await CallKit.reportCallEnded(callKitUUID, CallEndReason.Failed);
|
|
312
319
|
this.cleanupCall(callKitUUID);
|
|
320
|
+
// Clear push data even on error to prevent stale state
|
|
321
|
+
try {
|
|
322
|
+
await VoicePnBridge.clearPendingVoipPush();
|
|
323
|
+
} catch (_) {}
|
|
313
324
|
} finally {
|
|
314
325
|
this.processingCalls.delete(callKitUUID);
|
|
315
326
|
}
|
|
@@ -366,6 +377,12 @@ class CallKitCoordinator {
|
|
|
366
377
|
this.processingCalls.delete(callKitUUID);
|
|
367
378
|
this.cleanupCall(callKitUUID);
|
|
368
379
|
|
|
380
|
+
// Clear push data now that end action is fulfilled
|
|
381
|
+
try {
|
|
382
|
+
await VoicePnBridge.clearPendingVoipPush();
|
|
383
|
+
console.log('CallKitCoordinator: Cleared pending VoIP push after end fulfilled');
|
|
384
|
+
} catch (_) {}
|
|
385
|
+
|
|
369
386
|
// Check if app is in background and no more calls - disconnect client
|
|
370
387
|
await this.checkBackgroundDisconnection();
|
|
371
388
|
}
|
|
@@ -544,6 +561,12 @@ class CallKitCoordinator {
|
|
|
544
561
|
await VoicePnBridge.setPendingPushAction(pushAction, pushMetadata);
|
|
545
562
|
console.log('CallKitCoordinator: ✅ Set pending push action');
|
|
546
563
|
|
|
564
|
+
// Clear push data now that push notification answer is handled
|
|
565
|
+
try {
|
|
566
|
+
await VoicePnBridge.clearPendingVoipPush();
|
|
567
|
+
console.log('CallKitCoordinator: Cleared pending VoIP push after push answer handled');
|
|
568
|
+
} catch (_) {}
|
|
569
|
+
|
|
547
570
|
return;
|
|
548
571
|
}
|
|
549
572
|
|
|
@@ -577,6 +600,12 @@ class CallKitCoordinator {
|
|
|
577
600
|
// Clean up push notification state
|
|
578
601
|
await this.cleanupPushNotificationState();
|
|
579
602
|
|
|
603
|
+
// Clear push data now that rejection is handled
|
|
604
|
+
try {
|
|
605
|
+
await VoicePnBridge.clearPendingVoipPush();
|
|
606
|
+
console.log('CallKitCoordinator: Cleared pending VoIP push after rejection handled');
|
|
607
|
+
} catch (_) {}
|
|
608
|
+
|
|
580
609
|
console.log('CallKitCoordinator: 🎯 Push notification rejection handling complete');
|
|
581
610
|
return;
|
|
582
611
|
}
|
|
@@ -815,7 +844,11 @@ class CallKitCoordinator {
|
|
|
815
844
|
* This helps prevent premature flag resets during CallKit operations
|
|
816
845
|
*/
|
|
817
846
|
hasProcessingCalls(): boolean {
|
|
818
|
-
return this
|
|
847
|
+
// Also return true when isCallFromPush is set — this prevents the
|
|
848
|
+
// calls$ subscription in TelnyxVoiceApp from resetting protection flags
|
|
849
|
+
// (isHandlingForegroundCall, backgroundDetectorIgnore) before the WebRTC
|
|
850
|
+
// call arrives during push notification handling.
|
|
851
|
+
return this.processingCalls.size > 0 || this.isCallFromPush;
|
|
819
852
|
}
|
|
820
853
|
|
|
821
854
|
/**
|
package/src/telnyx-voice-app.tsx
CHANGED
|
@@ -374,8 +374,14 @@ const TelnyxVoiceAppComponent: React.FC<TelnyxVoiceAppProps> = ({
|
|
|
374
374
|
|
|
375
375
|
log('Found pending VoIP push data:', voipPayload);
|
|
376
376
|
|
|
377
|
-
|
|
378
|
-
|
|
377
|
+
// Do NOT clear push data here. Let it persist until the answer/end action
|
|
378
|
+
// is fulfilled in the CallKit coordinator. This prevents a race condition in
|
|
379
|
+
// Expo apps where the RN bridge mounts immediately on push notification —
|
|
380
|
+
// the push data would be consumed and cleared before the user answers,
|
|
381
|
+
// leaving the coordinator with nothing to work with.
|
|
382
|
+
// For non-Expo apps (RN mounts after answer), the coordinator's
|
|
383
|
+
// handlePushNotificationAnswer/Reject clears the data before
|
|
384
|
+
// checkForInitialPushNotification ever runs, so no loop occurs.
|
|
379
385
|
|
|
380
386
|
return { action: 'incoming_call', metadata: voipPayload.metadata, from_notification: true };
|
|
381
387
|
} catch (parseError) {
|
|
@@ -424,9 +430,14 @@ const TelnyxVoiceAppComponent: React.FC<TelnyxVoiceAppProps> = ({
|
|
|
424
430
|
|
|
425
431
|
log('Processing initial push notification...');
|
|
426
432
|
|
|
427
|
-
// Prevent duplicate processing if already connected
|
|
428
|
-
|
|
429
|
-
|
|
433
|
+
// Prevent duplicate processing if already connected or connecting.
|
|
434
|
+
// Since push data is no longer cleared on read, this guard prevents
|
|
435
|
+
// re-processing when checkForInitialPushNotification fires again on app resume.
|
|
436
|
+
if (
|
|
437
|
+
voipClient.currentConnectionState === TelnyxConnectionState.CONNECTED ||
|
|
438
|
+
voipClient.currentConnectionState === TelnyxConnectionState.CONNECTING
|
|
439
|
+
) {
|
|
440
|
+
log(`SKIPPING - Already ${voipClient.currentConnectionState}, preventing duplicate processing`);
|
|
430
441
|
return;
|
|
431
442
|
}
|
|
432
443
|
|