diff --git a/calls/react-native/events.mdx b/calls/react-native/events.mdx
index af039027a..49ce91bf5 100644
--- a/calls/react-native/events.mdx
+++ b/calls/react-native/events.mdx
@@ -103,7 +103,8 @@ CometChatCalls.addEventListener('onCameraFacingChanged', (facing) => {
| Event | Payload | Description |
|-------|---------|-------------|
-| `onParticipantJoined` | Participant | A participant joined |
+| `onParticipantConnected` | Participant | A participant's device prepared the session without joining. On the caller's prepared session this is the callee ringing |
+| `onParticipantJoined` | Participant | A participant joined. On the caller's prepared session this is the callee answering: join now |
| `onParticipantLeft` | Participant | A participant left |
| `onParticipantAudioMuted` | Participant | A participant muted their audio |
| `onParticipantAudioUnmuted` | Participant | A participant unmuted their audio |
@@ -252,8 +253,81 @@ function useCallEvents(onCallEnd: () => void) {
export default useCallEvents;
```
+## Events on a prepared session
+
+`CometChatCalls.getActiveSession()` returns the session as soon as it is **prepared** — which is what `initiateCall` does on the caller and what the Push SDK's ring does on the callee — before anyone has joined. Listening on it is what lets a caller show "Ringing…" and detect the answer without joining first.
+
+```ts
+const session = CometChatCalls.getActiveSession(); // null when no session is prepared or joined
+const me = CometChatCalls.getLoggedInUser()?.uid;
+
+session?.addEventListener('onParticipantConnected', p => {
+ if (p.uid !== me) setStatus('Ringing…'); // the callee's device got the push
+});
+session?.addEventListener('onParticipantJoined', p => {
+ if (p.uid !== me) join(); // the callee answered
+});
+```
+
+| Event | On a prepared session it means |
+|-------|-------------------------------|
+| `onParticipantConnected` | The other device prepared the session: it is ringing there |
+| `onParticipantJoined` | The other side answered |
+| `onSessionTimedOut` | Nobody joined in time |
+| `onConnectionClosed` | The call ended before anyone joined (declined, cancelled from another device, or the room went away) |
+
+
+**A prepared session has no roster.** It receives `onParticipantLeft` but never `onParticipantJoined` for participants who were already there, because there are none. `onParticipantConnected` is the only caller-side signal that the callee's device has been reached.
+
+
+---
+
+## Incoming call events
+
+On React Native these come from the Push Notifications SDK, not the Calls SDK. They report a call for the logged-in user starting and ending, whether it arrived by push while the app was killed or while it was running.
+
+```ts
+import { CometChatVoipNotifications } from '@cometchat/push-notifications-voip-react-native';
+
+const offRinging = CometChatVoipNotifications.onCallRinging(call => { /* ringing */ });
+const offAccepted = CometChatVoipNotifications.onCallAccepted(call => { /* answered here */ });
+const offEnded = CometChatVoipNotifications.onCallEnded(e => { /* e.reason */ });
+const offRingEnded = CometChatVoipNotifications.onRingEnded(e => { /* the ring stopped */ });
+```
+
+| Event | Payload | Description |
+|-------|---------|-------------|
+| `onCallRinging` | `PNCallInfo` | A call for the logged-in user is ringing |
+| `onCallAccepted` | `PNCallInfo` | The user answered; open your call screen |
+| `onCallEnded` | `PNCallEndEvent` | The call ended. Caller: `rejected`, `busy`, `unanswered`, `cancelled`. Callee still ringing: `cancelled`, `ongoing` (answered elsewhere), `rejected` / `busy` (declined here), `unanswered` |
+| `onRingEnded` | `PNRingEndEvent` | The ring stopped, whatever happened to the call |
+
+Each returns an unsubscribe function. Subscribing late is safe: a call answered before your component mounted is held and delivered to the first subscriber. See [Ringing](/calls/react-native/ringing).
+
+---
+
+## Login events
+
+`CometChatCalls.addLoginListener` reports the Calls SDK login state. The Push SDK uses it to keep the identity its ring gates read in step with the logged-in user; your app can use it to gate call UI.
+
+```ts
+CometChatCalls.addLoginListener('my-app', {
+ onLoginSuccess: user => console.log('logged in as', user.uid),
+ onLogoutSuccess: () => console.log('logged out'),
+});
+
+CometChatCalls.removeLoginListener('my-app');
+```
+
+
+Listener ids are keys: registering the same id twice replaces the earlier listener. Remove it when the screen goes away. The `User` the Calls SDK hands back is a plain object with a `uid` field, not a class with `getUid()`.
+
+
+---
+
## Related Documentation
+- [Ringing](/calls/react-native/ringing) - Start, answer and end calls
- [Actions](/calls/react-native/actions) - Control the call programmatically
- [Session Settings](/calls/react-native/session-settings) - Configure event listeners
- [Participant Management](/calls/react-native/participant-management) - Manage participants
diff --git a/calls/react-native/migrating-from-legacy-ringing.mdx b/calls/react-native/migrating-from-legacy-ringing.mdx
new file mode 100644
index 000000000..03a7353f4
--- /dev/null
+++ b/calls/react-native/migrating-from-legacy-ringing.mdx
@@ -0,0 +1,295 @@
+---
+title: "Migrating from Legacy Ringing"
+sidebarTitle: "Migrating from Legacy Ringing"
+sdk_version: "5.x"
+description: "Move a React Native app from Chat SDK call signalling and Push Notifications SDK 1.0 VoIP to ringing with the Calls SDK 5.1 and the Push Notifications SDK 2.0: what to add, what to delete, and how each method maps."
+---
+
+Legacy ringing signalled calls through the Chat SDK: `CometChat.initiateCall` created the call, a `CallListener` on the WebSocket told the callee, `acceptCall` and `rejectCall` answered, and the app rang the device itself — usually with `react-native-callkeep` and `react-native-voip-push-notification` wired up by hand. Ringing now runs on the Calls SDK and the Push Notifications SDK's VoIP package. The Chat SDK's calling methods are deprecated.
+
+This page is the diff. For the full description of the current flow see [Ringing](/calls/react-native/ringing); for the push side see [Push ringing](/calls/react-native/voip-calling).
+
+## Why it changed
+
+| Legacy | Now |
+|--------|-----|
+| Incoming calls arrived on the Chat WebSocket, so a killed or backgrounded app needed a separate push + CallKeep integration to ring | One path: the push rings in every app state, with the app killed included |
+| Four pieces of app code rang the device (listener, overlay, ringtone, busy check) | The Push SDK rings, draws the incoming screen, handles busy, timeout and missed calls |
+| `acceptCall` / `rejectCall` / `endCall` were HTTP calls your app had to sequence with the session | Answer = join. Decline, timeout and busy are sent by the Push SDK through a durable queue |
+| Caller only knew "accepted" or "rejected" | Caller sees "Ringing…" (the callee's device was reached), "answered", and the reason a call ended (`rejected`, `busy`, `unanswered`, `cancelled`) |
+| Chat SDK required for calling | Chat SDK needed for chat only; a calls-only app works |
+
+## Before and after
+
+```mermaid
+sequenceDiagram
+ participant A as Caller
+ participant Chat as Chat SDK (WebSocket)
+ participant B as Callee
+ rect rgb(240,240,240)
+ Note over A,B: Legacy ringing
+ A->>Chat: CometChat.initiateCall(call)
+ Chat-->>B: CallListener.onIncomingCallReceived
+ B->>B: App shows its own incoming UI, CallKeep, ringtone
+ B->>Chat: CometChat.acceptCall(sessionId)
+ Chat-->>A: CallListener.onOutgoingCallAccepted
+ A->>A: start session
+ B->>B: start session
+ A->>Chat: CometChat.endCall(sessionId)
+ Chat-->>B: onCallEndedMessageReceived
+ end
+```
+
+```mermaid
+sequenceDiagram
+ participant A as Caller
+ participant BE as Backend
+ participant P as Callee · Push SDK
+ participant C as Callee · Calls SDK
+ rect rgb(232,244,236)
+ Note over A,C: Ringing now
+ A->>BE: CometChatCalls.initiateCall(uid, options)
+ BE-->>P: push (initiated)
+ P->>P: rings, shows incoming screen / CallKit
+ P->>C: connect(sessionId)
+ C-->>A: onParticipantConnected ("Ringing…")
+ P->>C: Answer → onCallAccepted → mount the call component
+ C-->>A: onParticipantJoined → caller joins
+ A->>A: session.leave() to hang up
+ end
+```
+
+## Step 1: Update dependencies
+
+```bash
+npm install @cometchat/calls-sdk-react-native@^5.1.0 \
+ @cometchat/push-notifications-react-native@^2.0.0 \
+ @cometchat/push-notifications-voip-react-native@^2.0.0
+cd ios && pod install
+```
+
+- `minSdkVersion` 26 or higher on Android, iOS 13+.
+- Keep `@react-native-firebase/messaging` and `google-services.json`.
+- Remove `react-native-callkeep` and `react-native-voip-push-notification` if nothing else uses them; the VoIP package registers with PushKit and CallKit itself.
+
+
+`callMode` defaults to `'AUTO'`, which is what this page assumes. Installing the VoIP package is the opt-in: the core package detects it and installs the ringer.
+
+
+## Step 2: Replace the push configuration
+
+
+
+```ts
+await CometChatPushNotifications.init({
+ appId, region,
+ notificationSmallIcon: 'ic_notification',
+ voIPEnabled: true,
+});
+
+CometChatPushNotifications.setOnCallAnsweredHandler(async callInfo => {
+ await CometChat.acceptCall(callInfo.sessionId);
+ navigate('Call', { sessionId: callInfo.sessionId });
+});
+```
+
+
+```ts
+await CometChatPushNotifications.init({
+ appId, region,
+ fcmProviderId: FCM_PROVIDER_ID,
+ apnsProviderId: APNS_PROVIDER_ID,
+ notificationSmallIcon: 'ic_notification',
+ callKitName: 'Acme',
+});
+
+await CometChatVoipNotifications.init(); // no config: the core already has it
+
+CometChatVoipNotifications.onCallAccepted(call => {
+ navigate('Call', { sessionId: call.sessionId }); // no acceptCall to make
+});
+```
+
+
+
+
+A **calls-only** app passes its configuration to `CometChatVoipNotifications.init(config)` instead and never calls the core's `init`. An app that does both must not pass a config to the VoIP `init()` — that re-runs the core initialisation and drops every chat option.
+
+
+If your Chat and Calls SDKs point at a staging or dedicated deployment, pass the same host as `clientHost`. Otherwise the push token registers on the default host and no push ever arrives.
+
+## Step 3: Make `init()` the first thing you await
+
+```ts
+// index.js / App.tsx
+await CometChatVoipNotifications.init(); // FIRST
+await CometChatUIKit.init(uiKitSettings); // then everything else
+await restoreSessionAndLogin();
+```
+
+A cold launch caused by a call push has a ring window measured from the caller's side. Anything awaited before `init()` — a stored-credentials read, a remote config fetch, a login — spends that window, and the call is lost before the ring is wired up.
+
+## Step 4: Register the background task and delete the token code
+
+```js index.js
+import { registerBackgroundCallTask } from '@cometchat/push-notifications-voip-react-native';
+registerBackgroundCallTask();
+```
+
+The Push SDK registers the token on login and unregisters it on logout by itself. Remove:
+
+- `messaging().getToken()` + `registerPushToken(...)` after login
+- `VoipPushNotification.addEventListener('register', …)` and the APNs VoIP token plumbing
+- `unregisterPushToken(...)` before logout — replaced by `CometChatPushNotifications.unregister()`, which must run **before** `logout()`
+
+## Step 5: Delete the app-side ringing
+
+Everything in this list is now done by the Push SDK. Remove it, or it will fight the SDK's ring (two ringtones, two screens, a busy reject sent on top of the SDK's).
+
+- `CometChat.addCallListener(...)` and its `onIncomingCallReceived` handler
+- The incoming-call modal, overlay or screen you rendered from it
+- Your own ringtone or sound player for incoming calls
+- The "already in a call → reject with busy" check
+- `RNCallKeep.setup`, `displayIncomingCall`, `answerCall`/`endCall` event wiring, `VoipPushNotification` handlers, and the `READ_PHONE_STATE` / `MANAGE_OWN_CALLS` / `ANSWER_PHONE_CALLS` permission chain
+- Any foreground check around call pushes
+
+Keep: `RECORD_AUDIO`, `CAMERA` and `POST_NOTIFICATIONS` in the manifest, and `NSMicrophoneUsageDescription`, `NSCameraUsageDescription` and the `voip` background mode in `Info.plist`. The SDKs request them but do not declare them.
+
+## Step 6: Rewrite the call code
+
+| Legacy | Now | Notes |
+|--------|-----|-------|
+| `CometChat.initiateCall(new CometChat.Call(uid, type, RECEIVER_TYPE_USER))` | `CometChatCalls.initiateCall(uid, { type, receiverType, sessionSettings })` | Set `sessionSettings.sessionType`, or an audio call opens the camera |
+| `CometChat.initiateCall(call, timeoutSeconds)` | No client timeout. The ring window is 45 s on both sides (or the push's `ringTimeout`) | Run your own 45 s timer on the caller if you want the screen to close on its own |
+| `CallListener.onIncomingCallReceived(call)` | Nothing to write; the Push SDK rings. For your own state use `CometChatVoipNotifications.onCallRinging` | Fires whether the call came by push on a killed process or on a live one |
+| `CallListener.onOutgoingCallAccepted(call)` | `session.addEventListener('onParticipantJoined', …)` on the session `initiateCall` prepared, via `CometChatCalls.getActiveSession()` | New in-between event: `onParticipantConnected` = the callee's device is ringing |
+| `CallListener.onOutgoingCallRejected(call)` | `CometChatVoipNotifications.onCallEnded` with reason `rejected` or `busy` | |
+| `CallListener.onIncomingCallCancelled(call)` | `onCallEnded` with `cancelled`; the Push SDK has already stopped the ring | |
+| `CallListener.onCallEndedMessageReceived(call)` | `onSessionLeft` / `onConnectionClosed` on the joined session; `onCallEnded` with `unanswered` while ringing | |
+| `CometChat.acceptCall(sessionId)` then start the session | `onCallAccepted` fires; mount the call component | The Push SDK asked for mic/camera before opening your screen |
+| `CometChat.rejectCall(sessionId, CALL_STATUS_REJECTED)` | Push SDK Decline button. Own incoming screen: `CometChatVoipNotifications.declineCall(sessionId)` | Sends `busy` automatically when in another call |
+| `CometChat.rejectCall(sessionId, CALL_STATUS_BUSY)` | Nothing; the Push SDK decides busy | |
+| `CometChat.rejectCall(sessionId, CALL_STATUS_CANCELLED)` | `CometChatCalls.endCall(sessionId, 'cancelled')` | |
+| `CometChatCalls.startSession(...)` / `joinSession(...)` | `CometChatCalls.connect(sessionId)` then `` | The session is already prepared, so the join is near-instant |
+| `session.leaveSession()` + `CometChat.endCall(sessionId)` | `session.leave()` | No status to send on a hang-up; the room tells the other side and the backend logs it |
+| `CometChat.getActiveCall()` / `clearActiveCall()` | `CometChatCalls.getActiveSession()` (null when no session is prepared or joined) | |
+| `CometChat.logout()` | `CometChatCalls.logout()` as well (the UI Kit's `CometChatUIKit.logout` does both), after `CometChatPushNotifications.unregister()` | Otherwise the Calls SDK stays signed in as the old user and a ring is not stopped |
+
+### Caller screen, before and after
+
+
+
+```ts
+CometChat.addCallListener(LISTENER_ID, new CometChat.CallListener({
+ onOutgoingCallAccepted: call => startSession(call.getSessionId()),
+ onOutgoingCallRejected: () => close('rejected'),
+ onIncomingCallCancelled: () => close('cancelled'),
+}));
+
+const call = await CometChat.initiateCall(
+ new CometChat.Call(uid, CometChat.CALL_TYPE.AUDIO, CometChat.RECEIVER_TYPE.USER),
+);
+setStatus('Calling…');
+```
+
+
+```ts
+const result = await CometChatCalls.initiateCall(uid, {
+ type: 'audio',
+ receiverType: 'user',
+ sessionSettings: { sessionType: 'voice' },
+});
+if (!result.isSuccess) return showError(result.error);
+
+const session = CometChatCalls.getActiveSession();
+const me = CometChatCalls.getLoggedInUser()?.uid;
+setStatus('Calling…');
+
+session?.addEventListener('onParticipantConnected', p => {
+ if (p.uid !== me) setStatus('Ringing…'); // new: the callee's device was reached
+});
+session?.addEventListener('onParticipantJoined', p => {
+ if (p.uid !== me) joinCall(result.value.sessionId);
+});
+
+CometChatVoipNotifications.onCallEnded(e => close(e.reason)); // rejected / busy / unanswered
+```
+
+
+
+### Callee screen, before and after
+
+
+
+```ts
+CometChat.addCallListener(LISTENER_ID, new CometChat.CallListener({
+ onIncomingCallReceived: async call => {
+ if (inACall) return CometChat.rejectCall(call.getSessionId(), CometChat.CALL_STATUS.BUSY);
+ playRingtone();
+ RNCallKeep.displayIncomingCall(call.getSessionId(), call.getSender().getName());
+ showIncomingModal(call);
+ },
+ onIncomingCallCancelled: () => { stopRingtone(); hideIncomingModal(); },
+}));
+
+async function onAnswerPressed(call) {
+ stopRingtone();
+ await CometChat.acceptCall(call.getSessionId());
+ startSession(call.getSessionId());
+}
+```
+
+
+```ts
+// Nothing rings here any more: the Push SDK does, in every app state.
+CometChatVoipNotifications.onCallAccepted(call => {
+ navigate('Call', { sessionId: call.sessionId });
+});
+```
+
+
+
+## Step 7: Change the logout
+
+
+
+```ts
+await CometChat.logout();
+```
+
+
+```ts
+await CometChatPushNotifications.unregister(); // needs the credential; must come first
+CometChatVoipNotifications.clearIdentity();
+await CometChatUIKit.logout(); // logs out Chat and Calls
+```
+
+
+
+`unregister()` after `logout()` cannot authenticate the delete, so the token stays registered and the device goes on receiving the departed user's call pushes. Logging out of the Chat SDK alone leaves the Calls SDK signed in, so a ring keeps playing for the old user.
+
+## If you drew your own incoming-call screen
+
+You can keep it. Subscribe to `onCallRinging` instead of `CallListener.onIncomingCallReceived`, close it from `onRingEnded`, and answer and decline through `CometChatVoipNotifications.answerCall` / `declineCall`. Tell the SDK your screen is up with `notifyCallScreenOpen(sessionId)` so it does not also show its own. See [Your own incoming-call screen](/calls/react-native/voip-calling#your-own-incoming-call-screen).
+
+This replaces the in-app screen only. A push that arrives while your app is killed still rings with the SDK's native screen on Android and CallKit on iOS — neither can be drawn from JavaScript that is not running.
+
+## What users will notice
+
+- Calls now ring with the app killed, and from the lock screen.
+- The caller sees "Ringing…" once the callee's device has actually been reached, and a reason when a call ends.
+- A second call rings over a live one instead of being silently rejected.
+- Missed calls appear as their own notifications.
+
+## Checklist
+
+- [ ] Calls SDK 5.1, Push SDK 2.0 core **and** VoIP packages installed; CallKeep and VoIP-push libraries removed
+- [ ] `CometChatVoipNotifications.init()` is the first `await` on startup, and the chat app passes it **no** config
+- [ ] `registerBackgroundCallTask()` called at module scope in `index.js`
+- [ ] All `CometChat.addCallListener` call handling, app-side ringtones and busy checks deleted
+- [ ] Caller listens on the prepared session for `onParticipantConnected` / `onParticipantJoined`
+- [ ] `onCallAccepted` subscribed before the first call can arrive
+- [ ] Hang-up uses `session.leave()`, cancel uses `endCall(sessionId, 'cancelled')`
+- [ ] Logout unregisters push first, then clears the identity, then logs out
+- [ ] `ensureRingReady()` checked after sign-in; `NSMicrophoneUsageDescription` present
+- [ ] Staging or dedicated host passed to the Push SDK as well
diff --git a/calls/react-native/migration-guide-v5.mdx b/calls/react-native/migration-guide-v5.mdx
index dbc226733..ac9e31556 100644
--- a/calls/react-native/migration-guide-v5.mdx
+++ b/calls/react-native/migration-guide-v5.mdx
@@ -228,7 +228,7 @@ CometChatCalls.unmuteAudio(); // unmute
| v4 Method | v5 Method |
|---|---|
-| `CometChatCalls.endSession()` | `CometChatCalls.leaveSession()` |
+| `CometChatCalls.endSession()` | `session.leave()` |
| `CometChatCalls.switchCamera()` | `CometChatCalls.switchCamera()` |
| `CometChatCalls.muteAudio(true)` | `CometChatCalls.muteAudio()` |
| `CometChatCalls.muteAudio(false)` | `CometChatCalls.unmuteAudio()` |
@@ -355,6 +355,21 @@ CometChatCalls.addEventListener("onCallLayoutChanged", (layoutType) => { });
| `CometChatCalls.CallSettingsBuilder` | Pass plain `SessionSettings` object to `` |
| `CometChatCalls.OngoingCallListener` | `CometChatCalls.addEventListener()` |
| `CometChatCalls.startSession()` | Render `` (no `joinSession` in React Native) |
-| `CometChatCalls.endSession()` | `CometChatCalls.leaveSession()` |
+| `CometChatCalls.endSession()` | `session.leave()` |
| `RTCRecordingInfo` | `onRecordingStarted` / `onRecordingStopped` events |
| `CallSwitchRequestInfo` | *Removed (no replacement)* |
+
+---
+
+## 5.0 to 5.1
+
+5.1 moves ringing onto the Calls SDK and the Push Notifications SDK, and deprecates two more things:
+
+| Deprecated in 5.1 | Replacement |
+|-------------------|-------------|
+| `CometChatCalls.leaveSession()` | `session.leave()` |
+| Chat SDK calling: `CometChat.initiateCall`, `acceptCall`, `rejectCall`, `endCall`, `getActiveCall`, `clearActiveCall`, `addCallListener`, `CallListener` | `CometChatCalls.initiateCall`, `endCall(sessionId, status)`, the Push Notifications SDK's VoIP package |
+
+New in 5.1: `CometChatCalls.initiateCall`, `endCall(sessionId, status)`, `connect`, `getActiveSession`, `addLoginListener` / `LoginListener`, and `onParticipantConnected` on a prepared session.
+
+The step-by-step move from Chat SDK signalling is on [Migrating from Legacy Ringing](/calls/react-native/migrating-from-legacy-ringing).
diff --git a/calls/react-native/ringing.mdx b/calls/react-native/ringing.mdx
index 14f08d44c..3853f99aa 100644
--- a/calls/react-native/ringing.mdx
+++ b/calls/react-native/ringing.mdx
@@ -2,456 +2,385 @@
title: "Ringing"
sidebarTitle: "Ringing"
sdk_version: "5.x"
-description: "Configure CometChat Calls SDK v5 ringing on React Native for incoming calls, outgoing calls, call alerts, and accept or reject flows."
+description: "Ring users on React Native with the CometChat Calls SDK and the Push Notifications SDK: start a call, show it ringing, answer, decline, cancel and end it, with the app in any state."
---
-
- **Quick Reference for AI Agents & Developers**
+Ringing lets one user call another and lets the callee answer or decline, whether the callee's app is in the foreground, in the background or not running at all.
- ```tsx theme={null}
- import { CometChat } from '@cometchat/chat-sdk-react-native';
- import { CometChatCalls } from '@cometchat/calls-sdk-react-native';
+Two SDKs do the work:
- // Chat SDK = signaling. Calls SDK = media.
- const call = new CometChat.Call(uid, CometChat.CALL_TYPE.VIDEO, CometChat.RECEIVER_TYPE.USER);
- await CometChat.initiateCall(call); // ring them
+| SDK | Owns |
+|-----|------|
+| **Calls SDK** (`@cometchat/calls-sdk-react-native` 5.1+) | Creating the call, the media session, answering, declining, cancelling, ending, and the call events your code listens to |
+| **Push Notifications SDK, VoIP package** (`@cometchat/push-notifications-voip-react-native` 2.0+) | Ringing the callee's device: the call notification, the full-screen incoming-call screen on Android, CallKit on iOS, the ringtone, the ring timeout, missed calls and busy handling |
- CometChat.addCallListener('LISTENER_ID', new CometChat.CallListener({
- onIncomingCallReceived: (call) => { /* show your accept/reject UI */ },
- }));
+
+**Coming from the earlier ringing flow?** The Chat SDK's calling methods (`CometChat.initiateCall`, `acceptCall`, `rejectCall`, `endCall`, `CallListener`) are deprecated and are replaced by what this page describes. See [Migrating from legacy ringing](/calls/react-native/migrating-from-legacy-ringing) for a method-by-method mapping.
+
- const accepted = await CometChat.acceptCall(sessionId); // on accept
- const { token } = await CometChatCalls.generateToken(accepted.getSessionId());
- // → render
- ```
+## How ringing works
+
+The caller creates the call with the Calls SDK. The CometChat backend sends a push to the callee. The Push SDK rings the callee's device, and when the callee answers, your call screen joins the session. The caller's app learns that the callee is ringing and has answered from the Calls SDK, without joining the session first.
+
+```mermaid
+sequenceDiagram
+ participant A as Caller app
+ participant BE as CometChat backend
+ participant P as Callee · Push SDK
+ participant C as Callee · Calls SDK
+
+ A->>BE: CometChatCalls.initiateCall(receiverId, options)
+ Note over A: Session prepared, not joined ("Calling…")
+ BE-->>P: Push (callAction = initiated)
+ P->>P: Ring: notification, full-screen screen / CallKit, ringtone
+ P->>C: connect(sessionId) — pre-warms the session
+ C-->>A: onParticipantConnected → "Ringing…"
+ P->>P: Answer → microphone/camera prompt → onCallAccepted
+ C->>C: Mount CometChatCalls.Component with the call token
+ C-->>A: onParticipantJoined → caller joins too
+ Note over A,C: In call. Hang up with session.leave()
+```
- * **Two teardowns**: `CometChat.removeCallListener(id)` **and** the Calls-SDK listener + `leaveSession()`
- * Unanswered calls cancel after **45s** by default — `initiateCall(call, timeoutInSeconds)` to change it
- * ⚠️ This rings the callee **only while their app is open** — waking a closed app needs [VoIP Calling](/calls/react-native/voip-calling)
- * Testing ringing needs **two live clients** on separate devices
-
+What each side sees:
-Implement incoming and outgoing call notifications using the CometChat Chat SDK's calling features. The Calls SDK handles the actual call session, while the Chat SDK manages call signaling.
+| Stage | Caller | Callee |
+|-------|--------|--------|
+| Call created | `initiateCall` resolves; show "Calling…" | Push arrives; the Push SDK rings |
+| Callee's device reached | `onParticipantConnected` → show "Ringing…" | Ringing |
+| Callee answers | `onParticipantJoined` → mount the call component | `onCallAccepted` → mount the call component |
+| Callee declines / is busy / does not answer | `onCallEnded` with reason `rejected` / `busy` / `unanswered` | Ring stops; missed call if unanswered |
+| Caller cancels | — (you ended it) | Ring stops; missed call |
+| Either side hangs up | `onParticipantLeft` / `onSessionLeft` | same |
+
+## Prerequisites
+
+- [Calls SDK set up](/calls/react-native/setup) and the user [logged in](/calls/react-native/authentication).
+- Push notifications configured with the Push Notifications SDK 2.0. The whole push side of ringing is on the [Push ringing](/calls/react-native/voip-calling) page; this page assumes it is done.
+- Microphone and camera permissions declared: `RECORD_AUDIO` and `CAMERA` in `AndroidManifest.xml`, `NSMicrophoneUsageDescription` and `NSCameraUsageDescription` in `Info.plist`. The SDKs request them at runtime but do not declare them.
+- `minSdkVersion 26` on Android, iOS 13+.
+
+```json package.json
+{
+ "dependencies": {
+ "@cometchat/calls-sdk-react-native": "^5.1.0",
+ "@cometchat/push-notifications-react-native": "^2.0.0",
+ "@cometchat/push-notifications-voip-react-native": "^2.0.0"
+ }
+}
+```
+
+
+Send call pushes at **high** FCM priority. At normal priority the call still rings, but Android refuses the Push SDK's foreground service, so the process can be killed between the ring and the answer.
+
-Ringing functionality requires the CometChat Chat SDK (`@cometchat/chat-sdk-react-native`) for call signaling. The Calls SDK is used for the actual call session.
+**A missing `NSMicrophoneUsageDescription` crashes the app mid-ring on iOS.** The system terminates the process the moment the SDK touches the microphone, which looks exactly like the ring expiring on its own. Declare both usage strings before testing.
-## Prerequisites
+## Start a call
-1. CometChat Chat SDK integrated
-2. CometChat Calls SDK integrated
-3. Push notifications configured (for background calls)
+`CometChatCalls.initiateCall` creates the call on the backend, which rings the callee, and prepares the caller's media session. The caller does **not** join yet.
-## Initiate a Call
+```ts
+import { CometChatCalls } from '@cometchat/calls-sdk-react-native';
-Use the Chat SDK to initiate a call:
+const result = await CometChatCalls.initiateCall(receiverUid, {
+ type: 'audio', // or 'video'
+ receiverType: 'user',
+ sessionSettings: { sessionType: 'voice' }, // 'video' for a video call
+});
-```tsx
-import { CometChat } from '@cometchat/chat-sdk-react-native';
-
-async function initiateCall(receiverId: string, callType: string, receiverType: string) {
- const call = new CometChat.Call(receiverId, callType, receiverType);
-
- try {
- const outgoingCall = await CometChat.initiateCall(call);
- console.log('Call initiated:', outgoingCall);
- return outgoingCall;
- } catch (error) {
- console.error('Error initiating call:', error);
- throw error;
- }
+if (result.isSuccess) {
+ const call = result.value;
+ console.log(`Calling ${call.receiver}, session ${call.sessionId}`);
+ showOutgoingScreen(call);
+ listenOnPreparedSession(call);
+} else {
+ console.error('Could not start the call:', result.error);
}
+```
-// Initiate a video call to a user
-initiateCall('user_uid', CometChat.CALL_TYPE.VIDEO, CometChat.RECEIVER_TYPE.USER);
+| Parameter | Description |
+|-----------|-------------|
+| `receiverUid` | UID of the user to call |
+| `options.type` | `'audio'` or `'video'` |
+| `options.receiverType` | `'user'` |
+| `options.sessionSettings` | The settings the caller's session is prepared with. See the warning below. |
-// Initiate an audio call to a group
-initiateCall('group_guid', CometChat.CALL_TYPE.AUDIO, CometChat.RECEIVER_TYPE.GROUP);
-```
+`InitiatedCall` gives you `sessionId`, `messageId` (the id of the call message in the conversation), `sender`, `receiver`, `receiverType`, `type`, `status`, `initiatedAt`, `conversationId` and `session` — the session already being prepared for this call.
-By default, an unanswered call automatically cancels after 45 seconds. You can customize this duration by passing an optional `timeout` parameter (in seconds) as the second argument to `initiateCall()`.
+
+**Always set the session type in `sessionSettings`.** The Calls SDK freezes the session settings when it prepares the session and its default is video, so an audio call started without `sessionType: 'voice'` opens the caller's camera. The settings you pass here are the ones the caller later joins with.
+
-```tsx
-import { CometChat } from '@cometchat/chat-sdk-react-native';
+
+`initiateCall` resolves while the prepare is still in flight (`session.prepareStatus === 'pending'`). Await `session.settled` or read `session.prepareStatus` later for the outcome. **A failed prepare does not fail the call** — by then the call exists on the server and the callee is already ringing — and mounting the call component still works, falling back to a normal direct join.
+
-// Initiate a video call to a user with a 60-second timeout
-const userCall = new CometChat.Call('user_uid', CometChat.CALL_TYPE.VIDEO, CometChat.RECEIVER_TYPE.USER);
+
+There is no client-side ring timeout. Both sides stop at the ring window the backend sends in the push (45 seconds by default). A caller that wants its own screen to time out can end the call as `unanswered` after the same window, as shown under [Ring timeout](#ring-timeout).
+
-CometChat.initiateCall(userCall, 60).then(
- (outgoingCall) => {
- console.log('Call initiated with 60s timeout:', outgoingCall);
- },
- (error) => {
- console.error('Call initiation failed:', error);
- }
-);
+## Show "Ringing…" and detect the answer
-// Initiate an audio call to a group with a 60-second timeout
-const groupCall = new CometChat.Call('group_guid', CometChat.CALL_TYPE.AUDIO, CometChat.RECEIVER_TYPE.GROUP);
+The caller stays on the session that `initiateCall` prepared and listens to it. Do not join yet: joining only when the callee has joined keeps the caller's media off until there is someone to talk to, and it is what makes the two events below arrive.
-CometChat.initiateCall(groupCall, 60).then(
- (outgoingCall) => {
- console.log('Call initiated with 60s timeout:', outgoingCall);
- },
- (error) => {
- console.error('Call initiation failed:', error);
- }
-);
+```ts
+function listenOnPreparedSession(call) {
+ const session = CometChatCalls.getActiveSession();
+ if (!session) return;
+ const me = CometChatCalls.getLoggedInUser()?.uid;
+
+ const offConnected = session.addEventListener('onParticipantConnected', participant => {
+ // The callee's device received the push and is ringing.
+ if (participant.uid === me) return;
+ setStatus('Ringing…');
+ });
+
+ const offJoined = session.addEventListener('onParticipantJoined', participant => {
+ // The callee answered: now the caller joins.
+ if (participant.uid === me) return;
+ joinCall(call.sessionId);
+ });
+
+ // The prepared session closed before anyone joined: the call is over.
+ const offClosed = session.addEventListener('onConnectionClosed', closeOutgoingScreen);
+ const offTimedOut = session.addEventListener('onSessionTimedOut', closeOutgoingScreen);
+
+ return () => [offConnected, offJoined, offClosed, offTimedOut].forEach(off => off());
+}
```
-| Parameter | Type | Description |
-| ---------- | -------- | --------------------------------------------------------------------------------------------------------------- |
-| `call` | `Call` | The call object created with receiver ID, call type, and receiver type. |
-| `timeout` | `number` | Optional. The ringing duration in seconds before an unanswered call is automatically cancelled. Default: `45`. |
+| Event | On the prepared session it means |
+|-------|----------------------------------|
+| `onParticipantConnected(callee)` | The callee's phone got the push and the Push SDK pre-warmed the session: it is ringing there |
+| `onParticipantJoined(callee)` | The callee answered. Join now |
+| `onConnectionClosed` / `onSessionTimedOut` | The call ended before anyone joined (declined, cancelled from another device, or the session died) |
-## Listen for Incoming Calls
+`addEventListener` returns an unsubscribe function. Call it when your screen unmounts; listeners survive the session otherwise.
-Register a call listener to receive incoming calls:
+
+**A prepared session has no roster.** A ring-only connection receives `onParticipantLeft` but never `onParticipantJoined` for participants who were already there, because there are none. `onParticipantConnected` is the only caller-side signal that the callee's device has been reached.
+
-```tsx
-import { CometChat } from '@cometchat/chat-sdk-react-native';
-
-const listenerId = 'unique_listener_id';
-
-CometChat.addCallListener(
- listenerId,
- new CometChat.CallListener({
- onIncomingCallReceived: (call) => {
- console.log('Incoming call:', call);
- // Show incoming call UI
- },
- onOutgoingCallAccepted: (call) => {
- console.log('Call accepted:', call);
- // Start the call session
- },
- onOutgoingCallRejected: (call) => {
- console.log('Call rejected:', call);
- // Handle rejection
- },
- onIncomingCallCancelled: (call) => {
- console.log('Call cancelled:', call);
- // Dismiss incoming call UI
- },
- onCallEndedMessageReceived: (call) => {
- console.log('Call ended:', call);
- // Handle call end
- },
- })
-);
-
-// Remove listener when done
-CometChat.removeCallListener(listenerId);
+
+**The caller only shows "Ringing…" when the call was delivered by push.** A call delivered over the websocket to an app that is already in the foreground makes no pre-join on the callee's side, so no `onParticipantConnected` ever reaches the caller and the screen stays on "Calling…".
+
+
+## Listen for calls starting and ending
+
+On React Native the Push SDK — not the Calls SDK — is where an incoming call announces itself and its end. It fires even when nothing of yours was running when the push arrived, because it buffers natively and drains the buffer at `init()`.
+
+```ts
+import { CometChatVoipNotifications } from '@cometchat/push-notifications-voip-react-native';
+
+// A call for this user is ringing. The Push SDK already shows the ring;
+// use this for your own state (badge, analytics, refreshing a list).
+const offRinging = CometChatVoipNotifications.onCallRinging(call => {
+ console.log('ringing', call.sessionId, 'from', call.callerUid);
+});
+
+// The user answered — open your call screen.
+const offAccepted = CometChatVoipNotifications.onCallAccepted(call => {
+ openCallScreen(call.sessionId, call.callType);
+});
+
+// The call ended. On the caller: "rejected", "busy", "cancelled" or "unanswered".
+const offEnded = CometChatVoipNotifications.onCallEnded(event => {
+ if (event.sessionId === currentSessionId) closeOutgoingScreen(event.reason);
+});
+
+// The ring itself stopped, whatever happened to the call.
+const offRingEnded = CometChatVoipNotifications.onRingEnded(event => {
+ dismissIncomingUi(event.sessionId);
+});
+
+// When the screen goes away:
+[offRinging, offAccepted, offEnded, offRingEnded].forEach(off => off());
```
-## Accept a Call
+| Who | `onCallEnded` reason |
+|-----|----------------------|
+| Caller | `rejected` (callee declined), `busy` (callee was in another call), `unanswered` (nobody answered), `cancelled` (you cancelled from another device) |
+| Callee, still ringing | `cancelled` (caller gave up), `ongoing` (answered on another device), `rejected` / `busy` (declined here), `unanswered` (ring timed out) |
+| Callee, answered | Nothing; the joined session reports the end through `onSessionLeft` / `onConnectionClosed` |
+
+
+Each `on…` method returns an unsubscribe function, and subscribing late is safe: a call that was answered before your component mounted is held and delivered to the first subscriber.
+
+
+## Answer a call
-Accept an incoming call:
+The Push SDK rings and, on **Answer**, asks for the microphone (and the camera for a video call), brings your app to the front and emits `onCallAccepted`. There is no separate "accept" call to make.
```tsx
-async function acceptCall(sessionId: string) {
- try {
- const call = await CometChat.acceptCall(sessionId);
- console.log('Call accepted:', call);
- // Start the call session with Calls SDK
- return call;
- } catch (error) {
- console.error('Error accepting call:', error);
- throw error;
- }
+function App() {
+ const [activeCall, setActiveCall] = useState(null);
+
+ useEffect(() => CometChatVoipNotifications.onCallAccepted(call => {
+ setActiveCall(call); // call.sessionId, callerUid, callerName, callType
+ }), []);
+
+ if (activeCall) return ;
+ return ;
}
```
-## Reject a Call
+`PNCallInfo` carries `sessionId`, `callerUid`, `callerName`, `callerAvatar`, `callType`, `receiverId`, `receiverType`, `conversationId`, `source` and `expiresAtMs`.
-Reject an incoming call:
+
+**Every field of a call payload arrives from Android as a string.** `isPeerCall` is `"true"` / `"false"`, not a boolean, and `expiresAtMs` is a string of digits. Compare against strings or coerce before use.
+
+
+
+**On iOS, a programmatic answer from a cold or background launch is refused by CallKit.** The user's own tap on the CallKit screen is the only thing that can answer a call in that state, which also means automated cold-launch answer tests need a real tap.
+
+
+## Join the session
+
+Both sides join the same way: mount the Calls SDK's call component with the token of the prepared session. Because the session was prepared while ringing, the join is near-instant.
```tsx
-async function rejectCall(sessionId: string, status: string) {
- try {
- const call = await CometChat.rejectCall(sessionId, status);
- console.log('Call rejected:', call);
- return call;
- } catch (error) {
- console.error('Error rejecting call:', error);
- throw error;
- }
+import { CometChatCalls } from '@cometchat/calls-sdk-react-native';
+
+function CallScreen({ sessionId }) {
+ const [callToken, setCallToken] = useState(null);
+
+ useEffect(() => {
+ (async () => {
+ // Hands back the session already prepared for this call, without minting a second token.
+ const session = await CometChatCalls.connect(sessionId, { sessionType: 'voice' });
+ setCallToken(session.callToken);
+ })();
+ }, [sessionId]);
+
+ if (!callToken) return ;
+ return goBack()} />;
}
+```
+
+See [Join Session](/calls/react-native/join-session) for the component, [Session Settings](/calls/react-native/session-settings) for layouts and controls, and [Events](/calls/react-native/events) for everything the joined session reports.
+
+## Decline a call
-// Reject with status
-rejectCall(sessionId, CometChat.CALL_STATUS.REJECTED);
+Declining is handled by the Push SDK: the Decline button on the notification, on its full-screen incoming-call screen and on CallKit sends `endCall(sessionId, 'rejected')` through a queue that survives the process dying, and sends `'busy'` instead when the user is already in another call. The caller receives `onCallEnded` with that reason.
-// Mark as busy
-rejectCall(sessionId, CometChat.CALL_STATUS.BUSY);
+You only write decline code when you draw your own incoming-call screen:
+
+```ts
+CometChatVoipNotifications.declineCall(sessionId);
```
-## End a Call
+Without the Push SDK's VoIP package, decline through the Calls SDK directly:
-End an ongoing call:
+```ts
+await CometChatCalls.endCall(sessionId, 'rejected');
+```
-```tsx
-async function endCall(sessionId: string) {
- try {
- // End the call session
- CometChatCalls.leaveSession();
-
- // Notify other participants via Chat SDK
- const call = await CometChat.endCall(sessionId);
- console.log('Call ended:', call);
- return call;
- } catch (error) {
- console.error('Error ending call:', error);
- throw error;
- }
+
+**Android can decline from a fully killed app.** Register the background task so the decline survives the process going away:
+
+```ts
+import { registerBackgroundCallTask } from '@cometchat/push-notifications-voip-react-native';
+registerBackgroundCallTask(); // in index.js, at module scope
+```
+
+
+## Cancel a call
+
+The caller ends a call that is still ringing with `endCall` and the `cancelled` status. The callee's ring stops and a missed call is posted there.
+
+```ts
+async function cancelOutgoingCall(sessionId: string) {
+ const result = await CometChatCalls.endCall(sessionId, 'cancelled');
+ if (!result.isSuccess) console.error('Cancel failed:', result.error);
+ closeOutgoingScreen(); // close locally either way
}
```
-## Start Call Session After Accept
+Disable the End button after the first tap, and ignore an `onParticipantJoined` that arrives after you cancelled: the cancel wins.
-After accepting a call, start the Calls SDK session:
+## Ring timeout
-```tsx
-import { CometChatCalls } from '@cometchat/calls-sdk-react-native';
+The callee's Push SDK ends an unanswered 1:1 call as `unanswered` when its ring window closes (45 seconds unless the push carries a `ringTimeout`). To close the caller's screen at the same moment even if that signal is late, run your own timer from `initiateCall` and end the call with the same status:
-async function startCallSession(call: any) {
- const sessionId = call.getSessionId();
-
- try {
- // Generate token for the session
- const { token } = await CometChatCalls.generateToken(sessionId);
-
- // Create session settings
- const isAudioOnly = call.getType() === CometChat.CALL_TYPE.AUDIO;
-
- const sessionSettings = {
- sessionType: isAudioOnly ? 'VOICE' : 'VIDEO',
- } as const;
-
- // Subscribe to session events; addEventListener returns an unsubscribe function
- const unsubscribe = CometChatCalls.addEventListener('onSessionLeft', () => {
- console.log('Call ended');
- });
-
- return { token, sessionSettings, unsubscribe };
- } catch (error) {
- console.error('Error starting call session:', error);
- throw error;
+```ts
+const timer = setTimeout(async () => {
+ if (!answered) {
+ await CometChatCalls.endCall(sessionId, 'unanswered');
+ closeOutgoingScreen();
}
-}
+}, 45_000);
+
+// clear it on answer, cancel and unmount
```
-## Complete Example
+## End a call
-```tsx
-import React, { useState, useEffect, useCallback } from 'react';
-import { View, Text, TouchableOpacity, StyleSheet, Modal } from 'react-native';
-import { CometChat } from '@cometchat/chat-sdk-react-native';
-import { CometChatCalls } from '@cometchat/calls-sdk-react-native';
+Hang up by leaving the session. The room tells the other side, which receives `onParticipantLeft` and then `onSessionLeft` or `onConnectionClosed`. No status needs to be sent and the call is logged by the backend.
-interface Call {
- getSessionId: () => string;
- getType: () => string;
- getSender: () => { getName: () => string };
+```ts
+async function hangUp() {
+ await CometChatCalls.getActiveSession()?.leave();
+ goBack();
}
+```
-function CallManager({ children }: { children: React.ReactNode }) {
- const [incomingCall, setIncomingCall] = useState(null);
- const [activeCall, setActiveCall] = useState<{
- token: string;
- sessionSettings: any;
- } | null>(null);
+The other participant:
- useEffect(() => {
- const listenerId = 'call_manager_listener';
-
- CometChat.addCallListener(
- listenerId,
- new CometChat.CallListener({
- onIncomingCallReceived: (call: Call) => {
- setIncomingCall(call);
- },
- onOutgoingCallAccepted: async (call: Call) => {
- await startSession(call);
- },
- onOutgoingCallRejected: () => {
- setActiveCall(null);
- },
- onIncomingCallCancelled: () => {
- setIncomingCall(null);
- },
- onCallEndedMessageReceived: () => {
- setActiveCall(null);
- setIncomingCall(null);
- },
- })
- );
-
- return () => {
- CometChat.removeCallListener(listenerId);
- };
- }, []);
-
- const startSession = async (call: Call) => {
- try {
- const sessionId = call.getSessionId();
- const { token } = await CometChatCalls.generateToken(sessionId);
-
- const isAudioOnly = call.getType() === CometChat.CALL_TYPE.AUDIO;
-
- const sessionSettings = {
- sessionType: isAudioOnly ? 'VOICE' : 'VIDEO',
- } as const;
-
- // Clear active call when the session is left
- CometChatCalls.addEventListener('onSessionLeft', () => {
- setActiveCall(null);
- });
-
- setActiveCall({ token, sessionSettings });
- setIncomingCall(null);
- } catch (error) {
- console.error('Error starting session:', error);
- }
- };
-
- const handleAccept = async () => {
- if (!incomingCall) return;
-
- try {
- const call = await CometChat.acceptCall(incomingCall.getSessionId());
- await startSession(call);
- } catch (error) {
- console.error('Error accepting call:', error);
- }
- };
-
- const handleReject = async () => {
- if (!incomingCall) return;
-
- try {
- await CometChat.rejectCall(
- incomingCall.getSessionId(),
- CometChat.CALL_STATUS.REJECTED
- );
- setIncomingCall(null);
- } catch (error) {
- console.error('Error rejecting call:', error);
- }
- };
-
- const handleEndCall = async () => {
- if (!activeCall) return;
-
- CometChatCalls.leaveSession();
- setActiveCall(null);
- };
-
- return (
-
- {children}
-
- {/* Incoming Call Modal */}
-
-
-
-
- {incomingCall?.getSender().getName()}
-
-
- Incoming {incomingCall?.getType()} call
-
-
-
- Decline
-
-
- Accept
-
-
-
-
-
-
- {/* Active Call */}
- {activeCall && (
-
-
-
-
-
- )}
-
- );
-}
+```ts
+const session = CometChatCalls.getActiveSession();
+session?.addEventListener('onSessionLeft', goBack);
+session?.addEventListener('onConnectionClosed', goBack);
+```
-const styles = StyleSheet.create({
- container: {
- flex: 1,
- },
- modalOverlay: {
- flex: 1,
- backgroundColor: 'rgba(0, 0, 0, 0.8)',
- justifyContent: 'center',
- alignItems: 'center',
- },
- incomingCallCard: {
- backgroundColor: '#1a1a1a',
- borderRadius: 20,
- padding: 30,
- alignItems: 'center',
- width: '80%',
- },
- callerName: {
- color: '#fff',
- fontSize: 24,
- fontWeight: '600',
- marginBottom: 8,
- },
- callType: {
- color: '#999',
- fontSize: 16,
- marginBottom: 30,
- },
- callActions: {
- flexDirection: 'row',
- gap: 20,
- },
- callButton: {
- paddingHorizontal: 30,
- paddingVertical: 15,
- borderRadius: 30,
- },
- rejectButton: {
- backgroundColor: '#ff4444',
- },
- acceptButton: {
- backgroundColor: '#22c55e',
- },
- buttonText: {
- color: '#fff',
- fontSize: 16,
- fontWeight: '600',
- },
- callContainer: {
- flex: 1,
- backgroundColor: '#000',
- },
-});
+
+`leaveSession()` is deprecated in 5.1; use `session.leave()`. Leave only the session your screen owns: after answering a second call over a live one, the first screen's clean-up must not leave the new session.
+
+
+
+`leave()` takes the Calls SDK through a re-initialisation. A `connect()` issued inside that window never resolves, so wait for the leave to settle before preparing the next session — which is what the Push SDK does for you when a second call is answered over a live one.
+
-export default CallManager;
+## Second call while in a call
+
+Handled by the Push SDK, the same way a phone does it. A call that arrives while the user is in another call rings over it with "Answering ends your current call". **Answer** leaves the live call, prepares the new one and emits `onCallAccepted` again for the new session — open a fresh screen for it. **Decline** sends `busy` and keeps the live call. A call that arrives while another is already ringing, or while the user's own outgoing call is ringing, is declined as `busy` without ringing.
+
+## Group calls
+
+`initiateCall` is for 1:1 calls. A group call is a session every member joins with a session id you share with them (for example in a custom message). If a group call rings through the push, declining it or letting it ring out sends nothing: the call carries on for the other members, and the decline is local to that device.
+
+## Logout
+
+Log out of the Calls SDK when the user signs out (`CometChatCalls.logout`, or `CometChatUIKit.logout` when you use the UI Kit), and unregister push **before** the session goes away:
+
+```ts
+await CometChatPushNotifications.unregister(); // needs the credential; must come first
+CometChatVoipNotifications.clearIdentity();
+await CometChatUIKit.logout();
```
-## Related Documentation
+`unregister()` deletes the token from the backend and needs the logged-in credential, so after `logout()` there is nothing left to authenticate the delete. Skipping it leaves the device receiving the departed user's call pushes.
+
+
+The Push SDK keeps the ring identity in step with the Calls SDK's login listener: logging in writes the uid, logging out clears it. An app that logs the **Chat** SDK in and out without `CometChatCalls.login` / `logout` must call `clearIdentity()` itself, as above.
+
+
+## Call status values
+
+| Status | Meaning | Sent by |
+|--------|---------|---------|
+| `initiated` | Created, ringing | Backend, on `initiateCall` |
+| `ongoing` | Answered | Backend, when the callee joins |
+| `rejected` | Callee declined | Callee's Push SDK |
+| `busy` | Callee was in another call | Callee's Push SDK |
+| `cancelled` | Caller ended it before an answer | Caller, `endCall` |
+| `unanswered` | Nobody answered in the ring window | Callee's Push SDK (and optionally the caller's timer) |
+| `ended` | Hung up after being answered | Backend, when the session ends |
+| `missed` | How an unanswered or cancelled incoming call appears in call logs | Call logs |
+
+## Related
-- [VoIP Calling](/calls/react-native/voip-calling) - Push notifications for calls
-- [Join Session](/calls/react-native/join-session) - Start call sessions
-- [Background Handling](/calls/react-native/background-handling) - Handle background calls
+- [Push ringing](/calls/react-native/voip-calling): dependencies, `PNConfiguration`, the Firebase and PushKit setup, your own incoming-call screen, readiness checks and what happens when the app is killed.
+- [Migrating from legacy ringing](/calls/react-native/migrating-from-legacy-ringing).
+- [Events](/calls/react-native/events), [Actions](/calls/react-native/actions), [Call Logs](/calls/react-native/call-logs).
diff --git a/calls/react-native/setup.mdx b/calls/react-native/setup.mdx
index e6168a986..dba989b19 100644
--- a/calls/react-native/setup.mdx
+++ b/calls/react-native/setup.mdx
@@ -23,7 +23,7 @@ Use this Calls SDK directly only if you need custom call UI or advanced control.
```bash theme={null}
# The SDK plus its six REQUIRED peer dependencies — versions are pinned; do not float them
- npm install @cometchat/calls-sdk-react-native@5.0.5
+ npm install @cometchat/calls-sdk-react-native@5.1.0
npm install @react-native-async-storage/async-storage@^2.1.2 react-native-background-timer@^2.4.1 \
react-native-performance@^5.1.2 react-native-svg@^15.12.0 react-native-url-polyfill@2.0.0 \
react-native-webrtc@124.0.7
@@ -51,15 +51,28 @@ This guide walks you through installing the CometChat Calls SDK and configuring
### Using npm
```bash
-npm install @cometchat/calls-sdk-react-native@5.0.5
+npm install @cometchat/calls-sdk-react-native@5.1.0
```
### Using Yarn
```bash
-yarn add @cometchat/calls-sdk-react-native@5.0.5
+yarn add @cometchat/calls-sdk-react-native@5.1.0
```
+
+ **Ringing users?** Incoming calls ring through the Push Notifications SDK's VoIP package. Install
+ `@cometchat/push-notifications-react-native` and `@cometchat/push-notifications-voip-react-native`
+ as well, and follow [Push Ringing](/calls/react-native/voip-calling). The Calls SDK alone can start,
+ join and end calls, but it cannot ring a device that is not running your app.
+
+
+
+ **Initialise once, with the final hosts.** The SDK keeps the hosts from its first `init()`; a later
+ `init()` with different hosts is ignored for the rest of the process. If you use the CometChat UI Kit,
+ it initialises the Calls SDK for you from `UIKitSettings`, so do not call `init()` again yourself.
+
+
## Install Required Dependencies
The Calls SDK relies on the following packages. Install them alongside the SDK:
diff --git a/calls/react-native/troubleshooting.mdx b/calls/react-native/troubleshooting.mdx
index 23d7685dd..5f27c7872 100644
--- a/calls/react-native/troubleshooting.mdx
+++ b/calls/react-native/troubleshooting.mdx
@@ -62,3 +62,18 @@ sdk_version: "5.x"
| --- | --- | --- |
| PiP not working on iOS | iOS version below 15 or missing entitlement | Ensure iOS 15+ and PiP background mode is enabled |
| PiP not working on Android | Missing `supportsPictureInPicture` in manifest | Add PiP activity attribute in `AndroidManifest.xml`. See [PiP](/calls/react-native/picture-in-picture) |
+
+---
+
+## Ringing
+
+| Symptom | Cause | Fix |
+| --- | --- | --- |
+| Nothing rings in any app state | `@cometchat/push-notifications-voip-react-native` not installed, or `CometChatVoipNotifications.init()` never called | Install it and initialise it; `getDiagnostics()` reports `voip: false` when it is missing |
+| Rings in the foreground only | The app awaited something before `init()`, so the cold-launch ring window was spent | Make `CometChatVoipNotifications.init()` the first `await` in your entry point |
+| Caller never shows "Ringing…" | The caller joined right after `initiateCall` instead of listening on the prepared session, or the call was delivered over the websocket rather than by push | Listen for `onParticipantConnected` on `getActiveSession()` before joining |
+| Chat options silently lost after adding calling | A config was passed to `CometChatVoipNotifications.init()` in an app that also calls the core `init` | Pass the config to the core only; call the VoIP `init()` with no arguments |
+| Incoming calls stop after switching users | The ring identity is stale | Check `hasLoggedInUid` in diagnostics; log out through the Calls SDK or call `clearIdentity()` |
+| Answering a second call never connects | The new session was prepared before the live one was released | The Push SDK handles this; if you join yourself, wait for `leave()` to settle first |
+| iOS: the app dies mid-ring | `NSMicrophoneUsageDescription` is missing | Declare it; check the device crash reports before assuming a ring-timeout bug |
+| Two ringtones or two screens | Legacy app-side ringing still in place | Remove the `CallListener` ring path — see [Migrating from Legacy Ringing](/calls/react-native/migrating-from-legacy-ringing) |
diff --git a/calls/react-native/voip-calling.mdx b/calls/react-native/voip-calling.mdx
index 24cae7ae4..c607558f9 100644
--- a/calls/react-native/voip-calling.mdx
+++ b/calls/react-native/voip-calling.mdx
@@ -1,348 +1,475 @@
---
-title: "VoIP Calling"
-sidebarTitle: "VoIP Calling"
+title: "Push Ringing"
+sidebarTitle: "Push Ringing (VoIP)"
sdk_version: "5.x"
-description: "Set up CometChat Calls SDK v5 VoIP calling on React Native for call notifications, incoming call handling, and app wake flows."
+description: "Ring incoming CometChat calls on React Native with the Push Notifications SDK 2.0: FCM and PushKit setup, configuration, answering, permissions, busy and missed calls, your own ring screen, and behaviour when the app is killed."
---
-
- **Quick Reference for AI Agents & Developers**
+The Push Notifications SDK's VoIP package turns a CometChat call push into a ringing phone: on Android a call-style notification with Answer and Decline, a full-screen screen over the lock screen and the ringtone; on iOS a CallKit call. It works with the app in the foreground, in the background or not running at all.
+
+This page covers the push side of ringing. Starting, answering and ending calls with the Calls SDK is on the [Ringing](/calls/react-native/ringing) page.
+
+```mermaid
+flowchart LR
+ FCM["FCM data push (Android) PushKit (iOS)"] --> H["Push SDK native layer"]
+ H --> D{Push SDK decides}
+ D -->|ring| R["Notification + full-screen screen / CallKit"]
+ D -->|in another call| W[Rings over the live call]
+ D -->|another call ringing| B[Declined as busy]
+ R -->|Answer| P[Mic / camera prompt]
+ P --> A["onCallAccepted → your call screen"]
+ R -->|Decline| Q["endCall(rejected), queued"]
+ R -->|45 s| T["endCall(unanswered) + missed call"]
+```
- ```tsx theme={null}
- import { Platform } from 'react-native';
- import { CometChatNotifications } from '@cometchat/chat-sdk-react-native';
+## Prerequisites
- const { PushPlatforms } = CometChatNotifications;
+- [Calls SDK 5.1 set up](/calls/react-native/setup) and the user logged in.
+- **Android**: a Firebase project with your app, `google-services.json` in `android/app/`, and `@react-native-firebase/app` + `@react-native-firebase/messaging` installed.
+- **iOS**: a VoIP Services certificate uploaded to 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.
+- `minSdkVersion 26` on Android, iOS 13+.
- // Register AFTER login. iOS VoIP is its OWN platform, distinct from ordinary iOS push.
- // providerId is the Provider ID from Dashboard → Notifications.
- await CometChatNotifications.registerPushToken(
- token,
- Platform.OS === 'ios' ? PushPlatforms.APNS_REACT_NATIVE_VOIP
- : PushPlatforms.FCM_REACT_NATIVE_ANDROID,
- PROVIDER_ID,
- );
+## Step 1: Add the dependencies
+
+```bash
+npm install @cometchat/push-notifications-react-native \
+ @cometchat/push-notifications-voip-react-native \
+ @cometchat/calls-sdk-react-native
+cd ios && pod install
+```
- await CometChatNotifications.unregisterPushToken(); // on logout
- ```
-
- * **Not code-only.** iOS needs a VoIP `.p12` certificate + a native PushKit/CallKit module; Android needs Firebase + a self-managed `ConnectionService`
- * On iOS you **must** call `reportNewIncomingCall` for every VoIP push — the OS terminates an app that does not
- * Skipping the logout unregister leaves the next user on that device receiving the previous user's calls
- * **Prerequisite** → [Ringing](/calls/react-native/ringing) covers the foreground half
-
-
-Implement VoIP push notifications to receive incoming calls even when your app is in the background or terminated. This requires platform-specific configuration for iOS CallKit and Android ConnectionService.
-
-## iOS VoIP Configuration
-
-### Enable VoIP Push Notifications
-
-1. In Xcode, select your target
-2. Go to **Signing & Capabilities**
-3. Add **Push Notifications** capability
-4. Add **Background Modes** capability
-5. Enable **Voice over IP**
-
-### Create VoIP Certificate
-
-1. Go to [Apple Developer Portal](https://developer.apple.com)
-2. Navigate to **Certificates, Identifiers & Profiles**
-3. Create a new **VoIP Services Certificate**
-4. Download and install the certificate
-5. Export the `.p12` file for your server
-
-### Configure CometChat Dashboard
-
-1. Go to your CometChat Dashboard
-2. Navigate to **Notifications > Push Notifications**
-3. Upload your VoIP certificate (`.p12` file)
-4. Configure the certificate password
-
-### Implement CallKit
-
-Create a native module to handle CallKit:
-
-```swift
-// ios/CallKitManager.swift
-import CallKit
-import PushKit
-
-@objc(CallKitManager)
-class CallKitManager: NSObject, CXProviderDelegate, PKPushRegistryDelegate {
-
- static let shared = CallKitManager()
-
- private let provider: CXProvider
- private let callController = CXCallController()
- private var voipRegistry: PKPushRegistry?
-
- override init() {
- let config = CXProviderConfiguration()
- config.supportsVideo = true
- config.maximumCallsPerCallGroup = 1
- config.supportedHandleTypes = [.generic]
-
- provider = CXProvider(configuration: config)
- super.init()
- provider.setDelegate(self, queue: nil)
- }
-
- @objc func registerForVoIPPushes() {
- voipRegistry = PKPushRegistry(queue: .main)
- voipRegistry?.delegate = self
- voipRegistry?.desiredPushTypes = [.voIP]
- }
-
- // PKPushRegistryDelegate
- func pushRegistry(_ registry: PKPushRegistry, didUpdate pushCredentials: PKPushCredentials, for type: PKPushType) {
- let token = pushCredentials.token.map { String(format: "%02x", $0) }.joined()
- // Send token to CometChat
- NotificationCenter.default.post(
- name: NSNotification.Name("VoIPTokenReceived"),
- object: nil,
- userInfo: ["token": token]
- )
- }
-
- func pushRegistry(_ registry: PKPushRegistry, didReceiveIncomingPushWith payload: PKPushPayload, for type: PKPushType, completion: @escaping () -> Void) {
- guard type == .voIP else { return }
-
- let callerId = payload.dictionaryPayload["callerId"] as? String ?? "Unknown"
- let callerName = payload.dictionaryPayload["callerName"] as? String ?? "Unknown"
- let sessionId = payload.dictionaryPayload["sessionId"] as? String ?? ""
- let hasVideo = payload.dictionaryPayload["hasVideo"] as? Bool ?? false
-
- reportIncomingCall(
- uuid: UUID(),
- handle: callerId,
- callerName: callerName,
- hasVideo: hasVideo
- ) { error in
- completion()
- }
- }
-
- func reportIncomingCall(uuid: UUID, handle: String, callerName: String, hasVideo: Bool, completion: @escaping (Error?) -> Void) {
- let update = CXCallUpdate()
- update.remoteHandle = CXHandle(type: .generic, value: handle)
- update.localizedCallerName = callerName
- update.hasVideo = hasVideo
-
- provider.reportNewIncomingCall(with: uuid, update: update) { error in
- completion(error)
- }
- }
-
- // CXProviderDelegate
- func providerDidReset(_ provider: CXProvider) {}
-
- func provider(_ provider: CXProvider, perform action: CXAnswerCallAction) {
- // Notify React Native to accept the call
- NotificationCenter.default.post(
- name: NSNotification.Name("CallKitAnswerCall"),
- object: nil,
- userInfo: ["callUUID": action.callUUID.uuidString]
- )
- action.fulfill()
- }
-
- func provider(_ provider: CXProvider, perform action: CXEndCallAction) {
- // Notify React Native to end the call
- NotificationCenter.default.post(
- name: NSNotification.Name("CallKitEndCall"),
- object: nil,
- userInfo: ["callUUID": action.callUUID.uuidString]
- )
- action.fulfill()
- }
+```json package.json
+{
+ "dependencies": {
+ "@cometchat/calls-sdk-react-native": "^5.1.0",
+ "@cometchat/push-notifications-react-native": "^2.0.0",
+ "@cometchat/push-notifications-voip-react-native": "^2.0.0"
+ }
}
```
-## Android VoIP Configuration
+Installing the VoIP package is the whole opt-in: the core package detects it and installs the ringer. The Chat SDK is optional for both packages; a calls-only app works.
+
+
+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.
+
+
+## Step 2: Initialise once
-### Add Firebase Cloud Messaging
+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.
-1. Add Firebase to your Android project
-2. Add the FCM dependency to `android/app/build.gradle`:
+There are two shapes, and which one you use matters.
-```groovy
-dependencies {
- implementation 'com.google.firebase:firebase-messaging:23.0.0'
-}
-```
+**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:
-### Configure CometChat Dashboard
-
-1. Go to your CometChat Dashboard
-2. Navigate to **Notifications > Push Notifications**
-3. Upload your Firebase Server Key
-
-### Implement ConnectionService
-
-Create a ConnectionService for incoming calls:
-
-```java
-// android/app/src/main/java/com/yourapp/CallConnectionService.java
-package com.yourapp;
-
-import android.telecom.Connection;
-import android.telecom.ConnectionRequest;
-import android.telecom.ConnectionService;
-import android.telecom.PhoneAccountHandle;
-import android.telecom.TelecomManager;
-
-public class CallConnectionService extends ConnectionService {
-
- @Override
- public Connection onCreateIncomingConnection(
- PhoneAccountHandle connectionManagerPhoneAccount,
- ConnectionRequest request) {
-
- CallConnection connection = new CallConnection();
- connection.setConnectionProperties(Connection.PROPERTY_SELF_MANAGED);
- connection.setCallerDisplayName(
- request.getExtras().getString("callerName"),
- TelecomManager.PRESENTATION_ALLOWED
- );
- connection.setRinging();
-
- return connection;
- }
-
- @Override
- public Connection onCreateOutgoingConnection(
- PhoneAccountHandle connectionManagerPhoneAccount,
- ConnectionRequest request) {
-
- CallConnection connection = new CallConnection();
- connection.setConnectionProperties(Connection.PROPERTY_SELF_MANAGED);
- connection.setDialing();
-
- return connection;
- }
-}
+```ts
+import { CometChatVoipNotifications } from '@cometchat/push-notifications-voip-react-native';
+
+await CometChatVoipNotifications.init({
+ appId: APP_ID,
+ region: REGION,
+ fcmProviderId: FCM_PROVIDER_ID,
+ apnsProviderId: APNS_PROVIDER_ID,
+ callKitName: 'Acme',
+});
```
-### Register ConnectionService
+**An app that also does chat** initialises the core with its own settings first, then calls the VoIP `init()` with **no arguments**:
+
+```ts
+import { CometChatPushNotifications } from '@cometchat/push-notifications-react-native';
+import { CometChatVoipNotifications } from '@cometchat/push-notifications-voip-react-native';
-Add to `AndroidManifest.xml`:
+await CometChatPushNotifications.init({
+ appId: APP_ID,
+ region: REGION,
+ fcmProviderId: FCM_PROVIDER_ID,
+ apnsProviderId: APNS_PROVIDER_ID,
+ notificationSmallIcon: 'ic_notification',
+ androidChannelId: 'chat_messages',
+ callKitName: 'Acme',
+});
-```xml
-
-
-
-
-
+await CometChatVoipNotifications.init(); // no config
```
-### Add Permissions
+
+**Passing a config to `CometChatVoipNotifications.init()` when the core is already initialised re-runs that initialisation and drops the chat options you set.** A chat app that calls `init(config)` on the VoIP package silently loses `notificationSmallIcon`, `androidChannelId` and every other chat-side setting.
+
+
+
+**`CometChatVoipNotifications.init()` must be the first thing you `await` on startup.** Anything awaited before it — a stored-credentials read, a chat login, a remote config fetch — eats the cold-launch ring window, and a call that woke the process is lost before the ring is wired up.
+
+
+| Option | Default | What it does |
+|--------|---------|--------------|
+| `appId`, `region` | none | Your CometChat app; required unless the Chat SDK already initialised |
+| `fcmProviderId` / `apnsProviderId` | none | The dashboard providers the token is registered under |
+| `callMode` | `'AUTO'` | `'AUTO'` rings. `'MANUAL'` only parses and forwards; see [MANUAL mode](#manual-mode) |
+| `notificationSmallIcon` | none | Status-bar icon for every notification (Android) |
+| `androidChannelId` / `androidChannelName` | SDK defaults | The channel chat notifications are posted on |
+| `defaultRingTimeoutMs` | `45000` | Ring window when the push carries no `ringTimeout` |
+| `showMissedCallNotification` | `true` | Post a missed-call notification |
+| `ringInForeground` | `true` | Ring even when your app is in the foreground |
+| `showInForeground` | `false` | Show chat notifications while the app is in the foreground |
+| `callKitName` | app name | The name CallKit shows in the system call log (iOS) |
+| `callKitIconName`, `callKitRingtone`, `callKitSupportsVideo`, `callKitIncludeInRecents` | SDK defaults | CallKit appearance and behaviour (iOS) |
+| `clientHost` | none | Same value as the Chat SDK's host override; see [Staging deployments](#staging-or-dedicated-deployments) |
+| `enableBadgeCount`, `sendDeliveryReceipts` | `true` | Chat notification behaviour |
+| `tokenRegistrar`, `authToken` | auto | Token registration; leave the defaults |
+
+## Step 3: Register the background task
+
+Android can decline or time out a call while your app is fully killed. Register the background task at module scope in `index.js`, before `AppRegistry.registerComponent`:
+
+```js index.js
+import { registerBackgroundCallTask } from '@cometchat/push-notifications-voip-react-native';
+
+registerBackgroundCallTask();
+```
+
+Without it, a decline tapped on a killed app is never delivered and the caller rings on to `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.
-```xml
-
-
+Delegate only if your app already owns a `setBackgroundMessageHandler`:
+
+```js index.js
+import messaging from '@react-native-firebase/messaging';
+import { CometChatPushNotifications } from '@cometchat/push-notifications-react-native';
+
+messaging().setBackgroundMessageHandler(async remoteMessage => {
+ // A background handler is a fresh JS runtime with none of your app's state:
+ // initialise CometChat here before handling.
+ const handled = await CometChatPushNotifications.handlePushNotification(
+ remoteMessage.data ?? {},
+ remoteMessage.sentTime,
+ );
+ if (!handled) { /* not a CometChat push: your own handling */ }
+});
```
-## Register Push Token
+
+**Pass `sentTime` and hand over every push.** Firebase queues pushes while a device dozes; without the sent time a push delivered late looks fresh and rings for a call that is already over. And do not skip call pushes when the app is in the foreground: the push is the only signal that a call is coming in, in every app state.
+
-Register the VoIP/FCM token with CometChat using `CometChatNotifications.registerPushToken`. iOS VoIP is
-its own platform — `APNS_REACT_NATIVE_VOIP`, distinct from the `APNS_REACT_NATIVE_DEVICE` used for ordinary
-message notifications — so an app that both rings and notifies registers **two** tokens.
+
+**Send call pushes at high FCM priority.** Android lets a background app start a foreground service only with the temporary allowlist that a high-priority data message grants. At normal priority the call still rings, but the process can be killed between the ring and the answer.
+
+
+
+**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, asks for the microphone (and the camera for a video call), brings your app to the front and emits `onCallAccepted`. There is nothing to "accept".
```tsx
-import { CometChatNotifications } from '@cometchat/chat-sdk-react-native';
-
-const { PushPlatforms } = CometChatNotifications;
-
-async function registerPushToken(token: string, platform: 'ios' | 'android') {
- try {
- await CometChatNotifications.registerPushToken(
- token,
- platform === 'ios'
- ? PushPlatforms.APNS_REACT_NATIVE_VOIP // the PushKit token
- : PushPlatforms.FCM_REACT_NATIVE_ANDROID,
- PROVIDER_ID, // Dashboard → Notifications → Provider ID
- );
- console.log('Push token registered');
- } catch (error) {
- console.error('Error registering push token:', error);
- }
+function App() {
+ const [activeCall, setActiveCall] = useState(null);
+
+ useEffect(() => {
+ const off = CometChatVoipNotifications.onCallAccepted(call => setActiveCall(call));
+ const offEnded = CometChatVoipNotifications.onCallEnded(() => setActiveCall(null));
+ return () => { off(); offEnded(); };
+ }, []);
+
+ if (activeCall) return ;
+ return ;
}
```
-
- Register the token **after** the user logs in — a token registered with no logged-in user is attached to
- nobody. Re-register when the token refreshes.
+`PNCallInfo` fields: `sessionId`, `callerUid`, `callerName`, `callerAvatar`, `callType`, `receiverId`, `receiverType`, `conversationId`, `initiatedAtMs`, `expiresAtMs`, `isPeerCall` and `source` (`push` or `websocket`). The ring window is anchored to the caller's `initiatedAtMs`, not to a timer on this device, so both sides stop ringing together however late the push arrived; prefer `expiresAtMs` to a timer of your own.
+
+
+**Every field arrives from Android as a string**, including `isPeerCall` (`"true"` / `"false"`) and `expiresAtMs`. Coerce before use.
+
- `providerId` is the **Provider ID** you configured in **Dashboard → Notifications → Push Notifications**
- (for example `APNS_PROVIDER_ID`). Pass it so the token is bound to the right provider.
+
+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.
-### Unregister on Logout
+## Step 6: Permissions and ring readiness
-Release the token when the user logs out, or the next user on that device receives their calls:
+Declare in the manifest and `Info.plist` what the SDKs only request:
-```tsx
-import { CometChatNotifications } from '@cometchat/chat-sdk-react-native';
+```xml android/app/src/main/AndroidManifest.xml
+
+
+
+```
-async function unregisterPushToken() {
- try {
- await CometChatNotifications.unregisterPushToken();
- } catch (error) {
- console.error('Error unregistering push token:', error);
- }
+```xml ios/YourApp/Info.plist
+NSMicrophoneUsageDescription
+Used for voice and video calls.
+NSCameraUsageDescription
+Used for video calls.
+UIBackgroundModes
+voipaudio
+```
+
+Everything else (full-screen intent, foreground service of type `phoneCall`, wake lock, vibrate, the ring activity and the ring service on Android; CallKit and PushKit on iOS) comes from the packages' own manifests.
+
+
+**A missing `NSMicrophoneUsageDescription` terminates the app mid-ring on iOS.** It looks exactly like the ring expiring early. Check the device's crash reports before treating a short ring as an SDK bug.
+
+
+**Microphone and camera.** The SDK asks at the moment of answering: the answer is recorded first so the ring stops, the system prompt appears, and your screen opens whatever the user chose. A denied microphone joins muted and never fails the call. Ask earlier as well (at sign-in, for example) with `CometChatPNHelper.requestCallPermissions()` so the first call is not where the user meets the prompt.
+
+**Ring readiness.** Several device settings silence a call with no error anywhere. Check once after sign-in and tell the user:
+
+```ts
+const readiness = await CometChatPushNotifications.ensureRingReady();
+if (readiness !== 'ready') {
+ showBanner(`Incoming calls may not ring: ${readiness}`);
+ await CometChatPushNotifications.openRingSettings();
}
```
-## Handle Incoming VoIP Push
+| Result | Meaning |
+|--------|---------|
+| `ready` | Nothing in the way |
+| `notificationsOff` | The app cannot post notifications |
+| `channelMuted` | The user muted or downgraded the incoming-call channel |
+| `fullScreenIntentRevoked` | Android 14+ revoked full-screen intents: no ring screen on a locked device, only the heads-up notification |
+| `dndBlocking` | Do Not Disturb is filtering |
+| `overlayDenied` | The ring screen cannot be drawn over other apps |
+| `batteryOptimised` | The process may be frozen before the push arrives |
+| `autostartDisabled` | An OEM autostart restriction blocks the push wake |
+| `voipBackgroundModeMissing` | iOS: the `voip` background mode is not declared |
+| `voipTokenMissing` | iOS: PushKit never issued a token, so no call push can arrive |
+| `microphoneDenied` | The user refused the microphone at an answer; calls still ring but join muted |
+
+**Diagnostics for a bug report.** `getDiagnostics()` returns a redacted snapshot (whether a provider id exists, never what it is; never a token or payload):
+
+| Key | Read it as |
+|-----|------------|
+| `voip` | `false`: the VoIP package was never initialised, nothing will ring |
+| `hasLoggedInUid` | `false`: the ring gates have no identity, and a user's own outgoing call can ring their own phone |
+| `competingMessagingService` | non-`null`: another service is resolving ahead of the SDK's |
+| `canUseFullScreenIntent` | `false` on Android 14+: no ring screen on a locked device |
+| `callChannelImportance` | below `IMPORTANCE_HIGH`: no heads-up and no sound |
+| `interruptionFilter` | anything but `1`: Do Not Disturb is filtering |
+| `ringReadiness`, `ignoringBatteryOptimizations`, `canDrawOverlays` | Why a push may never reach the device |
+| `callMode`, `ringInForeground`, `defaultRingTimeoutMs` | `MANUAL` explains a phone that never rings by design |
+| `aggressiveOem`, `manufacturer`, `model`, `sdkInt`, `platform` | OEM background restrictions |
+
+`verifyPushSetup()` registers the token and reports `ok`, `tokenNotRegistered`, `notLoggedIn`, `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.
+
+| What happened | On the callee's device | What the caller learns |
+|---------------|------------------------|------------------------|
+| The user answers | Ring stops, `onCallAccepted` | `onParticipantJoined` |
+| The user declines | Ring stops; `endCall(sessionId, 'rejected')` is queued and retried across process death | `onCallEnded` with `rejected` |
+| The user declines while in another call | Same, with `busy` | `busy` |
+| The caller cancels | Ring stops on the `cancelled` push, or sooner when the prepared session closes; missed-call notification | Nothing; the caller ended it |
+| Nobody answers a 1:1 call | At the ring window the ring stops, a missed call is posted and `endCall(sessionId, 'unanswered')` is sent | `unanswered` |
+| Answered on another device | Ring stops on the `ongoing` push, no missed call | `onParticipantJoined` |
+| Nobody answers a group call | Ring stops, missed call posted, **nothing sent**; the call keeps running | Nothing |
+| The user logs out while ringing | Ring stops, nothing sent, no missed call | Rings to its own window, then `unanswered` |
+
+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.
+
+| Situation | Behaviour |
+|-----------|-----------|
+| Call while in a call | Rings over the live call with the normal ring; the text adds "Answering ends your current call". The live call is untouched |
+| Answer (End & accept) | Leaves the live call, waits for the Calls SDK to release it, prepares the new session, emits `onCallAccepted` for it |
+| Decline | `busy` to the caller; the live call continues |
+| No action | `unanswered` to the caller, missed call; the live call continues |
+| Call while another one is already ringing here | Declined as `busy` without ringing, missed call. The first keeps ringing |
+| Call while the user's own outgoing call is ringing | Declined as `busy` without ringing, missed call |
+
+
+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; a `cancelled`, `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; a call is declined as busy without ringing.
+
+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](/calls/react-native/call-logs).
+
+## When the app is killed
+
+This is the case the push exists for.
+
+1. The high-priority data push (Android) or the PushKit push (iOS) starts the app process.
+2. The native layer finds no in-memory configuration and restores it from disk.
+3. The ring runs on a foreground service of type `phoneCall` on Android, so the system keeps the process alive through the answer; on iOS CallKit holds the call. An alarm backs up the ring timeout.
+4. On Answer, the SDK asks for permissions, launches your app and buffers the answered call until your JavaScript subscribes.
+5. A decline or timeout is sent through the background task, so it goes out even if the process dies right after the tap.
+
+Call `CometChatVoipNotifications.init()` as the first `await` in your app's entry point, from stored credentials, so the answer finds a ready SDK.
+
+
+**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.
+
+```ts
+await CometChatVoipNotifications.init({
+ appId: APP_ID,
+ region: REGION,
+ clientHost: `${APP_ID}.apiclient-us.cometchat-staging.com/v3.0`,
+});
+```
+
+
+**The host override shapes differ between SDKs.** The Chat SDK wants a bare host, the Calls SDK a full URL, and giving one the other's shape hangs login forever rather than failing. The Calls SDK also has three base URLs — admin, client and calls — and setting only one splits call start and call end across environments.
+
+
+## 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; it unregisters on logout.
+
+Unregister **before** the session goes away:
+
+```ts
+await CometChatPushNotifications.unregister(); // needs the credential; must come first
+CometChatVoipNotifications.clearIdentity();
+await CometChatUIKit.logout();
+```
+
+`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.
+
+
+
+In a calls-only app, `logout()` needs the Chat SDK — so use `clearIdentity()` and `unregister()` instead, and call `init()` again before the next login or the device stays silent.
+
+
+## Your own incoming-call screen
+
+Subscribe to `onCallRinging` 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.
```tsx
-import { useEffect } from 'react';
-import { NativeEventEmitter, NativeModules, Platform } from 'react-native';
-import { CometChat } from '@cometchat/chat-sdk-react-native';
-import { CometChatCalls } from '@cometchat/calls-sdk-react-native';
+function IncomingCallOverlay() {
+ const [call, setCall] = useState(null);
-function useVoIPPush() {
useEffect(() => {
- if (Platform.OS === 'ios') {
- const eventEmitter = new NativeEventEmitter(NativeModules.CallKitManager);
-
- const answerSubscription = eventEmitter.addListener(
- 'CallKitAnswerCall',
- async (data) => {
- // Accept the call via Chat SDK
- const sessionId = data.sessionId;
- await CometChat.acceptCall(sessionId);
-
- // Start the call session
- const { token } = await CometChatCalls.generateToken(sessionId);
- // Navigate to call screen with token
- }
- );
-
- const endSubscription = eventEmitter.addListener(
- 'CallKitEndCall',
- async (data) => {
- CometChatCalls.leaveSession();
- }
- );
-
- return () => {
- answerSubscription.remove();
- endSubscription.remove();
- };
- }
+ const off = CometChatVoipNotifications.onCallRinging(setCall);
+ // Close when the ring ends elsewhere: caller cancelled, answered on another device, timed out.
+ const offEnd = CometChatVoipNotifications.onRingEnded(e => {
+ setCall(c => (c?.sessionId === e.sessionId ? null : c));
+ });
+ return () => { off(); offEnd(); };
}, []);
+
+ if (!call) return null;
+
+ return (
+
+ {call.callerName}
+
+ );
}
+```
-export default useVoIPPush;
+Tell the SDK when your screen is up and down, so it does not also show its own and so it knows a ring is being presented:
+
+```ts
+CometChatVoipNotifications.notifyCallScreenOpen(sessionId);
+CometChatVoipNotifications.notifyCallScreenClosed(sessionId);
```
-## Related Documentation
+Without `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, so the UI Kit's theme cannot reach it, and what you can change from the app is limited.
+
+
+**Colours on the Android ring screen are not currently overridable.** They are compiled into the screen, so defining `cometchat_pn_call_*` colours in your app has no effect. Text and the call icon can be overridden, as below.
+
+
+**Android — what you can override**, by declaring the same resource name in `android/app/src/main/res/` (an app's resource wins over the library's):
+
+- **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 other `cometchat_pn_*` strings — this is also how you localise the ring.
+- **Drawable**: `cometchat_pn_ic_call`.
+
+```xml android/app/src/main/res/values/strings.xml
+Incoming call
+```
+
+**iOS — CallKit owns the screen.** `callKitName`, `callKitIconName`, `callKitRingtone`, `callKitSupportsVideo` and `callKitIncludeInRecents` on `PNConfiguration` are the whole surface; the system draws the rest and it cannot be themed.
+
+**If you need the ring to match your brand**, draw it yourself with `ringInForeground: false` and your own component on `onCallRinging` — see [Your own incoming-call screen](#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:
+
+```ts
+CometChatVoipNotifications.showIncomingCall(payload); // post the ring, arm expiry
+CometChatVoipNotifications.keepAlive(payload); // foreground service while ringing
+CometChatVoipNotifications.showMissedCall(payload);
+CometChatVoipNotifications.endCallUI(sessionId);
+```
+
+
+In MANUAL mode with nothing built, the call is silently lost: no notification, no ringtone, no screen, nothing sent to the caller, who rings out to `unanswered`. Use MANUAL only if you are rebuilding the whole ring.
+
+
+## Reporting a call you placed yourself
+
+An outgoing call made from your own screen can be reported so the system call UI and the call log know about it:
+
+```ts
+CometChatVoipNotifications.reportOutgoingCall(sessionId, calleeName, hasVideo);
+CometChatVoipNotifications.reportCallConnected(sessionId);
+CometChatVoipNotifications.reportCallEnded(sessionId, 'ended');
+```
-- [Ringing](/calls/react-native/ringing) - Implement call notifications
-- [Background Handling](/calls/react-native/background-handling) - Keep calls active in background
-- [Setup](/calls/react-native/setup) - Initial SDK setup
+## Troubleshooting
+
+| Symptom | Check |
+|---------|-------|
+| No push arrives | Package name matches the Firebase app baked into the build; `google-services.json` present and for the right project; provider id set; the user is logged in (the token registers on login); `clientHost` matches the Chat and Calls SDKs |
+| Push arrives, nothing rings | The VoIP package was initialised (`voip: true` in diagnostics); `ensureRingReady()` is `ready`; the push was not older than its ring window |
+| Rings but no full-screen screen on a locked phone | `fullScreenIntentRevoked` on Android 14+: send the user to the settings page with `openRingSettings()` |
+| Rings, then the process dies before Answer | Call pushes sent at normal priority; send at high priority |
+| Answer opens the app but nothing happens | No `onCallAccepted` subscriber, or the app awaited something else before `init()` |
+| Answer joins fail with "app id does not exist" | Staging or dedicated host not given to the Push SDK |
+| Caller never sees "Ringing…" | The caller joined right after `initiateCall` instead of listening on the prepared session, or the call was delivered over the websocket rather than by push |
+| Incoming calls stopped after a user switch | The ring identity is stale; check `hasLoggedInUid` and that logout calls `clearIdentity()` |
+| Ring keeps playing after the user logged out | The app logged out of the Chat SDK only; also log out of the Calls SDK |
+| Two ringtones or two screens | Legacy app-side ringing (`CallListener` overlay, own sound) still in place; remove it |
+
+## Related
+
+- [Ringing](/calls/react-native/ringing): starting, answering and ending calls with the Calls SDK.
+- [Migrating from legacy ringing](/calls/react-native/migrating-from-legacy-ringing).
+- [Background Handling](/calls/react-native/background-handling).
+- [React Native Push Notifications](/notifications/react-native-push-notifications-android): chat notifications with the same SDK.
diff --git a/docs.json b/docs.json
index 96952f214..639d0500e 100644
--- a/docs.json
+++ b/docs.json
@@ -5486,7 +5486,8 @@
{
"group": "Migration Guide",
"pages": [
- "calls/react-native/migration-guide-v5"
+ "calls/react-native/migration-guide-v5",
+ "calls/react-native/migrating-from-legacy-ringing"
]
},
{
diff --git a/notifications/react-native-push-notifications-android.mdx b/notifications/react-native-push-notifications-android.mdx
index 1a2f951dd..72cc5dccf 100644
--- a/notifications/react-native-push-notifications-android.mdx
+++ b/notifications/react-native-push-notifications-android.mdx
@@ -8,7 +8,7 @@ description: "CometChat push notifications and VoIP calls in React Native apps o
| Field | Value |
| --- | --- |
| Platform | Android (FCM) |
-| Package | `@cometchat/push-notifications-react-native` |
+| Package | `@cometchat/push-notifications-react-native` — plus `@cometchat/push-notifications-voip-react-native` if the app has calling |
| Key APIs | `CometChatPushNotifications.init()`, `onNotificationTap()`, `onCallAccepted()`, `onCallEnded()`, `onMessageReceived()`, `unregister()`, `registerBackgroundCallTask()`, `CometChatPNHelper.requestNotificationPermission()` |
| Push Platform | `FCM_REACT_NATIVE_ANDROID`, registered by `init()` with `fcmProviderId` |
| Native setup | `google-services.json` + Google Services plugin, `minSdkVersion 24`, an `ic_notification` drawable. No manifest or Kotlin changes |
@@ -91,6 +91,8 @@ export const AppCredentials = {
```bash
npm install @cometchat/push-notifications-react-native
+# plus, if your app has calling:
+npm install @cometchat/push-notifications-voip-react-native
```
@@ -107,7 +109,7 @@ The JavaScript below is the same for Android and iOS — one set of files serves
```js index.js lines
import { AppRegistry } from 'react-native';
-import { registerBackgroundCallTask } from '@cometchat/push-notifications-react-native';
+import { registerBackgroundCallTask } from '@cometchat/push-notifications-voip-react-native';
import App from './App';
import { name as appName } from './app.json';
@@ -221,7 +223,7 @@ export function setupPushOnLogin(): () => void {
apnsProviderId: AppCredentials.apnsProviderId, // iOS (APNs device + VoIP)
notificationSmallIcon: 'ic_notification', // Android status-bar icon
showInForeground: true, // one notification while the app is open, too
- ringInForeground: false, // your app rings while it's open — see src/calls/IncomingCall.tsx
+ ringInForeground: true, // the VoIP package rings while the app is open too
});
};
start().catch(error => console.log('Push setup failed:', error));
@@ -328,55 +330,26 @@ whatever was on screen before — not to the conversation the reply belongs to.
conversation underneath, push it first and then the thread, or use a reset that builds both entries.
-**`src/calls/IncomingCall.tsx`** — `init()` above sets `ringInForeground: false`, so **while the app is open the package doesn't ring: your app must show its own incoming-call screen**, or calls won't ring at all while it's open. Calls reach an open app over the Chat SDK's connection; this component listens for them and shows the UI Kit's `CometChatIncomingCall`, which accepts the call and shows the call screen itself:
+**Incoming calls.** Nothing in this guide rings a call. Since Push Notifications SDK 2.0, ringing is owned by `@cometchat/push-notifications-voip-react-native`, which rings in every app state — foreground, background and killed — and replaces the `CometChat.addCallListener` + `CometChatIncomingCall` pattern earlier versions of this guide described.
-```tsx src/calls/IncomingCall.tsx lines
-import React, { useEffect, useState } from 'react';
-import { CometChat } from '@cometchat/chat-sdk-react-native';
-import { CometChatIncomingCall, CometChatUIEventHandler } from '@cometchat/chat-uikit-react-native';
-
-const LISTENER_ID = 'incoming-call';
-
-/**
- * Rings for a call while the app is open — init() sets ringInForeground: false, so the
- * package leaves this to the app. CometChatIncomingCall accepts the call and shows the call
- * screen itself; this component shows it when a call arrives and removes it when the call is
- * declined, cancelled by the caller, or ends.
- */
-export function IncomingCall() {
- const [call, setCall] = useState(null);
-
- useEffect(() => {
- CometChat.addCallListener(
- LISTENER_ID,
- new CometChat.CallListener({
- onIncomingCallReceived: (incoming: CometChat.Call) => setCall(incoming),
- onIncomingCallCancelled: () => setCall(null), // the caller hung up while it was ringing
- }),
- );
- // An accepted call ended.
- CometChatUIEventHandler.addCallListener(LISTENER_ID, { ccCallEnded: () => setCall(null) });
+```bash
+npm install @cometchat/push-notifications-voip-react-native
+```
- return () => {
- CometChat.removeCallListener(LISTENER_ID);
- CometChatUIEventHandler.removeCallListener(LISTENER_ID);
- };
- }, []);
+```ts
+import { CometChatVoipNotifications } from '@cometchat/push-notifications-voip-react-native';
- if (!call) return null;
- return setCall(null)} />;
-}
+await CometChatVoipNotifications.init(); // AFTER the core init, with NO config
+CometChatVoipNotifications.onCallAccepted(call => navigate(SCREENS.ongoingCall, call));
```
-
-Your app already shows an incoming-call screen while it's open? Keep it and skip this file. Don't want one? Set `ringInForeground: true` — the default — and skip this file: the package then rings with the system call UI while the app is open, too.
-
+
+Passing a config to `CometChatVoipNotifications.init()` in a chat app re-runs the core initialisation and drops every chat option set above, including `notificationSmallIcon` and the channel settings. Only a calls-only app passes its config there.
+
-
-Not using the UI Kit? Delete the `@cometchat/chat-uikit-react-native` import and the `emitMessageEvent` block in `markConversationRead`, point `SCREENS` and the route params at your own screens, and in `logout.ts` call `CometChat.logout()` instead of `CometChatUIKit.logout()`. For calls while the app is open, set `ringInForeground: true`, or build your own incoming-call screen on `CometChat.addCallListener` in place of `IncomingCall.tsx`.
-
+See [Push Ringing](/calls/react-native/voip-calling) for the whole calling setup, [Ringing](/calls/react-native/ringing) for placing and answering calls, and [Migrating from Legacy Ringing](/calls/react-native/migrating-from-legacy-ringing) if your app still uses `CometChat.addCallListener`.
-**`App.tsx`** — call `usePushOnLogin()` once, in a component that renders **after** CometChat is initialized, pass `navigationRef` to your `NavigationContainer`, and render `` **before** your navigator:
+**`App.tsx`** — call `usePushOnLogin()` once, in a component that renders **after** CometChat is initialized, pass `navigationRef` to your `NavigationContainer`:
```tsx App.tsx lines
import React, { useEffect, useState } from 'react';
@@ -387,7 +360,6 @@ import { CometChatUIKit, UIKitSettings } from '@cometchat/chat-uikit-react-nativ
import { AppCredentials } from './src/AppCredentials';
import { navigationRef } from './src/navigation/navigationRef';
import { usePushOnLogin } from './src/push/pushNotifications';
-import { IncomingCall } from './src/calls/IncomingCall';
export default function App() {
const [initialized, setInitialized] = useState(false);
@@ -414,14 +386,13 @@ function Root() {
return (
- {/* before your navigator: it shows at the top, and an accepted call fills the screen */}
{/* your existing navigator */}
);
}
```
-`usePushOnLogin()` starts push after a fresh login **and** when a session is restored on launch, and removes the handlers when the user logs out — so a later login never registers them twice. `` goes before your navigator because the UI Kit's incoming-call screen isn't a modal: rendered first, it shows at the top of the screen, and an accepted call fills the screen.
+`usePushOnLogin()` starts push after a fresh login **and** when a session is restored on launch, and removes the handlers when the user logs out — so a later login never registers them twice.
### 3.3 Align dependencies and configuration
@@ -430,7 +401,7 @@ function Root() {
- `fcmProviderId` (Android) and `apnsProviderId` (iOS) — from step 1.
- `notificationSmallIcon` — the Android status-bar icon.
- `showInForeground` (default `false`) — show chat notifications while the app is open.
- - `ringInForeground` (default `true`) — ring with the system call UI while the app is open. With `false`, a call that arrives while the app is open is left to your app, so your app must show its own incoming-call screen — `IncomingCall.tsx` above. With `false` and no such screen, calls don't ring while the app is open.
+ - `ringInForeground` (default `true`) — ring with the system call UI while the app is open. With `false`, a call that arrives while the app is open is left to your app, which must then draw its own incoming-call screen on `CometChatVoipNotifications.onCallRinging`; with `false` and no such screen, calls don't ring while the app is open. See [Your own incoming-call screen](/calls/react-native/voip-calling#your-own-incoming-call-screen).
- `voip` (default `true`) — `false` turns calls off for a chat-only app: no call rings, and iOS doesn't register the VoIP token.
- `androidChannelId`, `androidChannelName` — the Android notification channel.
@@ -610,7 +581,7 @@ await CometChatPushNotifications.registerToken('fcm', token); // or 'apns' / 'vo
| Event | What happens |
| --- | --- |
-| **Call push arrives** | App in the background or killed: the package starts a phone-call foreground service with the full-screen ringing screen, over the lock screen too. App open: it rings the same way unless `ringInForeground` is `false` — then your app's `IncomingCall` screen rings instead. The ring stops itself after 45 seconds if nothing ends it. |
+| **Call push arrives** | App in the background or killed: the package starts a phone-call foreground service with the full-screen ringing screen, over the lock screen too. App open: it rings the same way unless `ringInForeground` is `false` — then ringing is left to your app. The ring stops itself after 45 seconds if nothing ends it. |
| **Accept** | The app opens and the package accepts the call through the Chat SDK; then `onCallAccepted` fires and `openCallScreen` opens your call screen. |
| **Decline** | The package rejects the call through the Chat SDK. In a **fully killed** app the background task registered in `index.js` does it, so the caller sees the call rejected right away. |
| **Caller hangs up** | A cancel push stops the ring and `onCallEnded` fires; `endCall` tears the call down and leaves the call screen. |
@@ -748,7 +719,8 @@ Use physical devices and a **release** build: a debug build loads its JavaScript
| Declining a call in a killed app doesn't reject it | `registerBackgroundCallTask()` is called at module scope in `index.js`, and the app has been opened and logged in once since installing (so `init()` saved the Chat SDK settings). Test on a release build. |
| Token registration errors | The provider IDs match the dashboard exactly, and `usePushOnLogin()` is rendered after CometChat is initialized. |
| No notification while the app is open | Expected with `showInForeground: false` (the default) — set it to `true`. For calls, `ringInForeground` decides whether the system call UI or your in-app screen rings. |
-| A call doesn't ring while the app is open | `ringInForeground` is `false`, so your app must ring: render `` before your navigator (see *Wire the entry points*), or set `ringInForeground: true`. |
+| A call never rings at all, in any state | `@cometchat/push-notifications-voip-react-native` is not installed, or `CometChatVoipNotifications.init()` was never called — `getDiagnostics()` reports `voip: false`. |
+| A call doesn't ring while the app is open | `ringInForeground` is `false`, so your app must ring: draw your own screen on `onCallRinging`, or set `ringInForeground: true`. |
| Tapping a notification opens the app but not the conversation | `navigationRef` is passed to your `NavigationContainer`, navigation goes through `navigate()` from `navigationRef.ts`, and the route names in `SCREENS` match your navigator. |
| Thread reply opens an empty thread screen | The thread screen is given the user or group as well as the parent message, as `openFromNotification` does. |
| Handlers fire twice after logging out and in | Use `usePushOnLogin()` rather than calling `setupPushOnLogin()` directly — its cleanup must run on logout. |
diff --git a/notifications/react-native-push-notifications-ios.mdx b/notifications/react-native-push-notifications-ios.mdx
index 29c84daf5..208d481d6 100644
--- a/notifications/react-native-push-notifications-ios.mdx
+++ b/notifications/react-native-push-notifications-ios.mdx
@@ -8,7 +8,7 @@ description: "CometChat push notifications and VoIP calls in React Native apps o
| Field | Value |
| --- | --- |
| Platform | iOS (APNs + PushKit + CallKit) |
-| Package | `@cometchat/push-notifications-react-native` |
+| Package | `@cometchat/push-notifications-react-native` — plus `@cometchat/push-notifications-voip-react-native` if the app has calling |
| Key APIs | `CometChatPushNotifications.init()`, `onNotificationTap()`, `onCallAccepted()`, `onCallEnded()`, `onMessageReceived()`, `unregister()`, `CometChatPushNotificationsAppDelegate.registerForVoIPPushes()`, `CometChatPushNotificationsAppDelegate.didRegisterAPNsToken(_:)` |
| Push Platforms | `APNS_REACT_NATIVE_DEVICE` and `APNS_REACT_NATIVE_VOIP`, registered by `init()` with one `apnsProviderId` |
| Native setup | Push Notifications + Background Modes (Voice over IP, Remote notifications, Audio), microphone and camera usage strings, and in `AppDelegate`: `registerForVoIPPushes()` before React Native starts plus the APNs token method |
@@ -91,6 +91,8 @@ export const AppCredentials = {
```bash
npm install @cometchat/push-notifications-react-native
+# plus, if your app has calling:
+npm install @cometchat/push-notifications-voip-react-native
cd ios && pod install && cd ..
```
@@ -112,7 +114,7 @@ The JavaScript below is the same for Android and iOS — one set of files serves
```js index.js lines
import { AppRegistry } from 'react-native';
-import { registerBackgroundCallTask } from '@cometchat/push-notifications-react-native';
+import { registerBackgroundCallTask } from '@cometchat/push-notifications-voip-react-native';
import App from './App';
import { name as appName } from './app.json';
@@ -207,7 +209,7 @@ export function setupPushOnLogin(): () => void {
apnsProviderId: AppCredentials.apnsProviderId, // iOS (APNs device + VoIP)
notificationSmallIcon: 'ic_notification', // Android status-bar icon
showInForeground: true, // one notification while the app is open, too
- ringInForeground: false, // your app rings while it's open — see src/calls/IncomingCall.tsx
+ ringInForeground: true, // the VoIP package rings while the app is open too
});
};
start().catch(error => console.log('Push setup failed:', error));
@@ -314,55 +316,31 @@ whatever was on screen before — not to the conversation the reply belongs to.
conversation underneath, push it first and then the thread, or use a reset that builds both entries.
-**`src/calls/IncomingCall.tsx`** — `init()` above sets `ringInForeground: false`, so **while the app is open the package doesn't ring: your app must show its own incoming-call screen**, or calls won't ring at all while it's open. Calls reach an open app over the Chat SDK's connection; this component listens for them and shows the UI Kit's `CometChatIncomingCall`, which accepts the call and shows the call screen itself:
+**Incoming calls.** Nothing in this guide rings a call. Since Push Notifications SDK 2.0, ringing is owned by `@cometchat/push-notifications-voip-react-native`, which presents the call through CallKit in every app state — foreground, background and killed — and replaces the `CometChat.addCallListener` + `CometChatIncomingCall` pattern earlier versions of this guide described.
-```tsx src/calls/IncomingCall.tsx lines
-import React, { useEffect, useState } from 'react';
-import { CometChat } from '@cometchat/chat-sdk-react-native';
-import { CometChatIncomingCall, CometChatUIEventHandler } from '@cometchat/chat-uikit-react-native';
-
-const LISTENER_ID = 'incoming-call';
-
-/**
- * Rings for a call while the app is open — init() sets ringInForeground: false, so the
- * package leaves this to the app. CometChatIncomingCall accepts the call and shows the call
- * screen itself; this component shows it when a call arrives and removes it when the call is
- * declined, cancelled by the caller, or ends.
- */
-export function IncomingCall() {
- const [call, setCall] = useState(null);
-
- useEffect(() => {
- CometChat.addCallListener(
- LISTENER_ID,
- new CometChat.CallListener({
- onIncomingCallReceived: (incoming: CometChat.Call) => setCall(incoming),
- onIncomingCallCancelled: () => setCall(null), // the caller hung up while it was ringing
- }),
- );
- // An accepted call ended.
- CometChatUIEventHandler.addCallListener(LISTENER_ID, { ccCallEnded: () => setCall(null) });
+```bash
+npm install @cometchat/push-notifications-voip-react-native
+cd ios && pod install
+```
- return () => {
- CometChat.removeCallListener(LISTENER_ID);
- CometChatUIEventHandler.removeCallListener(LISTENER_ID);
- };
- }, []);
+```ts
+import { CometChatVoipNotifications } from '@cometchat/push-notifications-voip-react-native';
- if (!call) return null;
- return setCall(null)} />;
-}
+await CometChatVoipNotifications.init(); // AFTER the core init, with NO config
+CometChatVoipNotifications.onCallAccepted(call => navigate(SCREENS.ongoingCall, call));
```
-
-Your app already shows an incoming-call screen while it's open? Keep it and skip this file. Don't want one? Set `ringInForeground: true` — the default — and skip this file: the package then rings with the system call UI while the app is open, too.
-
+
+Passing a config to `CometChatVoipNotifications.init()` in a chat app re-runs the core initialisation and drops every chat option set above. Only a calls-only app passes its config there.
+
+
+
+A missing `NSMicrophoneUsageDescription` terminates the app mid-ring, which looks exactly like the ring expiring on its own. Declare it, and the `voip` background mode, before testing.
+
-
-Not using the UI Kit? Delete the `@cometchat/chat-uikit-react-native` import and the `emitMessageEvent` block in `markConversationRead`, point `SCREENS` and the route params at your own screens, and in `logout.ts` call `CometChat.logout()` instead of `CometChatUIKit.logout()`. For calls while the app is open, set `ringInForeground: true`, or build your own incoming-call screen on `CometChat.addCallListener` in place of `IncomingCall.tsx`.
-
+See [Push Ringing](/calls/react-native/voip-calling) for the whole calling setup, [Ringing](/calls/react-native/ringing) for placing and answering calls, and [Migrating from Legacy Ringing](/calls/react-native/migrating-from-legacy-ringing) if your app still uses `CometChat.addCallListener`.
-**`App.tsx`** — call `usePushOnLogin()` once, in a component that renders **after** CometChat is initialized, pass `navigationRef` to your `NavigationContainer`, and render `` **before** your navigator:
+**`App.tsx`** — call `usePushOnLogin()` once, in a component that renders **after** CometChat is initialized, pass `navigationRef` to your `NavigationContainer`:
```tsx App.tsx lines
import React, { useEffect, useState } from 'react';
@@ -373,7 +351,6 @@ import { CometChatUIKit, UIKitSettings } from '@cometchat/chat-uikit-react-nativ
import { AppCredentials } from './src/AppCredentials';
import { navigationRef } from './src/navigation/navigationRef';
import { usePushOnLogin } from './src/push/pushNotifications';
-import { IncomingCall } from './src/calls/IncomingCall';
export default function App() {
const [initialized, setInitialized] = useState(false);
@@ -400,14 +377,13 @@ function Root() {
return (
- {/* before your navigator: it shows at the top, and an accepted call fills the screen */}
{/* your existing navigator */}
);
}
```
-`usePushOnLogin()` starts push after a fresh login **and** when a session is restored on launch, and removes the handlers when the user logs out — so a later login never registers them twice. `` goes before your navigator because the UI Kit's incoming-call screen isn't a modal: rendered first, it shows at the top of the screen, and an accepted call fills the screen.
+`usePushOnLogin()` starts push after a fresh login **and** when a session is restored on launch, and removes the handlers when the user logs out — so a later login never registers them twice.
### 4.3 Align dependencies and configuration
@@ -416,7 +392,7 @@ function Root() {
- `fcmProviderId` (Android) and `apnsProviderId` (iOS) — from step 1.
- `notificationSmallIcon` — the Android status-bar icon.
- `showInForeground` (default `false`) — show chat notifications while the app is open.
- - `ringInForeground` (default `true`) — ring with the system call UI while the app is open. With `false`, a call that arrives while the app is open is left to your app, so your app must show its own incoming-call screen — `IncomingCall.tsx` above. With `false` and no such screen, calls don't ring while the app is open.
+ - `ringInForeground` (default `true`) — ring with the system call UI while the app is open. With `false`, a call that arrives while the app is open is left to your app, which must then draw its own incoming-call screen on `CometChatVoipNotifications.onCallRinging`; with `false` and no such screen, calls don't ring while the app is open. See [Your own incoming-call screen](/calls/react-native/voip-calling#your-own-incoming-call-screen).
- `voip` (default `true`) — `false` turns calls off for a chat-only app: no call rings, and iOS doesn't register the VoIP token.
- `androidChannelId`, `androidChannelName` — the Android notification channel.
@@ -548,7 +524,7 @@ You don't write PushKit or CallKit code, and you don't set a `UNUserNotification
| Event | What happens |
| --- | --- |
-| **VoIP push arrives** | The package reports the call to CallKit, which rings, in every app state — except while the app is open with `ringInForeground: false`, when your app's `IncomingCall` screen rings instead. Even then the report happens on iOS 26.3 and earlier, where PushKit requires **every** VoIP push to be reported: the package reports the call and ends it immediately, so nothing rings. iOS 26.4 added [`PKVoIPPushMetadata`](https://developer.apple.com/documentation/pushkit/pkvoippushmetadata), which tells the package when a report isn't required — the app is in the foreground, already on a call, or the push arrived too late to be viable — and the package then skips CallKit entirely, so no CallKit banner flashes over your own call screen. |
+| **VoIP push arrives** | The package reports the call to CallKit, which rings, in every app state — except while the app is open with `ringInForeground: false`, when ringing is left to your app. Even then the report happens on iOS 26.3 and earlier, where PushKit requires **every** VoIP push to be reported: the package reports the call and ends it immediately, so nothing rings. iOS 26.4 added [`PKVoIPPushMetadata`](https://developer.apple.com/documentation/pushkit/pkvoippushmetadata), which tells the package when a report isn't required — the app is in the foreground, already on a call, or the push arrived too late to be viable — and the package then skips CallKit entirely, so no CallKit banner flashes over your own call screen. |
| **Accept** | The package puts the audio session in call mode, accepts the call through the Chat SDK, and hands CallKit's audio to WebRTC; then `onCallAccepted` fires and `openCallScreen` opens your call screen. |
| **Decline** | The package rejects the call through the Chat SDK. In a killed app, iOS has already launched the app for the VoIP push, and the package keeps it running long enough to reject. |
| **Caller hangs up** | The cancel VoIP push ends the CallKit call and `onCallEnded` fires. |
@@ -692,7 +668,8 @@ Use a physical iPhone — the Simulator can't receive APNs or VoIP pushes — an
| Accepted call connects but has no audio | The **Audio** background mode is enabled, and the Calls SDK (with `react-native-webrtc`) is installed. |
| Token registration errors | The provider IDs match the dashboard exactly, and `usePushOnLogin()` is rendered after CometChat is initialized. |
| No notification while the app is open | Expected with `showInForeground: false` (the default) — set it to `true`. For calls, `ringInForeground` decides whether the system call UI or your in-app screen rings. |
-| A call doesn't ring while the app is open | `ringInForeground` is `false`, so your app must ring: render `` before your navigator (see *Wire the entry points*), or set `ringInForeground: true`. |
+| A call never rings at all, in any state | `@cometchat/push-notifications-voip-react-native` is not installed, or `CometChatVoipNotifications.init()` was never called — `getDiagnostics()` reports `voip: false`. |
+| A call doesn't ring while the app is open | `ringInForeground` is `false`, so your app must ring: draw your own screen on `onCallRinging`, or set `ringInForeground: true`. |
| Tapping a notification opens the app but not the conversation | `navigationRef` is passed to your `NavigationContainer`, navigation goes through `navigate()` from `navigationRef.ts`, and the route names in `SCREENS` match your navigator. |
| Thread reply opens an empty thread screen | The thread screen is given the user or group as well as the parent message, as `openFromNotification` does. |
| Handlers fire twice after logging out and in | Use `usePushOnLogin()` rather than calling `setupPushOnLogin()` directly — its cleanup must run on logout. |
diff --git a/ui-kit/react-native/calling-integration.mdx b/ui-kit/react-native/calling-integration.mdx
index 37af136d5..ee7e656ce 100644
--- a/ui-kit/react-native/calling-integration.mdx
+++ b/ui-kit/react-native/calling-integration.mdx
@@ -1,177 +1,229 @@
---
title: "Calling Integration"
-description: "Add voice and video calling to your React Native UI Kit application"
+description: "Add voice and video calling to your React Native UI Kit application: Calls SDK, push ringing, permissions and logout."
---
| Field | Value |
| --- | --- |
-| Components | `CometChatIncomingCall` |
-| Package | `@cometchat/calls-sdk-react-native` — **not** `@cometchat/calls-sdk-javascript` (that is the web package) |
-| Extra deps | `@react-native-community/netinfo` · `react-native-background-timer` · `react-native-callstats` · `react-native-webrtc` |
-| Enabling | **Installing the package is the switch** — the UI Kit detects it at startup. There is no `setCallingEnabled()` in React Native. Opt out with `disableCalling: true` in your settings |
-| Version floors | Android `minSdkVersion 24` · iOS deployment target `12.0` — below these the **build fails** |
-| Permissions | iOS `NSCameraUsageDescription` + `NSMicrophoneUsageDescription`; Android `CAMERA`, `RECORD_AUDIO`, `MODIFY_AUDIO_SETTINGS` |
-| After installing | `pod install` (bare) or a fresh development build (Expo). A JS reload will not pick up the native module |
-| Testing | **Simulators cannot capture camera or microphone** — verify calls on real devices |
-| Related | [Call](/ui-kit/react-native/call-features) · [Call Buttons](/ui-kit/react-native/call-buttons) · [Incoming Call](/ui-kit/react-native/incoming-call) |
+| Packages | `@cometchat/calls-sdk-react-native` (**not** `@cometchat/calls-sdk-javascript`, that is the web package) · `@cometchat/push-notifications-react-native` · `@cometchat/push-notifications-voip-react-native` |
+| Extra deps | `@react-native-community/netinfo` · `react-native-background-timer` · `react-native-callstats` · `react-native-webrtc` · `@react-native-firebase/messaging` |
+| Enabling | **Installing the packages is the switch** — the UI Kit detects the Calls SDK at startup. There is no `setEnableCalling()` in React Native. Opt out with `disableCalling: true` |
+| Incoming calls | Rung by the Push SDK in every app state. No `CallListener`, no incoming-call component to render |
+| Version floors | Android `minSdkVersion 26` · iOS 13 — below these calls do not ring |
+| Permissions | iOS `NSCameraUsageDescription` + `NSMicrophoneUsageDescription` + the `voip` background mode; Android `CAMERA`, `RECORD_AUDIO`, `MODIFY_AUDIO_SETTINGS`, `POST_NOTIFICATIONS` |
+| After installing | `pod install` (bare) or a fresh development build (Expo). A JS reload will not pick up the native modules |
+| Testing | Simulators cannot capture camera or microphone, and CallKit does not present calls there; verify ringing on real devices |
+| Related | [Ringing](/calls/react-native/ringing) · [Push Ringing](/calls/react-native/voip-calling) · [Call Buttons](/ui-kit/react-native/call-buttons) |
## Overview
-This guide walks you through adding voice and video calling capabilities to your React Native application using the CometChat UI Kit.
+With calling enabled, the UI Kit places calls from the message header and call logs, shows the outgoing call screen, and hosts the in-call screen. Incoming calls ring through the CometChat Push Notifications SDK: it shows the call notification and the full-screen incoming-call screen on Android and a CallKit call on iOS, plays the ringtone, and hands the answered call to your app, with the app in the foreground, in the background or not running at all.
Make sure you've completed the [Getting Started](/ui-kit/react-native/react-native-cli-integration) guide before proceeding.
-## Add the Calls SDK
+```mermaid
+sequenceDiagram
+ participant A as Caller · UI Kit
+ participant BE as CometChat backend
+ participant P as Callee · Push SDK
+ participant U as Callee · UI Kit
+
+ A->>BE: Call button → CometChatCalls.initiateCall
+ Note over A: Outgoing call screen: "calling…"
+ BE-->>P: Push (initiated)
+ P->>P: Rings: notification, full-screen screen / CallKit
+ P-->>A: Callee's device reached → "ringing…"
+ P->>U: Answer → mic/camera prompt → onCallAccepted
+ U->>U: Mounts the call screen
+ U-->>A: Callee joined → caller joins
+ Note over A,U: In call. Hang-up leaves the session on both sides.
+```
+
+What you write: the packages installed, the Push SDK configuration, one event subscription that opens your call screen, and the manifest permissions. There is no call listener to register and no incoming-call UI to show.
-Install the CometChat Calls SDK:
+## Step 1: Dependencies
```bash
-npm install @cometchat/calls-sdk-react-native
+npm install @cometchat/chat-uikit-react-native \
+ @cometchat/calls-sdk-react-native \
+ @cometchat/push-notifications-react-native \
+ @cometchat/push-notifications-voip-react-native \
+ @react-native-firebase/app @react-native-firebase/messaging
+cd ios && pod install
```
-Once added, the React Native UI Kit will automatically detect it and enable calling features. The [CallButtons](/ui-kit/react-native/call-buttons) component will appear within the [MessageHeader](/ui-kit/react-native/message-header) component.
+Installing the Calls SDK is what enables calling; the UI Kit detects it at startup. Put `google-services.json` in `android/app/` and `GoogleService-Info.plist` in your iOS target.
-
-
-
-
-
-
-
-
-
-
-
-
+`minSdkVersion` must be 26 or higher for ringing, and the iOS deployment target 13 or higher.
-## Add Permissions
+
+The UI Kit requires `@cometchat/calls-sdk-react-native` 5.1 or newer and both Push Notifications packages at 2.0 or newer. Ringing behaviour described here does not exist below those versions.
+
-### Android
+## Step 2: Initialise, in this order
-Add the following permissions to `android/app/src/main/AndroidManifest.xml`:
+```ts
+import { CometChatUIKit } from '@cometchat/chat-uikit-react-native';
+import { CometChatPushNotifications } from '@cometchat/push-notifications-react-native';
+import { CometChatVoipNotifications } from '@cometchat/push-notifications-voip-react-native';
-```xml
-
+// 1. Push first — a call push may be what started this process.
+await CometChatPushNotifications.init({
+ appId: APP_ID,
+ region: REGION,
+ fcmProviderId: FCM_PROVIDER_ID,
+ apnsProviderId: APNS_PROVIDER_ID,
+ notificationSmallIcon: 'ic_notification',
+ callKitName: 'Acme',
+});
+await CometChatVoipNotifications.init(); // no config — see the warning
-
-
-
-
-
+// 2. Then the UI Kit, which initialises the Chat and Calls SDKs.
+await CometChatUIKit.init({ appId: APP_ID, region: REGION, authKey: AUTH_KEY });
+```
+
+
+**Do not pass a config to `CometChatVoipNotifications.init()` in a chat app.** It re-runs the core initialisation and drops `notificationSmallIcon`, `androidChannelId` and every other chat-side option. Only a calls-only app, which never calls `CometChatPushNotifications.init`, passes its config there.
+
+
+
+**`CometChatVoipNotifications.init()` must be the first thing your app awaits.** A cold launch caused by a call push has a ring window measured from the caller's side; anything awaited before it — stored credentials, a remote config fetch, the login — spends that window and the call is lost.
+
-
-
-
-
+Run this at your app's entry point from stored credentials, not only after a screen opens. A push can start the app process to ring a call, and the answer must find a ready SDK.
+
+Also register the background task, at module scope:
+
+```js index.js
+import { registerBackgroundCallTask } from '@cometchat/push-notifications-voip-react-native';
+registerBackgroundCallTask();
```
-### iOS
+### Staging or dedicated deployments
-Add the following entries to `ios/{appname}/Info.plist`:
+Pass the same host to the UI Kit and the Push SDK. Otherwise the push token registers on the default production host, where your app id does not exist, and no call push ever arrives.
-```xml
-NSCameraUsageDescription
-Access camera
-NSMicrophoneUsageDescription
-Access Microphone
+```ts
+await CometChatPushNotifications.init({ appId: APP_ID, region: REGION, clientHost: CLIENT_HOST });
+await CometChatUIKit.init({ appId: APP_ID, region: REGION, authKey: AUTH_KEY, overrideClientHost: CLIENT_HOST });
```
-## Set Up Minimum Versions
+
+The Chat SDK wants a bare host and the Calls SDK a full URL; giving one the other's shape hangs login forever rather than failing with an error.
+
-### Android
+## Step 3: Open your call screen when the user answers
-In `android/app/build.gradle`, set the SDK versions:
+This is the only calling code an incoming call needs:
-```groovy
-android {
- compileSdkVersion 33
+```tsx
+function App() {
+ const [activeCall, setActiveCall] = useState(null);
+
+ useEffect(() => {
+ const off = CometChatVoipNotifications.onCallAccepted(call => setActiveCall(call));
+ const offEnded = CometChatVoipNotifications.onCallEnded(() => setActiveCall(null));
+ return () => { off(); offEnded(); };
+ }, []);
- defaultConfig {
- minSdkVersion 24
- targetSdkVersion 33
- }
+ if (activeCall) return ;
+ return ;
}
```
-### iOS
+A call answered before your JavaScript existed is buffered natively and replayed to the first subscriber, so subscribing late is safe — not subscribing at all means the user answers and nothing happens.
-In Xcode, set `IPHONEOS_DEPLOYMENT_TARGET` to 12.0, or add to your Podfile:
+Every option of the push configuration (ring timeout, missed-call notifications, your own ring screen, MANUAL mode) is on the Calls SDK's [Push Ringing](/calls/react-native/voip-calling) page.
-```ruby
-post_install do |installer|
- installer.pods_project.targets.each do |target|
- target.build_configurations.each do |build_configuration|
- build_configuration.build_settings['IPHONEOS_DEPLOYMENT_TARGET'] = '12.0'
- build_configuration.build_settings['EXCLUDED_ARCHS[sdk=iphonesimulator*]'] = 'arm64 i386'
- build_configuration.build_settings['ENABLE_BITCODE'] = 'NO'
- end
- end
-end
+## Step 4: Permissions
+
+```xml android/app/src/main/AndroidManifest.xml
+
+
+
+
+
+
```
+```xml ios/{appname}/Info.plist
+NSCameraUsageDescription
+Access camera
+NSMicrophoneUsageDescription
+Access microphone
+UIBackgroundModes
+voipaudio
+```
+
+The remaining permissions and components (full-screen intent, the ringing foreground service, the ring screen, PushKit and CallKit) come from the Push SDK's own manifests.
+
-**Apple Silicon (M1/M2/M3) users:** Excluding `arm64` from the simulator build prevents the app from running natively on Apple Silicon Macs. If you are developing on an Apple Silicon Mac, consider excluding only `i386` instead of `arm64 i386` to enable native simulator builds.
+**A missing `NSMicrophoneUsageDescription` terminates the app mid-ring on iOS**, which looks exactly like the ring expiring early.
-## Set Up Call Listeners
+At runtime:
-Register a call listener to receive call events in your component:
+- **Notifications**: after sign-in, call `CometChatPushNotifications.ensureRingReady()` and show the user anything other than `ready`. Notifications off, a muted call channel, a revoked full-screen intent and Do Not Disturb all silence calls with no error.
+- **Microphone and camera**: the Push SDK asks when the user answers; a denied microphone joins muted. Ask at sign-in as well with `CometChatPNHelper.requestCallPermissions()` if you do not want the first call to be where the user meets the prompt.
-```tsx
-import React, { useEffect, useRef, useState } from "react";
-import { SafeAreaView } from "react-native";
-import { CometChat } from "@cometchat/chat-sdk-react-native";
-import { CometChatIncomingCall } from "@cometchat/chat-uikit-react-native";
+```ts
+const readiness = await CometChatPushNotifications.ensureRingReady();
+if (readiness !== 'ready') {
+ Alert.alert('Incoming calls may not ring', String(readiness));
+ await CometChatPushNotifications.openRingSettings();
+}
+```
-const App = (): React.ReactElement => {
- const [loggedIn, setLoggedIn] = useState(false);
- const [callReceived, setCallReceived] = useState(false);
- const incomingCall = useRef(null);
- const listenerID = "UNIQUE_LISTENER_ID";
+## Step 5: Log out through the UI Kit
- useEffect(() => {
- CometChat.addCallListener(
- listenerID,
- new CometChat.CallListener({
- onIncomingCallReceived: (call: CometChat.Call) => {
- incomingCall.current = call;
- setCallReceived(true);
- },
- onOutgoingCallRejected: () => {
- incomingCall.current = null;
- setCallReceived(false);
- },
- onIncomingCallCancelled: () => {
- incomingCall.current = null;
- setCallReceived(false);
- },
- })
- );
-
- return () => {
- CometChat.removeCallListener(listenerID);
- };
- }, [loggedIn]);
-
- return (
-
- {loggedIn && callReceived && incomingCall.current ? (
- {
- incomingCall.current = null;
- setCallReceived(false);
- }}
- />
- ) : null}
-
- );
-};
+```ts
+await CometChatPushNotifications.unregister(); // needs the credential; must come first
+CometChatVoipNotifications.clearIdentity();
+await CometChatUIKit.logout();
```
+
+`CometChatUIKit.logout` logs out of the Chat SDK and the Calls SDK. `unregister()` must run **before** it: deleting the token from the backend needs the logged-in credential, and skipping it leaves the device receiving the departed user's call pushes.
+
+## What the UI Kit does from here
+
+Once calling is active, the voice and video call buttons render in the [MessageHeader](/ui-kit/react-native/message-header).
+
+
+
+
+
+| Moment | What happens | Component |
+|--------|--------------|-----------|
+| Tap a call button | `CometChatCalls.initiateCall`, then the outgoing screen opens | [Call Buttons](/ui-kit/react-native/call-buttons) |
+| Waiting | "Calling…", then "Ringing…" when the callee's device is reached; End cancels the call; 45 s with no answer ends it as unanswered | [Outgoing Call](/ui-kit/react-native/outgoing-call) |
+| Callee declines / is busy / does not answer | The outgoing screen closes with the reason | Outgoing Call |
+| Incoming call | The Push SDK rings and draws the incoming-call screen | Push SDK (no UI Kit component) |
+| Answer | `onCallAccepted` fires and your call screen mounts | [Call Features](/ui-kit/react-native/call-features) |
+| Hang-up | The session is left; the other side's screen closes | Call Features |
+| Call history | Call logs list from the Calls SDK; the call icon calls back | [Call Logs](/ui-kit/react-native/call-logs) |
+
+
+**"Ringing…" only appears when the call was delivered by push.** A call delivered over the websocket to an app already in the foreground makes no pre-join on the callee's side, so the caller's screen stays on "Calling…".
+
+
+To react to calls in your own code (a badge, analytics, refreshing a screen), subscribe to `CometChatVoipNotifications.onCallRinging` and `onCallEnded`. Do not use the Chat SDK's deprecated `CallListener`.
+
+## Styling the incoming-call screen
+
+The incoming-call screen is drawn natively, before any JavaScript exists, so the UI Kit theme cannot reach it.
+
+
+**The Android ring screen's colours are not currently overridable** — they are compiled into the screen. Its strings and the call icon can be overridden by declaring the same resource names in your app; on iOS the screen is CallKit's and only `callKitName`, `callKitIconName` and `callKitRingtone` are yours.
+
+
+To make the in-app ring match your theme, set `ringInForeground: false` and render your own screen from `CometChatVoipNotifications.onCallRinging`. See [Theming the ring](/calls/react-native/voip-calling#theming-the-ring) and [Your own incoming-call screen](/calls/react-native/voip-calling#your-own-incoming-call-screen).
+
+## Upgrading from an app that used `CometChat.addCallListener`
+
+Remove the `CallListener`, the `CometChatIncomingCall` component you rendered from it, any ringtone you played, the busy check, and the `react-native-callkeep` / `react-native-voip-push-notification` wiring, then follow the steps above. The method-by-method mapping is on [Migrating from Legacy Ringing](/calls/react-native/migrating-from-legacy-ringing).
diff --git a/ui-kit/react-native/incoming-call.mdx b/ui-kit/react-native/incoming-call.mdx
index fa2883c4d..5a328806d 100644
--- a/ui-kit/react-native/incoming-call.mdx
+++ b/ui-kit/react-native/incoming-call.mdx
@@ -1,8 +1,25 @@
---
title: "Incoming Call"
-description: "Display and handle incoming voice and video calls with CometChatIncomingCall component in React Native UI Kit, providing options to accept or decline."
+description: "Deprecated incoming call screen, kept for apps that draw the ring screen themselves; incoming calls now ring through the Push Notifications SDK."
---
+
+**Deprecated.** Incoming calls now ring through the CometChat Push Notifications SDK, which draws its own incoming-call screen on Android and presents the call through CallKit on iOS, in every app state including when the app is not running. `CometChatIncomingCall` is kept only for apps that opt into drawing the in-app ring screen themselves with `ringInForeground: false`. New integrations need no incoming-call component: see [Calling Integration](/ui-kit/react-native/calling-integration).
+
+
+## When to use it
+
+| You want | Do this |
+| --- | --- |
+| The default incoming-call screen | Nothing. The ring is drawn natively and themed by the `cometchat_pn_call_*` colour resources on Android, and by `callKitName` / `callKitIconName` / `callKitRingtone` on iOS. See [Styling the incoming-call screen](/ui-kit/react-native/calling-integration#styling-the-incoming-call-screen) |
+| This component as the in-app ring screen | Set `ringInForeground: false` and render it from `CometChatVoipNotifications.onCallRinging`. This applies to a call arriving while your app is open; a killed app still rings natively |
+| A fully custom ring screen | Your own component on `onCallRinging` / `onRingEnded` with `answerCall` / `declineCall`; see [Your own incoming-call screen](/calls/react-native/voip-calling#your-own-incoming-call-screen) |
+
+
+Do **not** drive this component from `CometChat.addCallListener` any more — the Chat SDK's calling methods are deprecated. The method-by-method mapping is on [Migrating from Legacy Ringing](/calls/react-native/migrating-from-legacy-ringing).
+
+
+
| Field | Value |