A comprehensive demo application showcasing the @telnyx/react-voice-commons-sdk library - a high-level, state-agnostic, drop-in module for the Telnyx React Native SDK that simplifies WebRTC voice calling integration.
This demo app demonstrates how to integrate the @telnyx/react-voice-commons-sdk library to create a fully functional VoIP calling application with native call UI support, push notifications, and background handling.
- TelnyxVoiceApp Integration: Automatic lifecycle management and push notification handling
- Native Call UI: CallKit (iOS) and ConnectionService (Android) integration
- Background Handling: Seamless app state transitions and background call processing
- Push Notifications: Firebase (Android) and APNs (iOS) integration
- Reactive State Management: RxJS-based state streams for real-time UI updates
- Modern UI Components: Built with NativeWind v4 and react-native-reusables
- Dark/Light Mode: Persistent theme support with system navigation bar matching
The @telnyx/react-voice-commons-sdk library provides:
- Headless Operation: Core library that can establish and manage call state programmatically
- Reactive Streams: All state exposed via RxJS observables for easy integration
- Simplified API: Single facade class (
TelnyxVoipClient) that hides underlying complexity - Automatic Lifecycle Management: Background/foreground handling with
TelnyxVoiceAppcomponent - Call State Management: Central state machine for managing multiple calls
- Session Management: Automatic connection lifecycle with reconnection logic
- Push Notification Support: Built-in handling for background push notifications
- TypeScript Support: Full TypeScript definitions for better developer experience
The demo shows how to integrate the library using the TelnyxVoiceApp component for automatic lifecycle management:
import { TelnyxVoiceApp, createTelnyxVoipClient } from '@telnyx/react-voice-commons-sdk';
// Create the VoIP client instance
const voipClient = createTelnyxVoipClient({
enableAppStateManagement: true, // Optional: Enable automatic app state management (default: true)
debug: true, // Optional: Enable debug logging
});
export default function App() {
return (
<TelnyxVoiceApp voipClient={voipClient} enableAutoReconnect={false} debug={true}>
<YourAppContent />
</TelnyxVoiceApp>
);
}const voipClient = createTelnyxVoipClient({
enableAppStateManagement: true, // Optional: Enable automatic app state management (default: true)
debug: true, // Optional: Enable debug logging
});Configuration Options Explained:
enableAppStateManagement: true- Optional (default: true): Enables automatic background/foreground app state management. When enabled, the library automatically disconnects when the app goes to background (unless there's an active call) and handles reconnection logic. Set tofalseif you want to handle app lifecycle manually.debug: true- Optional: Enables detailed logging for connection states, call transitions, and push notification processing. Useful for development and troubleshooting.
The TelnyxVoiceApp component handles:
- Automatic background/foreground lifecycle management
- Push notification processing from terminated state
- Login state management with automatic reconnection
- Background client management for push notifications
// Listen to connection state
voipClient.connectionState$.subscribe((state) => {
console.log('Connection state:', state);
});
// Listen to active calls
voipClient.calls$.subscribe((calls) => {
console.log('Active calls:', calls);
});
// Handle individual call state
call.callState$.subscribe((state) => {
console.log('Call state:', state);
});// Make a call
const call = await voipClient.newCall('1234567890');
// Answer incoming call
await call.answer();
// Control call
await call.mute();
await call.hold();
await call.hangup();
// Send DTMF tones (for IVRs, conference pins, etc.)
await call.dtmf('1'); // single digit
await call.dtmf('1234#'); // whole stringDTMF: call.dtmf(digits) sends each character as a Verto INFO message. Valid characters are 0-9, A-D, *, and #; anything else is silently dropped. The call must be ACTIVE — throws otherwise.
You do not need to wire up JS push handlers. The native layer does the work:
- Android:
TelnyxFirebaseMessagingServicereceives the FCM message, shows the incoming call notification, and (on Answer/Decline) launchesTelnyxMainActivitywith the call intent. - iOS:
TelnyxVoipPushHandlerreceives the PushKit payload and reports the call to CallKit.
In both cases the SDK connects the socket and restores the call internally — you just observe the VoIP client's streams to render UI.
In the snippets below,
voipClientis the instance returned bycreateTelnyxVoipClient()(see Basic Setup). It is a singleton, so it's safe to reference the same module-level value from any component, or to read it from context.
What to observe after the SDK-driven push login:
voipClient.connectionState$— emitsCONNECTEDwhen the socket is up and authenticated (there is no separateloginState$).voipClient.activeCall$— emits theCallonce the SDK has processed the push and the call has arrived. Navigate to your in-call screen here.voipClient.currentActiveCall— synchronous accessor for the cold-start race: on a push-launched mount, the call may already be present by the time you subscribe, so check this first and route immediately if set.
React.useEffect(() => {
if (voipClient.currentActiveCall) router.replace('/call');
const sub = voipClient.activeCall$.subscribe((call) => {
if (call) router.replace('/call');
});
return () => sub.unsubscribe();
}, []);CallKit (iOS) and ConnectionService (Android) already render the native incoming-call UI, so this navigation only matters once the user taps into the app.
When the OS wakes your app from a terminated state to deliver a call, the SDK is already logging in with stored credentials as part of the push flow. If your app also triggers a login on mount, you will get two competing sessions (double-login), which causes the call to fail or the socket to churn.
Use TelnyxVoipClient.isLaunchedFromPushNotification() to skip your own login when a push is in flight:
import { TelnyxVoipClient } from '@telnyx/react-voice-commons-sdk';
React.useEffect(() => {
TelnyxVoipClient.isLaunchedFromPushNotification().then((isFromPush) => {
if (isFromPush) {
// SDK is handling login via the push flow — do nothing.
return;
}
// Normal cold start — safe to auto-login.
voipClient.loginFromStoredConfig();
});
}, []);This is a static method (callable before the client is constructed) and works on both platforms. It returns true if the app was launched by an FCM intent (Android) or a PushKit payload (iOS) that has not yet been consumed.
The single most common integration bug is re-logging in while the SDK is already handling a push:
Do not do this:
// WRONG — runs on every mount, including push-launched cold starts
React.useEffect(() => {
voipClient.login(config); // or loginFromStoredConfig(), or loginWithToken()
}, []);Do this instead:
// Right — guard the login on push-launched cold starts
React.useEffect(() => {
TelnyxVoipClient.isLaunchedFromPushNotification().then((isFromPush) => {
if (!isFromPush) voipClient.loginFromStoredConfig();
});
}, []);Symptoms of getting this wrong: the incoming call rings briefly then disappears, the socket disconnects mid-call, or CallKit shows a call that immediately ends. If you see those, check whether your app is calling login* unconditionally on mount.
The library supports both credential-based and token-based authentication with automatic persistence for seamless reconnection.
import { createCredentialConfig } from '@telnyx/react-voice-commons-sdk';
const config = createCredentialConfig('your_sip_username', 'your_sip_password', {
debug: true,
pushNotificationDeviceToken: 'your_device_token',
pushWhenActive: true,
// iOS only: explicitly opt in to backend missed-call cleanup pushes.
enableMissedCallNotifications: true,
});
await voipClient.login(config);import { createTokenConfig } from '@telnyx/react-voice-commons-sdk';
const config = createTokenConfig('your_jwt_token', {
debug: true,
pushNotificationDeviceToken: 'your_device_token',
pushWhenActive: true,
// iOS only: explicitly opt in to backend missed-call cleanup pushes.
enableMissedCallNotifications: true,
});
await voipClient.loginWithToken(config);pushWhenActive is opt-in and defaults to false. Enable it on iOS when the device must receive a second PushKit call while another call is active or held.
enableMissedCallNotifications is also opt-in and defaults to false. Enable it only for iOS apps that want Telnyx to send backend missed-call cleanup pushes ("Missed call!") when a pending incoming call expires or is rejected elsewhere. When enabled, the SDK advertises ReactNative-mpn-<version> in the login User-Agent; when disabled, it advertises ReactNative-<version>. Android missed-call cleanup push behavior is unchanged.
Android uses the device's selected phone ringtone by default. To use an app-bundled sound instead, add a supported audio file such as my_ringtone.wav to android/app/src/main/res/raw/ and pass its resource name without the extension when logging in:
const config = createCredentialConfig('your_sip_username', 'your_sip_password', {
incomingCallRingtone: 'my_ringtone',
});The SDK persists this setting for incoming FCM pushes. If the resource is missing or the option is omitted, it falls back to the device ringtone.
The library automatically stores authentication data securely for seamless reconnection. You don't need to manually manage these storage keys - the library handles everything internally.
The library uses these AsyncStorage keys internally:
@telnyx_username- SIP username (credential auth)@telnyx_password- SIP password (credential auth)@credential_token- JWT authentication token (token auth)@push_token- Push notification device token@push_when_active- Whether the device remains eligible for PushKit calls while connected
Note: These are managed automatically by the library. You only need to call login() once, and the library will handle storage and future reconnections.
// Automatically reconnects using internally stored credentials or token
const success = await voipClient.loginFromStoredConfig();
if (!success) {
// No stored authentication data found, show login UI
console.log('Please log in again');
}When Auto-Reconnection Happens:
- App launches from background/terminated state
- Push notification received while disconnected
- Network reconnection after connectivity loss
- App state changes (foreground/background transitions)
Demo App Note: The demo app's TelnyxLoginForm component does additional storage for UI convenience (pre-filling the login form). This is separate from the library's internal authentication storage and is not required for production apps.
If you need to clear stored authentication data manually:
import AsyncStorage from '@react-native-async-storage/async-storage';
// Clear all Telnyx authentication data
await AsyncStorage.multiRemove([
'@telnyx_username',
'@telnyx_password',
'@credential_token',
'@push_token',
]);Important: This is typically not needed. The library manages authentication storage automatically. Only use this for advanced scenarios like implementing a "logout" feature that clears all stored data.
The demo app shows complete native integration for both platforms. These integrations are required for production apps using the library.
Your app's MainActivity should extend TelnyxMainActivity for automatic push notification handling:
// In your app's MainActivity.kt
import com.telnyx.react_voice_commons.TelnyxMainActivity
class MainActivity : TelnyxMainActivity() {
// Your app-specific code
override fun onHandleIntent(intent: Intent) {
super.onHandleIntent(intent)
// Handle any additional intent processing
}
}The TelnyxMainActivity provides:
- Automatic push notification intent handling
- Call action processing (Answer/Decline from notifications)
- Proper lifecycle management for VoIP functionality
- Integration with
VoicePnManagerfor push notification state
- Place
google-services.jsonin the project root. - Create an FCM service that extends
TelnyxFirebaseMessagingServiceand a notification action receiver that extendsTelnyxNotificationActionReceiver, then register both inAndroidManifest.xml. See docs-markdown/push-notification/app-setup.md for the full manifest + Kotlin boilerplate.
Do not register
messaging().setBackgroundMessageHandler(...)from JS. Android push is handled entirely by the nativeTelnyxFirebaseMessagingService— adding a JS handler will fight the native layer and can double-process calls. There is noTelnyxVoiceApp.handleBackgroundPushstep required on Android.
Your AppDelegate only needs to implement PKPushRegistryDelegate for VoIP push notifications. CallKit integration is automatically handled by CallBridge:
import PushKit
import TelnyxVoiceCommons
@UIApplicationMain
public class AppDelegate: ExpoAppDelegate, PKPushRegistryDelegate {
public override func application(
_ application: UIApplication,
didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? = nil
) -> Bool {
// Your existing setup...
// Initialize VoIP push registry via react-voice-commons
TelnyxVoipPushHandler.initializeVoipRegistration()
return super.application(application, didFinishLaunchingWithOptions: launchOptions)
}
// MARK: - VoIP Push Notifications
public func pushRegistry(_ registry: PKPushRegistry, didUpdate pushCredentials: PKPushCredentials, for type: PKPushType) {
TelnyxVoipPushHandler.shared.handleVoipTokenUpdate(pushCredentials, type: type)
}
public func pushRegistry(_ registry: PKPushRegistry, didReceiveIncomingPushWith payload: PKPushPayload, for type: PKPushType, completion: @escaping () -> Void) {
TelnyxVoipPushHandler.shared.handleVoipPush(payload, type: type, completion: completion)
}
}Note: CallKit integration (CXProvider, CXProviderDelegate, audio session management) is automatically handled by the internal CallBridge component. You don't need to implement any CallKit delegate methods manually.
- Configure VoIP push certificates in your Apple Developer account
- The
TelnyxVoipPushHandlerautomatically handles token registration and push processing - CallKit integration is automatically managed by CallBridge - no manual setup required
- Push Notification Handling: Both platforms handle background push notifications properly
- Native Call UI: CallKit (iOS, managed by CallBridge) and ConnectionService (Android) integration
- Audio Session Management: Automatic audio session handling for VoIP calls via CallBridge
- Background Processing: Seamless app state transitions and background call handling
- Intent Processing: Android intent handling for notification actions (Answer/Decline)
- Token Management: Automatic push token registration and updates
- Node.js and npm
- Expo CLI
- iOS development: Xcode
- Android development: Android Studio
-
Remove global Expo CLI (if previously installed):
npm uninstall -g expo
-
Install dependencies (including local Expo):
npm install
-
Run Expo install to ensure all dependencies are properly configured:
npx expo install
-
Configure Firebase for Android:
- Download the
google-services.jsonfile from your Firebase project - Place it in the root directory of the project
- Download the
-
Prebuild the project:
npx expo prebuild
-
Install iOS dependencies (required for iOS):
cd ios && pod install && cd ..
-
Start Metro bundler (in a separate terminal):
npx expo start --clear --reset-cache
-
Run on iOS:
npx expo run:ios
-
Run on Android:
npx expo run:android
Note: Make sure you have the necessary development environment set up for iOS (Xcode) or Android (Android Studio) before running the respective commands.
Once running, the demo app provides:
- Login Form: Enter your Telnyx SIP credentials or token
- Dialer Interface: Make outgoing calls with a native dialer UI
- Call Management: Answer, decline, hold, mute, and transfer calls
- Native Call UI: Integrated CallKit (iOS) and ConnectionService (Android)
- Push Notifications: Receive calls when app is in background or terminated
- Multiple Call Support: Handle multiple simultaneous calls
- Background Handling: Seamless app state transitions
The demo app includes debug logging enabled by default:
// Enable debug logging for the Telnyx SDK
if (__DEV__) {
log.setLevel('debug');
(global as any).__TELNYX_DEBUG__ = true;
}This provides detailed logging for:
- Connection state changes
- Call state transitions
- Push notification processing
- Background lifecycle events
If you encounter network connection errors on iOS:
# Try clearing cache and resetting
npx expo start --clear --reset-cache
# Or use tunnel mode
npx expo start --tunnel
# Or specify host explicitly
npx expo start --host lanIf you see VoIP-related crashes on iOS, ensure your AppDelegate.swift includes the VoIP push notification delegate methods. The project includes these by default, but if you encounter issues, rebuild the iOS project:
npx expo prebuild --platform ios --clean
npx expo run:iosTwo causes to check:
- You're calling
login*manually whileTelnyxVoiceAppis running withenableAutoReconnect={true}— pick one. - You're calling
login*on mount without guarding against push-launched cold starts. Wrap the call withTelnyxVoipClient.isLaunchedFromPushNotification()— see Push Notification Flow.
Check if enableAutoReconnect is set appropriately for your use case in the TelnyxVoiceApp configuration.
- Android:
- Verify
google-services.jsonis in the correct location and Firebase is properly configured - Ensure your MainActivity extends
TelnyxMainActivity - Check that
VoicePnManageris properly handling push actions
- Verify
- iOS:
- Ensure VoIP push certificates are configured in your Apple Developer account
- Verify AppDelegate implements
PKPushRegistryDelegateand delegates toTelnyxVoipPushHandler - Check that
TelnyxVoipPushHandler.initializeVoipRegistration()is called indidFinishLaunchingWithOptions
- Both: Verify the SDK is authenticated (or has stored credentials) — the native push flow will attempt to log in using stored credentials when a call comes in
- Android: Ensure MainActivity extends
TelnyxMainActivityfor proper intent handling - iOS: Verify AppDelegate implements
PKPushRegistryDelegateand delegates toTelnyxVoipPushHandler - CallKit: On iOS, CallKit integration is automatically handled by CallBridge - no manual setup required
- iOS: Audio session management is automatically handled by CallBridge
- Android: Verify ConnectionService is properly configured for audio routing
- Both: Ensure proper audio permissions are granted
Ensure you're unsubscribing from RxJS observables in your React components:
useEffect(() => {
const subscription = voipClient.connectionState$.subscribe(handleStateChange);
return () => subscription.unsubscribe();
}, []);For complete API documentation and advanced usage patterns, see:
- API Reference - Auto-generated TypeDoc documentation
- TelnyxVoiceApp Documentation - Detailed component guide
- iOS Missed Call Notifications - iOS-specific missed call handling with CallKit
- Trickle ICE Guide - Enabling and disabling Trickle ICE for faster call setup
- Push-When-Active Guide - Multidevice call delivery and answered-elsewhere handling
MIT License - see LICENSE file for details.