React Native quick start
This page is for React Native CLI (bare) apps. Using Expo? The Expo quick start does all the native steps below for you.
Callx supports React Native 0.76 and later, on the New Architecture (as a typed TurboModule) and on the legacy architecture.
1. Install
npm install @bear-block/callx
cd ios && pod installRaise Android's minimum SDK to 29 in android/build.gradle; the React Native template defaults to 24:
buildscript {
ext {
minSdkVersion = 29
}
}The package contains native code, so rebuild the app; a JavaScript reload is not enough.
2. Configure the native projects
iOS
In Xcode, under Signing & Capabilities:
- Add Push Notifications.
- Add Background Modes and tick Audio, AirPlay, and Picture in Picture and Voice over IP.
Add a microphone description to Info.plist:
<key>NSMicrophoneUsageDescription</key>
<string>Example uses the microphone for calls.</string>Android
Add the permissions your app requests at runtime to android/app/src/main/AndroidManifest.xml. Callx's own manifest already declares MANAGE_OWN_CALLS, USE_FULL_SCREEN_INTENT and its incoming-call screen.
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.RECORD_AUDIO" />
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />Add Firebase to the Android app (google-services.json, the Google Services Gradle plugin and com.google.firebase:firebase-messaging).
3. Bootstrap the native core
The core starts with the process, before any push arrives and before the JavaScript bundle loads.
Android: MainApplication
// android/app/src/main/java/com/example/calls/MainApplication.kt
import com.google.firebase.messaging.FirebaseMessaging
import dev.callx.reactnative.CallxModule
import dev.callx.telecom.CallxPushTokens
class MainApplication : Application(), ReactApplication {
override fun onCreate() {
super.onCreate()
try {
CallxModule.bootstrap(this)
FirebaseMessaging.getInstance().token.addOnSuccessListener(CallxPushTokens::updateFcm)
} catch (error: Exception) {
// No Telecom on this device, or a storage error: calling is unavailable.
}
// …the rest of the template's onCreate (loadReactNative, etc.)
}
}Pass a CallxBootstrapConfig as the second argument to set an accountGeneration, a listener or your own media. See native host integration.
Android: forward FCM messages
// android/app/src/main/java/com/example/calls/MessagingService.kt
import com.google.firebase.messaging.FirebaseMessagingService
import com.google.firebase.messaging.RemoteMessage
import dev.callx.telecom.CallxBootstrap
import dev.callx.telecom.CallxPushTokens
class MessagingService : FirebaseMessagingService() {
override fun onMessageReceived(message: RemoteMessage) {
val ingress = CallxBootstrap.started?.ingress
if (ingress?.handlePush(message.data, message.priority, message.originalPriority) == true) return
// Not a Callx invitation: handle your app's other messages.
}
override fun onNewToken(token: String) = CallxPushTokens.updateFcm(token)
}<service android:name=".MessagingService" android:exported="false">
<intent-filter>
<action android:name="com.google.firebase.MESSAGING_EVENT" />
</intent-filter>
</service>Using @react-native-firebase/messaging?
Extend its service instead of FirebaseMessagingService, so your other messages still reach JavaScript: class MessagingService : ReactNativeFirebaseMessagingService(), call super.onMessageReceived(message) for non-Callx messages, and replace its manifest entry with yours (tools:node="replace"). The Expo plugin generates exactly this.
iOS: AppDelegate
Add two lines at the top of application(_:didFinishLaunchingWithOptions:), before the template starts React Native:
// ios/Example/AppDelegate.swift
import callx_react_native
func application(
_ application: UIApplication,
didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? = nil
) -> Bool {
do {
try CallxReactNativeHost.bootstrap(CallxBootstrapConfig())
} catch {
NSLog("Callx: calling is unavailable: \(error)")
}
// …the template's code that starts React Native
}Callx owns the PKPushRegistry, reports each VoIP push to CallKit before the handler returns, and records the VoIP token for getPushToken().
4. Use it from JavaScript
Keep one Callx instance for the whole app, for example in a module or a context provider.
import {Callx} from '@bear-block/callx';
export const callx = new Callx();
export async function startCalling() {
const capabilities = await callx.setup();
if (!capabilities.nativeCalling) return; // Bootstrap failed or no Telecom.
// Send this to your backend so it can push invitations to this device.
const token = await callx.getPushToken(); // {type: 'voip' | 'fcm', token}
if (token) await api.registerPushToken(token.type, token.token);
// Your UI follows the native state.
return callx.observe(({call}) => {
if (!call) return showIdle();
switch (call.state) {
case 'incoming': return showIncoming(call);
case 'connecting': return showConnecting(call);
case 'active': return showInCall(call);
case 'held': return showOnHold(call);
case 'ended': return showEnded(call.endReason);
case 'outgoing': return showDialing(call);
}
});
}
export async function answer(callId: string) {
const result = await callx.answer(callId);
if (result.status !== 'applied') showError(result.error?.message);
}Ask for the microphone permission (and notifications on Android 13+) while the app is in use, before the first call arrives.
5. Send a test call
npx -p @bear-block/callx-testkit callx-push android \
--service-account firebase-adminsdk.json --token <FCM token>Kill the app, send the push, and the phone rings. The backend guide has the payloads your server sends in production.
Next steps
Optional call overlay and mini-call
Callx also exports an optional app UI layer. Incoming stays on Home until accepted, an expanded call overlays navigation, and Back minimizes to an in-app mini-call. The native core remains the call-state owner. See call UI for the exports and integration.