From the Streaming CDN team. The original, kept up to date, is at streaming-cdn.ewin888.com/guides/react-native-incoming-call-screen.
A calling app has to ring when it is not running: on a locked iPhone, on an Android phone in a pocket, after the system has reclaimed the process. That takes a push that wakes the app and a system incoming-call screen, on each platform in its own way. This guide shows how the Streaming CDN calling SDK does it for React Native and Expo.
How a call reaches a sleeping phone
- The caller starts a call; the call service sends a push with the call id, caller and mode (audio or video).
- iOS: a VoIP push (PushKit) wakes the app, which reports the call to CallKit. Android: an FCM message wakes the app, which shows a full-screen incoming-call notification.
- The person answers from the lock screen. The app accepts the call with its client token.
- The service makes the accept atomic — the first device to answer wins — and returns the media credential to that device only. Every other device signed in as the same person stops ringing.
The push itself carries no media token and no server key; the media credential is returned only after the accept succeeds.
{
"callId": "rtc_call_...",
"callerId": "user-17",
"callerName": "Alex",
"mode": "video",
"expiresAt": "2026-07-26T04:00:00Z"
}
Install
npm install @streaming-cdn/rtc-react-native react-native-webrtc
- Bare React Native apps rebuild the native project as usual.
- Expo apps need a development build or EAS Build (see the next section).
- Upgrading from a version without the native call layer means rebuilding the native project; until then native calls report that the module is unavailable.
Expo: what changes
- Install the SDK and the native WebRTC runtime with
npx expo install @streaming-cdn/rtc-react-native react-native-webrtc(or npm). - Add
"@streaming-cdn/rtc-react-native"topluginsinapp.json/app.config.js; the plugin writes the camera and microphone usage descriptions. - Run
npx expo prebuild, then make a development build (eas build --profile development, ornpx expo run:ios/npx expo run:android). Expo Go cannot load a native WebRTC runtime, so calls do not work there. - Test push wake-up on a real device: the iOS simulator does not receive VoIP pushes, and the Android full-screen permission below should be checked on the phones your users actually have.
{
"expo": {
"plugins": ["@streaming-cdn/rtc-react-native"]
}
}
Register each device for push wake-up
- Your backend issues a client token per signed-in user and device. The
deviceIdmust identify the installation, never the person: the service tells the answering user's other devices to stop ringing by comparing device ids. - Register the device for managed push only if you want the service to wake the app: iOS registers its PushKit VoIP token (64 hex characters), Android registers its FCM token. A registration whose platform does not match the client token is refused with
400 push_token_platform_mismatchinstead of being stored and failing silently later. - Tokens the push provider later reports as invalid are marked and skipped on the next call.
- Already have your own push or WebSocket channel? Deliver the incoming-call payload through it and hand it to the SDK; managed push is optional.
Join a call
Your backend keeps the server API key and hands the app a short-lived credential:
import { ReactNativeRtcClient } from "@streaming-cdn/rtc-react-native";
const identity = await fetch("https://your-backend.example.com/api/rtc/identity", {
credentials: "include"
}).then((response) => response.json());
const rtc = new ReactNativeRtcClient({
credential: identity.credential,
mode: "video",
publishAudio: true,
publishVideo: true,
receiveAudio: true,
receiveVideo: true
});
await rtc.join();
What the native call layer gives you
| Capability | API | Platform behaviour |
|---|---|---|
| Locked-screen and background calls | RtcNativeIncomingCalls |
Android full-screen intent notification; iOS PushKit with CallKit |
| Ringtone and ringback | RtcCallTones |
Honours the Android ringer mode; on iOS CallKit owns the incoming ringtone |
| Audio output routing |
client.setAudioOutput(), RtcAudioRouting
|
Earpiece, speaker and Bluetooth through Android AudioManager and iOS AVAudioSession
|
On iOS a call identifier must be a UUID. On Android your app owns its Firebase registration and forwards payloads to the SDK, so Firebase stays an optional dependency.
The Android 14 trap: full-screen intent
USE_FULL_SCREEN_INTENT is what lets an incoming call take over the screen. Three facts to know before shipping:
- Since Android 14 (API 34) it is a special app access, granted automatically only to apps whose core function is alarms or calls. Other apps start without it.
- Since 31 May 2024, Google Play requires a declaration for it in the Play Console (App content → Sensitive permissions). Submit it before review.
- When the grant is missing there is no error: the call silently becomes an ordinary heads-up notification that is easy to miss. A build can pass testing and still be effectively uncallable on real devices.
Check at runtime and guide the user before the first call:
const caps = await RtcNativeIncomingCalls.getIncomingCallCapabilities();
// caps.presentation: "full-screen" | "notification" | "none"
if (caps.presentation !== "full-screen") {
// explain why, then:
await RtcNativeIncomingCalls.openFullScreenIntentSettings();
}
getIncomingCallCapabilities() also reports notificationsEnabled; when it is false the call will not appear at all. On iOS the result is always full-screen, because CallKit owns presentation.
Ringing ends on its own
A ringing invitation is valid for 45 seconds and a call nobody answers is closed as missed. On Android the incoming-call notification also carries a system timeout (45 seconds by default, ringTimeoutMs in the push payload, 10–120 seconds), so ringing stops even if the app process was reclaimed and no cancel can run.
Errors worth handling
| Response | Meaning | What to do |
|---|---|---|
401 |
The client identity token expired | Refresh it through your backend |
403 |
This identity is not a recipient of the call | Do not show the call |
409 |
Another device answered, or the call stopped ringing | End the incoming UI quietly |
Push after expiresAt
|
The call is over | Discard it without showing any UI |
The Streaming CDN calling SDK brings voice and video calls with system incoming-call screens to iOS, Android, Web, React Native and Expo. Learn more.
Top comments (0)