Prerequisites
- Calls SDK 5.1 set up and the user logged in.
- Android: a Firebase project with your app,
google-services.jsoninandroid/app/, and the Google Services plugin applied (Step 1). You don’t need@react-native-firebase. - iOS: an APNs provider with a token-based (
.p8) key in the CometChat dashboard, and the Push Notifications capability plus the Voice over IP background mode enabled in Xcode. - Push notifications enabled in the CometChat dashboard with an FCM provider (Android) and an APNs provider (iOS), and those providers’ ids.
- React Native 0.78 or later,
minSdkVersion 24on Android, and an iOS deployment target of 15.1 or later (what current React Native requires, whatever the packages’ podspecs allow). Some ring behaviours degrade below API 26, where Android gained notification channels and full-screen intents: on 24–25 the ring posts a plain notification instead of the channel-backed, full-screen one.
Step 1: Add the dependencies
package.json
The Calls SDK is a dependency of the VoIP package, not a peer: the package calls into it to pre-warm and leave sessions, and a version mismatch there is a silent failure rather than a build error.
Native setup
Android: apply the Google Services plugin. The push package already depends onfirebase-messaging, so don’t add it yourself.
android/build.gradle
android/app/build.gradle
ios/YourApp/AppDelegate.swift
AppDelegate.
Step 2: Initialise once
Run this once per app start, as early as you can — the configuration is saved natively, so a push that wakes a cold process still finds it. There are two shapes, and which one you use matters. A calls-only app passes its configuration to the VoIP package and calls nothing else. Calling still needs a push token, and the core package is what registers it, so this initialises the core for you:init() first and with no arguments, then initialises the core with its own chat settings:
Don’t pass a config to
CometChatVoipNotifications.init() in a chat app. With a config it initialises the core for you, using only what you passed — so notificationSmallIcon, androidChannelId and every other chat-side setting is never set. Pass a config there only in a calls-only app, which calls nothing else.Step 3: Register the background task
Android can send a decline while your app is fully killed. Register the background task at module scope inindex.js, before AppRegistry.registerComponent:
index.js
unanswered.
Step 4: Firebase delegation (only if you own the transport)
You normally skip this step. The package ships its own Firebase messaging service and registers it in its manifest, so call pushes reach it without any code of yours. Declaring your own service in addition is what breaks delivery, not what fixes it. Delegate only if your app already owns asetBackgroundMessageHandler:
index.js
More than one messaging service is not a conflict. Every app that includes Firebase declares
FirebaseMessagingService at priority −500; what matters is the service Android actually resolves to. The SDK reports that in getDiagnostics() as competingMessagingService, which is null when all is well.Step 5: Answer and join
On Answer the SDK stops the ring, brings your app to the front and emitsonCallAccepted. It doesn’t ask for the microphone or camera; request them after sign-in (Step 6). There is nothing to “accept”.
PNCallInfo fields: sessionId, callerUid, callerName, callerAvatar, callType, receiverId, receiverType, expiresAtMs, isPeerCall, source (push for a call the package received, app for one handed over with showIncomingCall) and callAction. expiresAtMs is an absolute instant anchored to the caller’s own start, not a duration from now, so both sides stop ringing together however late the push arrived — read it rather than starting a timer of your own.
A call answered before your JavaScript existed — the killed-app case this package is for — is buffered natively and replayed to the first
onCallAccepted subscriber. Subscribing late is safe; not subscribing at all means the user answers and nothing happens.Step 6: Permissions and ring readiness
On Android there is nothing to declare.POST_NOTIFICATIONS comes from the push package’s manifest, and RECORD_AUDIO, CAMERA and BLUETOOTH_CONNECT from the VoIP package’s; the manifest merger adds them to your app. The SDK requests the runtime prompts for you — CometChatPNHelper.requestNotificationPermission() and requestCallPermissions().
On iOS you do declare these, because Info.plist keys are your app’s own and nothing merges them in from a pod:
ios/YourApp/Info.plist
phoneCall, MANAGE_OWN_CALLS, wake lock, vibrate, the ring activity and the ring service) comes from the packages’ own manifests too, and CallKit and PushKit are linked by the VoIP pod. See the Android permissions breakdown for which package declares what, and what a chat-only app gets.
Microphone and camera — ask for these yourself. On React Native the package does not prompt when the user answers; it dismisses the ring and hands the call straight to your JavaScript. Call CometChatPNHelper.requestCallPermissions() once after sign-in, so the permission is already granted by the time a call arrives.
Ring readiness. Several device settings silence a call with no error anywhere. Check once after sign-in and tell the user:
Diagnostics for a bug report.
getDiagnostics() returns a redacted snapshot (whether a provider id exists, never what it is; never a token or payload):
verifyPushSetup() registers the token and reports one of seven values: ok, notInitialised (no init() yet), notLoggedIn, notificationsDisabled (the OS permission is off, so nothing can be shown), tokenNotRegistered, testPushUnavailable (registration worked, the loopback test push is not available on the backend yet: a warning, not a failure) or testPushTimeout (the test push never arrived: delivery is broken). The token value is never logged, only its length and a short prefix.
How a ring ends
Every way out is handled by the SDK and needs no code in your app.
Once the session is prepared, the Calls SDK reporting it ended also stops the ring immediately, without waiting for a terminal push.
An
ongoing push is addressed to the CALLER, not the callee. Its sender is whoever performed the action — the callee who answered — so neither a sender nor a receiver comparison can tell you whether it concerns you. The SDK gates it on local ring state instead.A call while in a call
Call waiting, the way the phone app does it. “In a call” means the Calls SDK is joined to a session, or a call answered here is still joining; a session that is only prepared while it rings does not count.The SDK never decides on its own that the user is busy. A second call always rings;
busy is sent only when the user taps Decline on it. The one thing the SDK does suppress is a call’s own push arriving back on the caller’s device — that is filtered out rather than rung, and nothing is sent to anyone.End & accept waits for the new session’s prepare to settle, up to 5 seconds, before handing the call to your screen. Joining before the previous session has been released is what silently never completes.
Missed calls
Posted as “Missed call” / “Missed group call” with the caller’s name and call type, one per session, grouped under the SDK’s own summary so Android never folds them into your other notifications. Posted when: nobody answers in the ring window; acancelled, unanswered or timed_out push ends a ring here; such a push arrives for a call that never rang here (the initiated push was lost or overtaken); an initiated push arrives already expired.
Not posted when: answered here or on another device; declined (rejected or busy) here; ended; the user’s own outgoing call; logout while ringing.
Turn missed calls off with showMissedCallNotification: false. This is a notification only; call history comes from Call Logs.
When the app is killed
This is the case the push exists for.- The high-priority data push (Android) or the PushKit push (iOS) starts the app process.
- The native layer finds no in-memory configuration and restores it from disk.
- The ring runs on a foreground service of type
phoneCallon Android, so the system keeps the process alive through the answer; on iOS CallKit holds the call. An alarm backs up the ring timeout. - On Answer, the SDK launches your app and buffers the answered call until your JavaScript subscribes. It does not ask for the microphone here — request it after sign-in instead (see Step 6).
- A decline is sent through the background task, so it goes out even if the process dies right after the tap. A ring that simply times out sends nothing — the ring stops, a missed call is posted locally, and the caller’s
unansweredcomes from the backend’s own ring window.
CometChatVoipNotifications.init() as the first await in your app’s entry point — before reading stored credentials or restoring the session, not from them. It needs no login to wire up the ring; awaiting a credentials read first spends the cold-launch ring window, which is what the warning in step 2 is about. Restore the session straight afterwards, and the answered call is delivered once your JavaScript subscribes.
On iOS, APNs and VoIP pushes really are delivered to the Simulator and will cold-launch a killed app. Only CallKit’s own call presentation does not appear there, so test the ring on a device and the delivery path anywhere.
Staging or dedicated deployments
If your Chat and Calls SDKs point at a staging or dedicated host, give the Push SDK the same one. Otherwise the token is registered on the default production host, where your app id does not exist, and no push ever arrives.Tokens
You do not register the push token. The SDK fetches it from Firebase or PushKit and registers it on login, under the Chat auth token when there is one and the Calls auth token otherwise. Unregistering is yours to call. Unlike the native Android and iOS SDKs, this package installs no logout listener, so nothing deletes the token on its own — see below. Unregister before the session goes away:unregister() deletes the token from the backend and needs the logged-in credential, so after logout() there is nothing left to authenticate the delete, and the device goes on receiving the departed user’s call pushes.
The SDK keeps its ring identity in step with the Calls SDK’s login listener: logging in writes the uid, logging out clears it, and
init() re-reads it to heal a login it missed. An app that logs the Chat SDK in and out without CometChatCalls.login / logout must call clearIdentity() itself.Your own incoming-call screen
Subscribe toonCallRinging and draw your own UI. The SDK still owns the ringtone, the pre-warm, the timeout, the keep-alive and the decline queue; only the UI is yours.
onRingEnded, a cancelled call leaves your screen showing an Answer button for a call that no longer exists.
This replaces the in-app screen only. A push that arrives while your app is killed still rings with the SDK’s own native screen on Android and CallKit on iOS, because neither can be drawn from JavaScript that is not running.
Theming the ring
The native ring screen is drawn before any JavaScript exists — that is what lets a push ring a killed app — so the UI Kit’s theme cannot reach it. Android gives you two ways to colour it; on iOS the screen is CallKit’s and belongs to the system.A theme, set from JavaScript (Android)
mode: "system" is read when the screen is drawn, so a call arriving after the user switches the device to dark is drawn dark without your app running.
Set
acceptIcon whenever you set a light acceptButton, or the white glyph disappears into it.
Colour resources (Android)
Every field above falls back to acometchat_pn_call_* colour resource, which your app can replace at build time by declaring the same name — an app’s resource wins over a library’s. Use this when the colours never change at runtime, or to set a colour the theme leaves out.
android/app/src/main/res/values/colors.xml
Strings and the call icon (Android)
Declared the same way, by name. This is also how you localise the ring.- Strings:
cometchat_pn_incoming_call,cometchat_pn_call,cometchat_pn_unknown_caller,cometchat_pn_incoming_audio_call,cometchat_pn_incoming_video_call,cometchat_pn_missed_audio_call, and the othercometchat_pn_*strings. - Drawable:
cometchat_pn_ic_call.
android/app/src/main/res/values/strings.xml
iOS
CallKit draws the incoming call and its appearance belongs to the system.callKitName, callKitIconName, callKitRingtone, callKitSupportsVideo and callKitIncludeInRecents on PNConfiguration are the whole surface; incomingCallTheme is ignored there. The native iOS SDK has the same limit — it is not a React Native one.
The colours above paint the full-screen ring. Android styles the call-style notification itself, so a heads-up ring on an unlocked device keeps the system’s colours.
ringInForeground: false and your own component on onCallRinging — see Your own incoming-call screen. That covers a call arriving while your app is open; a push to a killed app still rings with the native screen, which is the point of it.
The avatar is always the caller’s initials; the screen does not load the avatar URL. The ringtone is the device’s default on the incoming-call channel; because Android freezes a channel’s sound at creation, the SDK never changes it afterwards.
Manual mode
callMode: 'manual' makes the SDK parse, gate and forward only. It never rings on its own. The call reaches onCallRinging, and from there every step is yours:
Reporting a call you placed yourself
iOS only. An outgoing call you place yourself has to be reported to CallKit; nothing does it for you. Incoming calls get a CallKit report for free because PushKit forces one, but an outgoing call has to ask — and without it CallKit does not own the audio session, so a cellular call or another VoIP app can take the microphone and never give it back, the call never reaches Recents, and iOS cannot show it in the in-call status bar.All three are no-ops on Android, which has no equivalent: an outgoing call there is ordinary app audio, with no system call UI and no Recents entry. They are declared on both platforms deliberately, so you can call them unconditionally without a
Platform.OS check.Knowing about a call in your own code
Listen on this SDK, not on the Calls SDK. On React Native the VoIP package is where an incoming call announces itself and its end; the Calls SDK has no incoming-call listener, because on this platform it never sees the call until you join one. It is the same callback whether the call arrived by push on a killed process or on a live one — the package buffers the ring natively and drains the buffer atinit().
on… returns an unsubscribe function — call it when your screen unmounts, or every handler fires twice after the next mount. Subscribing late is safe: a ring or an answered call that arrived before your component mounted is held and delivered to the first subscriber, so the call that launched a killed app is not lost.
Coming from the Android or iOS guides? They tell you to listen on the Calls SDK with
addIncomingCallListener. React Native has no such method — the split between the two packages puts incoming calls on the VoIP package instead, as above. The Calls SDK’s addEventListener is for a session you are already connected to, not for calls arriving.Troubleshooting
Related
- Ringing: starting, answering and ending calls with the Calls SDK.
- Migrating from legacy ringing.
- Background Handling.
- React Native Push Notifications: chat notifications with the same SDK.