diff --git a/calls/ios/actions.mdx b/calls/ios/actions.mdx
index 96819670d..8dfca0713 100644
--- a/calls/ios/actions.mdx
+++ b/calls/ios/actions.mdx
@@ -25,7 +25,7 @@ CallSession *callSession = [CallSession shared];
-Always check `isCallSessionActive()` before calling actions to ensure there's an active call.
+Always check `isSessionJoined()` before calling in-call actions to ensure you are in a call.
## Actions
@@ -383,38 +383,48 @@ CallSession.shared.setChatButtonUnreadCount(5)
### Is Session Active
-Returns `true` if a call session is active, `false` otherwise.
+`isSessionActive()` returns `true` while the SDK owns a session: one being prepared or prepared (for example an outgoing call that is still ringing), one being joined, or a joined one. `isSessionJoined()` is the narrower "in a call" check: `true` only once the media session is joined.
```swift
-let isActive = CallSession.shared.isCallSessionActive()
+let isActive = CallSession.shared.isSessionActive()
+let isInCall = CallSession.shared.isSessionJoined()
```
```objectivec
-BOOL isActive = [[CallSession shared] isCallSessionActive];
+BOOL isActive = [[CallSession shared] isSessionActive];
+BOOL isInCall = [[CallSession shared] isSessionJoined];
```
+
+`isCallSessionActive()` is deprecated in 5.1 and renamed `isSessionActive()`. Because it now also covers a prepared session, use `isSessionJoined()` where you meant "in a call".
+
+
### Leave Session
-Ends your participation and disconnects gracefully. The call continues for other participants.
+Ends your participation and disconnects gracefully. In a 1:1 call this is the hang-up: the other side receives `onParticipantLeft` and the backend records the end, and a session joined as a peer call (`setPeerCall(true)`) ends for both. In a group call the session continues for the other participants. Leaving also cancels a session that is only prepared.
```swift
-CallSession.shared.leaveSession()
+CallSession.shared.leave()
```
```objectivec
-[[CallSession shared] leaveSession];
+[[CallSession shared] leave];
```
+
+`leaveSession()` is deprecated in 5.1 and only calls `leave()`. To end a call that is still ringing use `CometChatCalls.endCall(sessionID:status:onSuccess:onError:)` with `EndCallStatus.cancelled` (caller), as described in [Ringing](/calls/ios/ringing); `leave()` alone does not tell the other side why.
+
+
### Raise Hand
Shows a hand-raised indicator to get attention from other participants.
diff --git a/calls/ios/custom-control-panel.mdx b/calls/ios/custom-control-panel.mdx
index bc85aaaef..92481403e 100644
--- a/calls/ios/custom-control-panel.mdx
+++ b/calls/ios/custom-control-panel.mdx
@@ -260,7 +260,7 @@ private func setupControlListeners() {
}
@objc private func endCall() {
- CallSession.shared.leaveSession()
+ CallSession.shared.leave()
navigationController?.popViewController(animated: true)
}
```
@@ -300,7 +300,7 @@ private func setupControlListeners() {
}
- (void)endCall {
- [[CallSession shared] leaveSession];
+ [[CallSession shared] leave];
[self.navigationController popViewControllerAnimated:YES];
}
```
diff --git a/calls/ios/events.mdx b/calls/ios/events.mdx
index ecbded7d6..ef70bd564 100644
--- a/calls/ios/events.mdx
+++ b/calls/ios/events.mdx
@@ -5,7 +5,7 @@ sdk_version: "5.x"
description: "Handle CometChat Calls SDK v5 events on iOS for call lifecycle, participants, media state, errors, and real-time updates."
---
-Handle call session events to build responsive UIs. The SDK provides five event listener protocols to monitor session status, participant activities, media changes, button clicks, and layout changes.
+Handle call session events to build responsive UIs. The SDK provides five session listener protocols to monitor session status, participant activities, media changes, button clicks, and layout changes, plus two global listeners for incoming calls and login state. The SDK holds every listener weakly and removes none of them for you, so keep a strong reference to each listener object and remove it when your screen goes away.
## Get CallSession Instance
@@ -24,6 +24,26 @@ CallSession *callSession = [CallSession shared];
+`CallSession.shared` always exists, so listeners registered on it work before any session does. To ask whether there is a session right now, use `CometChatCalls.getActiveSession()`:
+
+
+
+```swift
+// nil when no session is prepared or joined
+if let session = CometChatCalls.getActiveSession() {
+ print("Live session \(session.sessionId ?? "")")
+}
+```
+
+
+```objectivec
+CallSession *session = [CometChatCalls getActiveSession]; // nil when no session is prepared or joined
+```
+
+
+
+`getActiveSession()` returns `CallSession.shared` as soon as a session is **prepared**, which is what `CometChatCalls.initiateCall` does on the caller and what the Push SDK's ring does on the callee, before anyone joins. That is what lets a caller listen for "ringing" and "answered" without joining (see [Ringing](/calls/ios/ringing)). Before a session is prepared or joined it returns nil, so code that registers through it (`getActiveSession()?.addSessionStatusListener(self)`) registers nothing; register on `CallSession.shared` instead.
+
Remember to remove listeners when your view controller is deallocated to prevent memory leaks. Use `removeSessionStatusListener`, `removeParticipantEventListener`, etc.
@@ -48,6 +68,10 @@ class CallViewController: UIViewController, SessionStatusListener {
CallSession.shared.removeSessionStatusListener(self)
}
+ func onConnected() {
+ // A prepared session is connected (signalling only): the join will be near-instant
+ }
+
func onSessionJoined() {
// Successfully connected to the session
}
@@ -71,7 +95,8 @@ class CallViewController: UIViewController, SessionStatusListener {
}
func onConnectionClosed() {
- // Connection permanently closed
+ // Connection permanently closed. On a session that was only prepared
+ // (an outgoing call still ringing), this means the call ended before anyone joined.
navigationController?.popViewController(animated: true)
}
}
@@ -93,6 +118,10 @@ class CallViewController: UIViewController, SessionStatusListener {
[[CallSession shared] removeSessionStatusListener:self];
}
+- (void)onConnected {
+ // A prepared session is connected (signalling only): the join will be near-instant
+}
+
- (void)onSessionJoined {
// Successfully connected to the session
}
@@ -116,7 +145,8 @@ class CallViewController: UIViewController, SessionStatusListener {
}
- (void)onConnectionClosed {
- // Connection permanently closed
+ // Connection permanently closed. On a session that was only prepared
+ // (an outgoing call still ringing), this means the call ended before anyone joined.
[self.navigationController popViewControllerAnimated:YES];
}
@@ -127,12 +157,13 @@ class CallViewController: UIViewController, SessionStatusListener {
| Event | Description |
|-------|-------------|
+| `onConnected()` | A prepared session connected: signalling only, no media, and other participants do not see you yet. Fires once per prepare (`initiateCall` on the caller, the Push SDK's ring on the callee); the media join is `onSessionJoined()` |
| `onSessionJoined()` | Successfully connected and joined the session |
-| `onSessionLeft()` | Left the session via `leaveSession()` or was removed |
-| `onSessionTimedOut()` | Session ended due to inactivity timeout |
+| `onSessionLeft()` | Left the session via `leave()` or was removed |
+| `onSessionTimedOut()` | Session ended due to inactivity timeout. Not used on a prepared session: one nobody joins within 5 minutes reports `onConnectionClosed()` |
| `onConnectionLost()` | Network interrupted, SDK attempting reconnection |
| `onConnectionRestored()` | Connection restored after being lost |
-| `onConnectionClosed()` | Connection permanently closed, cannot reconnect |
+| `onConnectionClosed()` | Connection permanently closed, cannot reconnect. On a prepared session: the call ended before anyone joined (declined, busy, rung out, cancelled, the room went away, or nobody joined within 5 minutes) |
---
@@ -154,8 +185,13 @@ class CallViewController: UIViewController, ParticipantEventListener {
CallSession.shared.removeParticipantEventListener(self)
}
+ func onParticipantConnected(participant: Participant) {
+ // A participant's device prepared the session but has not joined yet.
+ // On an outgoing call: the callee's phone is ringing.
+ }
+
func onParticipantJoined(participant: Participant) {
- // A participant joined the call
+ // A participant joined the call. On an outgoing call: the callee answered.
}
func onParticipantLeft(participant: Participant) {
@@ -196,8 +232,13 @@ class CallViewController: UIViewController, ParticipantEventListener {
[[CallSession shared] removeParticipantEventListener:self];
}
+- (void)onParticipantConnectedWithParticipant:(Participant *)participant {
+ // A participant's device prepared the session but has not joined yet.
+ // On an outgoing call: the callee's phone is ringing.
+}
+
- (void)onParticipantJoinedWithParticipant:(Participant *)participant {
- // A participant joined the call
+ // A participant joined the call. On an outgoing call: the callee answered.
}
- (void)onParticipantLeftWithParticipant:(Participant *)participant {
@@ -217,7 +258,8 @@ class CallViewController: UIViewController, ParticipantEventListener {
| Event | Parameter | Description |
|-------|-----------|-------------|
-| `onParticipantJoined` | `Participant` | A participant connected to the call |
+| `onParticipantConnected` | `Participant` | A remote participant's device prepared the session without joining. On the caller's prepared session this is the callee ringing |
+| `onParticipantJoined` | `Participant` | A participant joined the call. On the caller's prepared session this is the callee answering: join now |
| `onParticipantLeft` | `Participant` | A participant disconnected from the call |
| `onParticipantListChanged` | `[Participant]` | Participant list updated |
| `onParticipantAudioMuted` | `Participant` | A participant muted their microphone |
@@ -354,7 +396,7 @@ class CallViewController: UIViewController, MediaEventsListener {
`MediaEventsListener` has **no** screen-share callback. Screen sharing is observed on
- [`ParticipantEventListener`](#participanteventlistener) via `onParticipantStartedScreenShare` /
+ [`ParticipantEventListener`](#participant-events) via `onParticipantStartedScreenShare` /
`onParticipantStoppedScreenShare` — and on iOS those are **receive-only**: the SDK reports a share
started from another client, but cannot start one locally (that needs a Broadcast Upload Extension).
@@ -468,7 +510,7 @@ class CallViewController: UIViewController, ButtonClickListener {
| `onRecordingToggleButtonClicked` | Recording toggle button was tapped |
-Button click events fire before the SDK's default action executes. Use these to add custom logic alongside default behavior.
+Button click events fire before the SDK's default action executes. Use these to add custom logic alongside default behavior. The Leave button is the exception: the SDK leaves the session itself when it is tapped, so only clean up in `onLeaveSessionButtonClicked`.
---
@@ -587,3 +629,112 @@ CallSession.shared.clearAllListeners()
```
+
+---
+
+## Incoming Call Events
+
+`CometChatCalls.addIncomingCallListener` is a global listener, not tied to a session. It reports a call ringing on this device and the end of that ring, whether the call arrived by VoIP push while the app was killed or while it was running. The Push Notifications SDK feeds it; it reports incoming calls only, not the end of your own outgoing call.
+
+
+
+```swift
+final class CallEvents: NSObject, CometChatIncomingCallListener {
+ func onIncomingCallReceived(call: CometChatIncomingCall) {
+ // A call for this user is ringing (CallKit already shows it)
+ }
+
+ func onIncomingCallEnded(call: CometChatIncomingCall, reason: String) {
+ // The ring ended: "cancelled", "ongoing", "unanswered", "rejected", "busy", "ended"
+ }
+}
+
+let callEvents = CallEvents() // held weakly by the SDK: keep this reference
+CometChatCalls.addIncomingCallListener("my-screen", callEvents)
+
+CometChatCalls.removeIncomingCallListener("my-screen")
+```
+
+
+```objectivec
+@interface CallEvents : NSObject
+@end
+
+@implementation CallEvents
+- (void)onIncomingCallReceivedWithCall:(CometChatIncomingCall *)call {
+ // A call for this user is ringing (CallKit already shows it)
+}
+- (void)onIncomingCallEndedWithCall:(CometChatIncomingCall *)call reason:(NSString *)reason {
+ // The ring ended
+}
+@end
+
+self.callEvents = [[CallEvents alloc] init]; // held weakly by the SDK: keep this reference
+[CometChatCalls addIncomingCallListener:@"my-screen" :self.callEvents];
+
+[CometChatCalls removeIncomingCallListener:@"my-screen"];
+```
+
+
+
+| Event | Parameters | Description |
+|-------|------------|-------------|
+| `onIncomingCallReceived` | `CometChatIncomingCall` | A call for the logged-in user is ringing on this device |
+| `onIncomingCallEnded` | `CometChatIncomingCall`, `reason` | The call ended before it was answered here, once per call: `cancelled` (the caller gave up), `ongoing` (answered on another device), `unanswered`, `rejected` / `busy`, or `ended` (the session prepared for it closed before the answer). Treat any other reason as "the ring is over". A decline or ring-out on this device is reported to the Push SDK delegate's `callDidEnd(_:reason:)` instead |
+
+`CometChatIncomingCall` carries `sessionId`, `callerUid`, `callerName`, `callerAvatarUrl`, `callType`, `receiverType`, `receiverUid`, `conversationId`, `initiatedAt` (milliseconds), `isVideo` and `isPeerCall`.
+
+
+Listener ids are keys: registering the same id twice replaces the earlier listener. Do not use the id `cometchat-push-notifications`, which the Push SDK registers under.
+
+
+---
+
+## Login Events
+
+`CometChatCalls.addLoginListener` reports the Calls SDK login state. The Push SDK uses it to register and unregister the push tokens and to stop a ring when the user logs out; your app can use it to gate call UI.
+
+
+
+```swift
+import CometChatCallsSDK
+
+// Qualify the protocol if your app also imports the Chat SDK, which has its own CometChatLoginDelegate.
+final class CallsLoginEvents: NSObject, CometChatCallsSDK.CometChatLoginDelegate {
+ func onLoginSuccess(user: CallsUser) {}
+ func onLoginFailed(error: CometChatCallException?) {}
+ func onLogoutSuccess() {}
+ func onLogoutFailed(error: CometChatCallException?) {}
+}
+
+let loginEvents = CallsLoginEvents() // held weakly by the SDK: keep this reference
+CometChatCalls.addLoginListener("my-app", loginEvents)
+
+CometChatCalls.removeLoginListener("my-app")
+```
+
+
+```objectivec
+// self adopts the Calls SDK's CometChatLoginDelegate
+- (void)onLoginSuccessWithUser:(CallsSDKUser *)user {}
+- (void)onLoginFailedWithError:(CometChatCallException *)error {}
+- (void)onLogoutSuccess {}
+- (void)onLogoutFailedWithError:(CometChatCallException *)error {}
+
+[CometChatCalls addLoginListener:@"my-app" :self];
+
+[CometChatCalls removeLoginListener:@"my-app"];
+```
+
+
+
+| Event | Description |
+|-------|-------------|
+| `onLoginSuccess(user:)` | A user logged in to the Calls SDK |
+| `onLoginFailed(error:)` | A login failed |
+| `onLogoutSuccess()` | The user logged out |
+| `onLogoutFailed(error:)` | A logout failed |
+
+
+Login listener ids are keys too: registering the same id twice replaces the earlier listener. Do not use the id `cometchat-push-notifications-calls`, which the Push SDK registers under.
+
diff --git a/calls/ios/migrating-from-legacy-ringing.mdx b/calls/ios/migrating-from-legacy-ringing.mdx
new file mode 100644
index 000000000..df0cb01bf
--- /dev/null
+++ b/calls/ios/migrating-from-legacy-ringing.mdx
@@ -0,0 +1,294 @@
+---
+title: "Migrating from Legacy Ringing"
+sidebarTitle: "Migrating from Legacy Ringing"
+sdk_version: "5.x"
+description: "Move an iOS app from Chat SDK call signalling and Push Notifications SDK 1.0 or hand-written PushKit and CallKit code 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 `CometChatCallDelegate` on the WebSocket told the callee, `acceptCall` and `rejectCall` answered, and the app rang the device itself, either with its own `PKPushRegistry` and `CXProvider` code or with the Push Notifications SDK 1.0. Ringing now runs on the Calls SDK and the Push Notifications SDK's Calls product. The Chat SDK's calling methods are superseded by the Calls SDK and no longer used; a coming Chat SDK release marks them deprecated.
+
+This page is the diff. For the full description of the current flow see [Ringing](/calls/ios/ringing); for the push side see [Push ringing](/calls/ios/voip-calling).
+
+## Before you start
+
+Enable the **ringing migration** setting for your app in the CometChat dashboard, then follow the
+steps below. This page is for apps that use the SDKs directly. If you use the CometChat UI Kit, its
+support for this flow comes with a coming UI Kit release; keep legacy ringing until then.
+
+{/* TODO(RC): add the setting's exact name and dashboard path and a screenshot once the toggle ships. */}
+
+## Why it changed
+
+| Legacy | Now |
+|--------|-----|
+| Incoming calls arrived on the Chat WebSocket, so a backgrounded or killed app needed a separate PushKit + CallKit integration to ring | One path: the VoIP push rings through CallKit in every app state, the app killed and the phone locked included |
+| Your app wrote the `PKPushRegistryDelegate`, the `CXProviderDelegate`, the incoming UI and the busy check | The Push SDK owns PushKit and CallKit, handles busy, call waiting, the ring timeout and missed calls |
+| `acceptCall` / `rejectCall` / `endCall` were HTTP calls your app had to sequence with `joinSession` | Answer = join. Decline, ring-out and busy are sent by the Push SDK through a queue that survives the app being suspended |
+| Caller only knew "accepted" or "rejected" | Caller sees "Ringing…" (the callee's device was reached) and "answered". A decline, busy or ring-out is read from Call Logs while ringing |
+| 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: CometChatCallDelegate.onIncomingCallReceived
+ B->>B: App reports to CallKit or shows its own UI
+ B->>Chat: CometChat.acceptCall(sessionID:)
+ Chat-->>A: onOutgoingCallAccepted
+ A->>A: joinSession
+ B->>B: joinSession
+ 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(receiver:options:)
+ BE-->>P: VoIP push (initiated)
+ P->>P: reports to CallKit, which rings
+ P->>C: connect(sessionID:)
+ C-->>A: onParticipantConnected ("Ringing…")
+ P->>C: Answer → presentCall(_:) → joinSession
+ C-->>A: onParticipantJoined → joinSession
+ A->>A: CallSession.shared.leave() to hang up
+ end
+```
+
+## Step 1: Update dependencies
+
+```ruby Podfile
+pod 'CometChatCallsSDK', '~> 5.1.0' # was 5.0.x
+pod 'CometChatPushNotificationsSwift', '~> 2.0.0' # Core; was 'CometChatPushNotifications' 1.0
+pod 'CometChatPushNotificationsCalls', '~> 2.0.0' # new: the PushKit + CallKit ringer
+pod 'CometChatPushNotificationsChat', '~> 2.0.0' # only if you show chat notifications
+# CometChatSDK stays for chat; it is no longer used for calls
+```
+
+With Swift Package Manager, pick the same products from the Push Notifications SDK package. A Notification Service Extension links Core and Chat only, never Calls.
+
+- Keep the **Push Notifications** capability and **Background Modes → Voice over IP** (and **Remote notifications** for chat pushes).
+- Keep `NSMicrophoneUsageDescription` and `NSCameraUsageDescription` in `Info.plist`.
+
+Linking the Calls product turns ringing on. There is no second initializer.
+
+## Step 2: Replace the push configuration
+
+
+
+```swift
+// After CometChat.init(...)
+CometChatPushNotifications.shared.initialize(config: .init(
+ providerId: "YOUR_PROVIDER_ID",
+ callkitIconName: "CallKitIcon"
+))
+CometChatPushNotifications.shared.delegate = self
+```
+
+
+```swift
+// In application(_:didFinishLaunchingWithOptions:), before anything else
+CometChatPushNotifications.shared.delegate = pushDelegate // held weakly: keep it alive
+CometChatPushNotifications.shared.initialize(config: PNConfiguration(
+ appId: "APP_ID",
+ region: "REGION",
+ providerId: "YOUR_PROVIDER_ID",
+ callKit: CallKitBranding(iconTemplateImageName: "CallKitIcon")
+))
+```
+
+
+
+`appId` and `region` are required in 2.0: the Push SDK registers tokens and brings the Calls SDK up in a process the push started, before your own code has initialized anything. `initialize` must run in `didFinishLaunching`, because it creates the PushKit registry and iOS only delivers the VoIP push that launched a killed app when that registry exists by the end of launch.
+
+## Step 3: Replace the call delegate
+
+
+
+```swift
+func presentCallScreen(for call: Call, sessionId: String) {
+ // open the call screen, which calls CometChat.acceptCall and joinSession
+}
+func onCallMissed(call: Call, reason: CometChatMissedCallReason) { }
+func onCallCleanupComplete() { }
+
+// after Calls SDK login:
+CometChatPushNotifications.shared.notifyCallsSDKReady()
+```
+
+
+```swift
+final class CallPushDelegate: NSObject, CometChatPushNotificationsCallsDelegate {
+ func presentCall(_ call: IncomingCall) -> Bool {
+ // present the call screen on the root view controller; it calls joinSession at once
+ return true
+ }
+ func callDidEnd(_ call: IncomingCall, reason: CallEndReason) {
+ // dismiss the call screen for call.sessionId
+ }
+}
+// No notifyCallsSDKReady(): the Push SDK waits for the Calls SDK itself.
+```
+
+
+
+`presentCall` fires immediately on Answer in every app state, the phone locked included, at most once per session. Return `false` when you cannot show the call, and the Push SDK ends it.
+
+## Step 4: Delete the token code
+
+The Push SDK registers the APNs and VoIP tokens on login by itself. Its own unregister at logout can run after the user's auth token is gone, so call `unregisterTokens` and wait for it before you log out ([Step 7](#step-7-change-the-logout)). Remove:
+
+- `CometChatNotifications.registerPushToken(...)` after login and `unregisterPushToken(...)` before logout
+- The VoIP token handling in your own `pushRegistry(_:didUpdate:for:)`
+
+Keep forwarding the APNs device token with `CometChatPushNotifications.shared.registerDeviceToken(deviceToken)` in `application(_:didRegisterForRemoteNotificationsWithDeviceToken:)`; the VoIP token is captured by the Calls product. If your app must keep its own `PKPushRegistry`, see [Owning PushKit yourself](/calls/ios/voip-calling#owning-pushkit-yourself).
+
+## 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 CallKit calls, two providers that cannot end each other's calls, a busy reject sent on top of the SDK's).
+
+- `CometChat.addCallListener(...)` / `CometChat.calldelegate` and the `onIncomingCallReceived` handler
+- The incoming-call screen, banner or alert you showed from it, and your own ringtone for incoming calls
+- Your `PKPushRegistry` and `PKPushRegistryDelegate`, `CXProvider`, `CXProviderDelegate`, `CXCallController` and the `CallManager` that tied them to CometChat (the pattern the earlier VoIP Calling guide described)
+- Any `AVAudioSession.setActive(true)` in CallKit callbacks: CallKit owns the call's audio session, and activating it yourself causes one-way audio on lock-screen answers
+- The "already in a call → reject with busy" check
+- With Push SDK 1.0: `foregroundCallPresentation`, `CometChatIncomingCallView` / `incomingCallStyle`, `notifyCallsSDKReady()`
+
+## Step 6: Rewrite the call code
+
+| Legacy | Now | Notes |
+|--------|-----|-------|
+| `CometChat.initiateCall(call: Call(receiverId:callType:receiverType:), onSuccess:onError:)` | `CometChatCalls.initiateCall(receiver:options:onSuccess:onError:)` → `InitiatedCall` | Set `options.sessionSettings` with the session type, or an audio call opens the camera |
+| `CometChat.initiateCall(call:timeout:onSuccess:onError:)` | No client timeout. The callee's device rings for 45 s (or the push's `ringTimeout`) and then ends a 1:1 call as `unanswered` | Run your own 45 s timer on the caller: if the callee's device is never reached, nothing else ends the caller's ringing for 5 minutes |
+| `CometChatCallDelegate.onIncomingCallReceived(incomingCall:error:)` | Nothing to write; the Push SDK rings through CallKit. For your own state use `CometChatIncomingCallListener.onIncomingCallReceived(call:)` | One listener whether the call came by push to a killed app or a running one |
+| `onOutgoingCallAccepted(acceptedCall:error:)` | `ParticipantEventListener.onParticipantJoined(participant:)` on `CallSession.shared`, the session `initiateCall` prepared | New in-between event: `onParticipantConnected` = the callee's device is ringing |
+| `onOutgoingCallRejected(rejectedCall:error:)` | Poll [Call Logs](/calls/ios/call-logs) for the call's status while it rings; `SessionStatusListener.onConnectionClosed()` on the prepared session when it closes | Not reliably pushed to the caller on iOS: a busy decline, or a decline before the callee's device connected, closes nothing |
+| `onIncomingCallCancelled(canceledCall:error:)` | `CometChatIncomingCallListener.onIncomingCallEnded(call:reason:)` on the callee, with `cancelled` or, once the callee's device had prepared the session, `ended`; CallKit has already stopped the ring | |
+| `onCallEndedMessageReceived(endedCall:error:)` | `SessionStatusListener.onSessionLeft()` / `onConnectionClosed()` on the joined session | |
+| `CometChat.acceptCall(sessionID:onSuccess:onError:)` then `joinSession` | `presentCall(_:)` opens your screen, which calls `joinSession(sessionID:...)`. Nothing else to send | The Push SDK asks for the microphone and camera after Answer |
+| `CometChat.rejectCall(sessionID:status: .rejected, ...)` | CallKit's Decline. Your own button: `CometChatPushNotificationsCalls.decline(sessionId:)` | Sends `busy` automatically when in another call |
+| `CometChat.rejectCall(sessionID:status: .busy, ...)` | Nothing; the Push SDK decides busy | |
+| `CometChat.rejectCall(sessionID:status: .cancelled, ...)` | `CometChatCalls.endCall(sessionID:status: EndCallStatus.cancelled, onSuccess:onError:)` | Also tears down the caller's prepared session |
+| `CallSession.shared.leaveSession()` + `CometChat.endCall(sessionID:)` | `CallSession.shared.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()` (nil when no session is prepared or joined) | |
+| `CometChat.startCall(...)`, Chat SDK `CallSettings` | `joinSession` + the 5.x listeners (see the [v5 migration guide](/calls/ios/migration-guide-v5)) | |
+| Push SDK 1.0 `presentCallScreen(for:sessionId:)` | `presentCall(_:) -> Bool` | Takes the push SDK's `IncomingCall`, not a Chat SDK `Call` |
+| Push SDK 1.0 `onCallMissed(call:reason:)` | `callDidEnd(_:reason:)` | CallKit writes the missed call to Recents |
+| `CometChat.logout(...)` | `CometChatCalls.logout(...)` as well, after awaiting `unregisterTokens` | Otherwise the Calls SDK stays signed in as the old user |
+
+### Caller screen, before and after
+
+
+
+```swift
+CometChat.addCallListener(listenerId, self)
+
+func onOutgoingCallAccepted(acceptedCall: Call?, error: CometChatException?) {
+ joinCallSession(sessionId: acceptedCall?.sessionID ?? "")
+}
+func onOutgoingCallRejected(rejectedCall: Call?, error: CometChatException?) {
+ dismiss(animated: true)
+}
+
+@objc func endTapped() {
+ CometChat.rejectCall(sessionID: sessionId, status: .cancelled,
+ onSuccess: { _ in }, onError: { _ in })
+}
+```
+
+
+```swift
+CallSession.shared.addSessionStatusListener(self)
+CallSession.shared.addParticipantEventListener(self)
+
+func onParticipantConnected(participant: Participant) { statusLabel.text = "Ringing…" }
+func onParticipantJoined(participant: Participant) {
+ joinCallSession(sessionId: sessionId, isVideo: isVideo)
+}
+func onConnectionClosed() {
+ dismiss(animated: true) // the prepared session closed; busy and early declines come from Call Logs
+}
+
+@objc func endTapped() {
+ CometChatCalls.endCall(sessionID: sessionId, status: EndCallStatus.cancelled,
+ onSuccess: {}, onError: { _ in })
+}
+```
+
+
+
+### Callee screen, before and after
+
+
+
+```swift
+// Called from your CometChatCallDelegate or your CXProviderDelegate's answer action
+func answer(sessionId: String) {
+ CometChat.acceptCall(sessionID: sessionId, onSuccess: { call in
+ self.joinCallSession(sessionId: call?.sessionID ?? "")
+ }, onError: { _ in })
+}
+func decline(sessionId: String) {
+ CometChat.rejectCall(sessionID: sessionId, status: .rejected,
+ onSuccess: { _ in }, onError: { _ in })
+}
+```
+
+
+```swift
+// Opened by your delegate's presentCall(_:): the user already answered on CallKit
+override func viewDidLoad() {
+ super.viewDidLoad()
+ joinCallSession(sessionId: sessionId, isVideo: isVideo)
+}
+// There is no decline button here: CallKit's Decline sends it through the Push SDK.
+```
+
+
+
+## Step 7: Change the logout
+
+```swift
+// Unregister first: both token DELETEs need the user's auth token.
+CometChatPushNotifications.shared.unregisterTokens(completion: { _ in
+ CometChat.logout(onSuccess: { _ in }, onError: { _ in })
+ CometChatCalls.logout(onSuccess: { _ in }, onError: { _ in }) // new: stops a ringing call
+})
+```
+
+## What users will notice
+
+- Calls ring through CallKit with the app killed and from the lock screen, with no app code on the ring path.
+- The caller sees "Ringing…" once the callee's phone is reached, and the call screen closes when the call is declined or missed (from the Call Logs status, or the ring timer).
+- The callee stops ringing after 45 seconds, and the caller's screen closes on its own timer.
+- Missed calls appear in the Phone app's Recents.
+- A call during a call is call waiting (End & Accept ends the current call, Decline sends busy), not an automatic reject.
+- The microphone and camera prompt appears after Answer the first time, on the device that answers; on a locked phone it waits for the unlock.
+- Outgoing calls appear in Recents too, and survive an incoming cellular call.
+
+## Checklist
+
+- [ ] `CometChatCallsSDK` 5.1, Push SDK 2.0 Core + Calls products added; the Notification Service Extension links Core + Chat only
+- [ ] Push Notifications capability, Background Modes → Voice over IP, microphone and camera usage strings
+- [ ] `initialize(config:)` runs in `didFinishLaunching` with `appId`, `region` and `providerId`
+- [ ] The delegate adopts `CometChatPushNotificationsCallsDelegate` and is kept alive; `presentCall(_:)` presents and joins at once
+- [ ] Manual token registration and `notifyCallsSDKReady()` removed
+- [ ] `CometChat.addCallListener`, your own PushKit / CallKit code, incoming UI, ringtone, busy check and `setActive` calls removed
+- [ ] `initiateCall` sets the session type in `sessionSettings`, built with a new `SessionSettingsBuilder()`
+- [ ] Caller listens on the prepared session and joins only on `onParticipantJoined`; runs a 45 s ring timer
+- [ ] Hang-up uses `leave()`; no `CometChat.endCall`
+- [ ] Logout awaits `unregisterTokens`, then logs out of the Calls SDK as well
+- [ ] `ensureRingReady` runs after sign-in and anything other than `.ready` is shown to the user
+- [ ] Tested on a device: ring with the app killed and the phone locked, decline, cancel, no answer, call during a call, logout while ringing
diff --git a/calls/ios/migration-guide-v5.mdx b/calls/ios/migration-guide-v5.mdx
index 1c16205df..d45f3bf13 100644
--- a/calls/ios/migration-guide-v5.mdx
+++ b/calls/ios/migration-guide-v5.mdx
@@ -235,7 +235,7 @@ CometChatCalls.stopRecording()
```swift
let callSession = CallSession.shared
-callSession.leaveSession()
+callSession.leave()
callSession.switchCamera()
callSession.muteAudio() // or callSession.unmuteAudio()
callSession.pauseVideo() // or callSession.resumeVideo()
@@ -252,7 +252,7 @@ callSession.stopRecording()
| v4 Static Method | v5 CallSession Method |
|---|---|
-| `CometChatCalls.endSession()` | `callSession.leaveSession()` |
+| `CometChatCalls.endSession()` | `callSession.leave()` |
| `CometChatCalls.switchCamera()` | `callSession.switchCamera()` |
| `CometChatCalls.audioMuted(true)` | `callSession.muteAudio()` |
| `CometChatCalls.audioMuted(false)` | `callSession.unmuteAudio()` |
@@ -421,3 +421,31 @@ These classes still exist in v5 for backward compatibility but are deprecated:
| `CallsEventsDelegate` | 5 focused listeners on `CallSession` |
| `RTCRecordingInfo` | `MediaEventsListener` callbacks |
| `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 `CallSession` methods:
+
+| Deprecated in 5.1 | Replacement |
+|-------------------|-------------|
+| `CallSession.leaveSession()` | `CallSession.leave()` |
+| `CallSession.isCallSessionActive()` | `CallSession.isSessionActive()`, which is also `true` while a session is only prepared; use `isSessionJoined()` for "in a call" |
+| Chat SDK calling (superseded; deprecated in a coming Chat SDK release): `CometChat.initiateCall`, `acceptCall`, `rejectCall`, `endCall`, `startCall`, `getActiveCall`, `clearActiveCall`, `addCallListener` / `calldelegate`, `CometChatCallDelegate`, `CallSettings` | `CometChatCalls.initiateCall`, `endCall(sessionID:status:)`, `addIncomingCallListener`, the Push Notifications SDK Calls product, `leave()` |
+
+New in 5.1:
+
+- `CometChatCalls.initiateCall(receiver:options:onSuccess:onError:)` with `InitiateCallOptions` and `InitiatedCall`
+- `CometChatCalls.endCall(sessionID:status:onSuccess:onError:)` with `EndCallStatus`
+- `CometChatCalls.connect(sessionID:callSetting:onSuccess:onError:)`, which prepares a session ahead of `joinSession`, and `CallSession.prepareStatus` / `prepareError` with `PrepareStatus`
+- `CometChatCalls.getActiveSession()`, `CometChatCalls.getSessionState(onSuccess:)`, `CometChatCalls.getParticipants(onSuccess:)` and `CallSession.getParticipants(onSuccess:)`
+- `CometChatCalls.addIncomingCallListener` / `removeIncomingCallListener` with `CometChatIncomingCallListener` and `CometChatIncomingCall`
+- `SessionStatusListener.onConnected()` and `ParticipantEventListener.onParticipantConnected(participant:)`
+- `CallSession.leave()`, `isSessionActive()`, `isSessionJoined()`, `sessionId` and `callToken`
+- `SessionSettingsBuilder.setPeerCall(_:)`: a 1:1 session that ends for both participants when either leaves
+- `CallSessionType` and `CallReceiverType` constants
+
+On a session prepared by `connect` or `initiateCall`, `joinSession` ignores the settings it is passed: the ones frozen at prepare apply.
+
+The step-by-step move from Chat SDK signalling is on [Migrating from Legacy Ringing](/calls/ios/migrating-from-legacy-ringing).
diff --git a/calls/ios/overview.mdx b/calls/ios/overview.mdx
index be2c773c6..ecd84672a 100644
--- a/calls/ios/overview.mdx
+++ b/calls/ios/overview.mdx
@@ -14,7 +14,7 @@ Before integrating the Calls SDK, ensure you have:
1. **CometChat Account**: [Sign up](https://app.cometchat.com/signup) and create an app to get your App ID, Region, and API Key
2. **CometChat Users**: Users must exist in CometChat to use calling features. For testing, create users via the [Dashboard](https://app.cometchat.com) or [REST API](/rest-api/users/create). Authentication is handled by the Calls SDK - see [Authentication](/calls/ios/authentication)
3. **iOS Requirements**:
- - Minimum iOS version: 16.0
+ - Minimum iOS version: 15.1
- Xcode 14.0 or later
- Swift 5.0 or later
4. **Permissions**: Camera and microphone permissions for video/audio calls
@@ -29,20 +29,24 @@ sequenceDiagram
App->>CometChatCalls: init()
App->>CometChatCalls: login()
- App->>CometChatCalls: generateToken()
- App->>CometChatCalls: joinSession()
- CometChatCalls-->>App: CallSession.shared
+ App->>CometChatCalls: joinSession(sessionID:)
+ CometChatCalls-->>App: onSuccess (then use CallSession.shared)
App->>CallSession: Actions (mute, pause, etc.)
CallSession-->>App: Event callbacks
- App->>CallSession: leaveSession()
+ App->>CallSession: leave()
```
+Every call is a session joined by id. Where the id comes from depends on the kind of call:
+
+- **Direct call**: your app shares a session id (for example in a message) and each participant calls `joinSession` with it. Group calls and meetings work this way.
+- **Ringing call**: the caller starts it with `CometChatCalls.initiateCall`, the Push Notifications SDK rings the callee through CallKit, and both sides join the session once the callee answers. See [Ringing](/calls/ios/ringing) and [Push Ringing](/calls/ios/voip-calling).
+
## Features
- Incoming and outgoing call notifications with accept/reject functionality
+ Start a call, ring the callee through push and CallKit, show "Ringing…", answer, decline, cancel and end
diff --git a/calls/ios/ringing.mdx b/calls/ios/ringing.mdx
index 1c94a2433..c3c92c663 100644
--- a/calls/ios/ringing.mdx
+++ b/calls/ios/ringing.mdx
@@ -2,322 +2,444 @@
title: "Ringing"
sidebarTitle: "Ringing"
sdk_version: "5.x"
-description: "Configure CometChat Calls SDK v5 ringing on iOS for incoming calls, outgoing calls, call alerts, and accept or reject flows."
+description: "Ring users on iOS 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."
---
-Implement incoming and outgoing call notifications with accept/reject functionality. Ringing enables real-time call signaling between users, allowing them to initiate calls and respond to incoming call requests.
+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, not running at all, or the phone is locked.
+
+Two SDKs do the work:
+
+| SDK | Owns |
+|-----|------|
+| **Calls SDK** (`CometChatCallsSDK` 5.1+) | Creating the call, the media session, answering, declining, cancelling, ending, and the call events your code listens to |
+| **Push Notifications SDK, Calls product** (`CometChatPushNotificationsCalls` 2.0+) | Ringing the callee's device: the VoIP push (PushKit), the system incoming-call screen (CallKit), the ringtone, the ring timeout, missed calls in Recents, busy and call waiting. It also puts calls on CallKit, which owns the call's audio |
-Ringing functionality requires the CometChat Chat SDK to be integrated alongside the Calls SDK. The Chat SDK handles call signaling (initiating, accepting, rejecting calls), while the Calls SDK manages the actual call session.
+**Coming from the earlier ringing flow?** The Chat SDK's calling methods (`CometChat.initiateCall`, `acceptCall`, `rejectCall`, `endCall`, `CometChatCallDelegate`) are superseded by the Calls SDK and no longer used; a coming Chat SDK release marks them deprecated. This page describes the replacement. See [Migrating from legacy ringing](/calls/ios/migrating-from-legacy-ringing) for a method-by-method mapping.
-## How Ringing Works
-
-The ringing flow involves two SDKs working together:
+## How ringing works
-1. **Chat SDK** - Handles call signaling (initiate, accept, reject, cancel)
-2. **Calls SDK** - Manages the actual call session once accepted
+The caller creates the call with the Calls SDK. The CometChat backend sends a VoIP push to the callee. The Push SDK reports the call to CallKit, which rings the 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 Caller
- participant ChatSDK
- participant Receiver
- participant CallsSDK
-
- Caller->>ChatSDK: initiateCall()
- ChatSDK->>Receiver: onIncomingCallReceived
- Receiver->>ChatSDK: acceptCall()
- ChatSDK-->>Caller: onOutgoingCallAccepted
- Caller->>CallsSDK: joinSession()
- Receiver->>CallsSDK: joinSession()
+ 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(receiver:options:)
+ Note over A: Session prepared, not joined ("Calling…")
+ BE-->>P: VoIP push (callAction = initiated)
+ P->>P: Ring: CallKit incoming-call screen, ringtone
+ P->>C: connect(sessionID:) — pre-warms the session
+ C-->>A: onParticipantConnected → "Ringing…"
+ P->>P: Answer (CallKit) → presentCall(_:) on your delegate
+ C->>C: joinSession(sessionID:)
+ C-->>A: onParticipantJoined → caller joins too
+ Note over A,C: In call. Hang up with CallSession.shared.leave()
```
-## Initiate a Call
+What each side sees:
-Use the Chat SDK to initiate a call to a user or group:
+| Stage | Caller | Callee |
+|-------|--------|--------|
+| Call created | `initiateCall` succeeds; show "Calling…" | VoIP push arrives; CallKit rings |
+| Callee's device reached | `onParticipantConnected` → show "Ringing…" | Ringing |
+| Callee answers | `onParticipantJoined` → `joinSession` | `presentCall(_:)` → your call screen → `joinSession` |
+| Callee declines / is busy / does not answer | Not reliably reported. `onConnectionClosed` fires only when the callee's device had prepared the session; a busy decline, or a decline before the callee's device connected, closes nothing. Poll [Call Logs](/calls/ios/call-logs) while ringing, and keep your [ring timer](#ring-timeout) | Ring stops; `callDidEnd(_:reason:)`; a missed call in Recents if unanswered |
+| Caller cancels | — (you ended it) | Ring stops; `onIncomingCallEnded` with `cancelled`, or `ended` when the callee's device had prepared the session; a missed call in Recents |
+| Either side hangs up | `onParticipantLeft` / `onSessionLeft` | same |
-
-
-```swift
-let receiverID = "USER_ID"
-let receiverType: CometChat.ReceiverType = .user
-let callType: CometChat.CallType = .video
+
+On iOS the caller is not told when, or why, the callee ended the call: `onIncomingCallEnded` is not reported for your own outgoing call. A decline or ring-out closes the caller's prepared session (`onConnectionClosed`) only when the callee's device had prepared it. A busy decline never does (the callee is in another call and never joins the room), and neither does a decline made before the callee's device connected. So while the call rings, read its status from [Call Logs](/calls/ios/call-logs) every few seconds and end the screen on `rejected`, `busy` or `unanswered`, as the [ring timer](#ring-timeout) does when nothing arrives.
+
-let call = Call(receiverId: receiverID, callType: callType, receiverType: receiverType)
+## Prerequisites
-CometChat.initiateCall(call: call, onSuccess: { call in
- print("Call initiated: \(call?.sessionID ?? "")")
- // Show outgoing call UI
-}, onError: { error in
- print("Call initiation failed: \(error?.errorDescription ?? "")")
-})
+- [Calls SDK set up](/calls/ios/setup) and the user [logged in](/calls/ios/authentication).
+- Push notifications configured with the Push Notifications SDK 2.0 Core and Calls products. The whole push side of ringing is on the [Push ringing](/calls/ios/voip-calling) page; this page assumes it is done.
+- `NSMicrophoneUsageDescription` and `NSCameraUsageDescription` in your `Info.plist`. Add the camera string even if your app only places audio calls: the Calls SDK links the camera.
+- The **Push Notifications** capability and **Background Modes → Voice over IP** on your app target. Without them PushKit delivers no VoIP push and a killed app cannot ring.
+- A physical device. PushKit and CallKit do not work in the Simulator.
+
+```ruby Podfile
+pod 'CometChatCallsSDK', '~> 5.1.0'
+pod 'CometChatPushNotificationsSwift', '~> 2.0.0' # Core
+pod 'CometChatPushNotificationsCalls', '~> 2.0.0' # PushKit + CallKit ringer
```
-
-
-```swift
-let receiverID = "USER_ID"
-let receiverType: CometChat.ReceiverType = .user
-let callType: CometChat.CallType = .video
-let call = Call(receiverId: receiverID, callType: callType, receiverType: receiverType)
+
+Call `CometChatPushNotifications.shared.initialize(config:)` from `application(_:didFinishLaunchingWithOptions:)`. The Push SDK creates the PushKit registry inside `initialize`, and iOS only delivers the VoIP push that launched a killed app when that registry exists by the end of launch. Initialized any later, the first call after a cold start is lost.
+
+
+## Start a call
-// Set a custom timeout of 30 seconds (default is 45 seconds)
-CometChat.initiateCall(call: call, timeout: 30, onSuccess: { call in
- print("Call initiated: \(call?.sessionID ?? "")")
- // Show outgoing call UI
+`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.
+
+
+
+```swift
+let options = InitiateCallOptions()
+options.type = CallSessionType.audio // or CallSessionType.video (the default)
+options.receiverType = CallReceiverType.user
+options.sessionSettings = SessionSettingsBuilder()
+ .setSessionType(.voice) // .video for a video call
+ .setPeerCall(true) // 1:1: the call ends for both when either leaves
+ .build()
+
+CometChatCalls.initiateCall(receiver: receiverUid, options: options, onSuccess: { call in
+ print("Calling \(call.receiver ?? ""), session \(call.sessionId)")
+ DispatchQueue.main.async {
+ self.showOutgoingScreen(call)
+ self.listenOnPreparedSession(call)
+ }
}, onError: { error in
- print("Call initiation failed: \(error?.errorDescription ?? "")")
+ print("Could not start the call: \(error?.errorDescription ?? "")")
})
```
```objectivec
-NSString *receiverID = @"USER_ID";
-ReceiverType receiverType = ReceiverTypeUser;
-CallType callType = CallTypeVideo;
-
-Call *call = [[Call alloc] initWithReceiverId:receiverID
- callType:callType
- receiverType:receiverType];
-
-[CometChat initiateCallWithCall:call
- onSuccess:^(Call * call) {
- NSLog(@"Call initiated: %@", call.sessionID);
- // Show outgoing call UI
-} onError:^(CometChatException * error) {
- NSLog(@"Call initiation failed: %@", error.errorDescription);
+InitiateCallOptions *options = [[InitiateCallOptions alloc] init];
+options.type = CallSessionType.audio; // or CallSessionType.video (the default)
+options.receiverType = CallReceiverType.user;
+options.sessionSettings = [[[[[SessionSettingsBuilder alloc] init]
+ setSessionType:SessionTypeVoice] // SessionTypeVideo for a video call
+ setPeerCall:YES] // 1:1: the call ends for both when either leaves
+ build];
+
+[CometChatCalls initiateCallWithReceiver:receiverUid
+ options:options
+ onSuccess:^(InitiatedCall * call) {
+ NSLog(@"Calling %@, session %@", call.receiver, call.sessionId);
+ dispatch_async(dispatch_get_main_queue(), ^{
+ [self showOutgoingScreen:call];
+ [self listenOnPreparedSession:call];
+ });
+} onError:^(CometChatCallException * error) {
+ NSLog(@"Could not start the call: %@", error.errorDescription);
}];
```
-| Parameter | Type | Description |
-|-----------|------|-------------|
-| `receiverID` | String | UID of the user or GUID of the group to call |
-| `receiverType` | ReceiverType | `.user` or `.group` |
-| `callType` | CallType | `.video` or `.audio` |
-| `timeout` | Int | Optional. The timeout duration in seconds for the call to be answered before it's automatically cancelled. Defaults to 45 seconds. |
+| Parameter | Description |
+|-----------|-------------|
+| `receiver` | UID of the user to call |
+| `options.type` | `CallSessionType.audio` or `CallSessionType.video` (default) |
+| `options.receiverType` | `CallReceiverType.user` (default), or `CallReceiverType.group` with the group's GUID as `receiver` (see [Group calls](#group-calls)) |
+| `options.sessionSettings` | The settings the caller's session is prepared with. See the warning below. |
-## Listen for Incoming Calls
+`InitiatedCall` gives you `sessionId`, `messageId` (the id of the call message in the conversation), `conversationId`, `sender`, `receiver`, `receiverType`, `type`, `status`, `initiatedAt` (in seconds) and `session`, the `CallSession` being prepared.
-Register a call listener to receive incoming call notifications:
+`initiateCall` fails with `SESSION_ALREADY_ACTIVE` while the user is joining or in another call; leave that call first.
+
+
+**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 `.setSessionType(.voice)` opens the caller's camera. The settings you pass here are the ones the caller later joins with.
+
+Build them with a new `SessionSettingsBuilder()`. `CometChatCalls.sessionSettingsBuilder` is a single shared builder whose `build()` returns the same object every time, so a setting made for one call carries into the next.
+
+
+
+There is no client-side ring timeout. The callee's device stops at the ring window the push carries (`ringTimeout`), or the Push SDK's default of 45 seconds, and ends a 1:1 call as `unanswered`; that closes the caller's prepared session only when the callee's device had prepared it. If the callee's device is never reached, nothing ends the caller's ringing until its prepared session expires after 5 minutes, so run your own timer on the caller and end the call as `unanswered` after the same window, as shown under [Ring timeout](#ring-timeout).
+
+
+## Show "Ringing…" and detect the answer
+
+The caller stays on the session that `initiateCall` prepared and listens to it. Do not call `joinSession` 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.
+
+Register on `CallSession.shared`. It is the session `initiateCall` is preparing (the same object `call.session` and `CometChatCalls.getActiveSession()` return), and registering on it works before the prepare has finished.
```swift
-let listenerID = "UNIQUE_LISTENER_ID"
+class OutgoingCallViewController: UIViewController, SessionStatusListener, ParticipantEventListener {
-CometChat.addCallListener(listenerID, self)
+ var call: InitiatedCall!
+ private var answered = false
-// Implement CometChatCallDelegate
-extension CallViewController: CometChatCallDelegate {
-
- func onIncomingCallReceived(incomingCall: Call?, error: CometChatException?) {
- guard let call = incomingCall else { return }
- print("Incoming call from: \((call.callInitiator as? User)?.name ?? "")")
- // Show incoming call UI with accept/reject options
+ func listenOnPreparedSession(_ call: InitiatedCall) {
+ self.call = call
+ CallSession.shared.addSessionStatusListener(self)
+ CallSession.shared.addParticipantEventListener(self)
}
- func onOutgoingCallAccepted(acceptedCall: Call?, error: CometChatException?) {
- guard let call = acceptedCall else { return }
- print("Call accepted, joining session...")
- joinCallSession(sessionId: call.sessionID ?? "")
+ // The callee's device received the push and is ringing.
+ func onParticipantConnected(participant: Participant) {
+ setStatus("Ringing…")
}
- func onOutgoingCallRejected(rejectedCall: Call?, error: CometChatException?) {
- print("Call rejected")
- // Dismiss outgoing call UI
+ // The callee answered: now the caller joins.
+ func onParticipantJoined(participant: Participant) {
+ guard !answered else { return }
+ answered = true
+ removeListeners() // the call screen registers its own
+ joinCallSession(sessionId: call.sessionId, isVideo: call.type == CallSessionType.video)
}
- func onIncomingCallCancelled(canceledCall: Call?, error: CometChatException?) {
- print("Incoming call cancelled")
- // Dismiss incoming call UI
+ // The prepared session closed before anyone joined: the call is over.
+ func onConnectionClosed() {
+ closeOutgoingScreen()
}
- func onCallEndedMessageReceived(endedCall: Call?, error: CometChatException?) {
- print("Call ended")
+ func removeListeners() {
+ CallSession.shared.removeSessionStatusListener(self)
+ CallSession.shared.removeParticipantEventListener(self)
}
+
+ deinit { removeListeners() }
}
```
```objectivec
-NSString *listenerID = @"UNIQUE_LISTENER_ID";
+@interface OutgoingCallViewController ()
+@property (nonatomic, strong) InitiatedCall *call;
+@property (nonatomic, assign) BOOL answered;
+@end
-[CometChat addCallListener:listenerID delegate:self];
+@implementation OutgoingCallViewController
-// Implement CometChatCallDelegate
-- (void)onIncomingCallReceivedWithIncomingCall:(Call *)incomingCall
- error:(CometChatException *)error {
- NSLog(@"Incoming call from: %@", ((User *)incomingCall.callInitiator).name);
- // Show incoming call UI with accept/reject options
+- (void)listenOnPreparedSession:(InitiatedCall *)call {
+ self.call = call;
+ [[CallSession shared] addSessionStatusListener:self];
+ [[CallSession shared] addParticipantEventListener:self];
}
-- (void)onOutgoingCallAcceptedWithAcceptedCall:(Call *)acceptedCall
- error:(CometChatException *)error {
- NSLog(@"Call accepted, joining session...");
- [self joinCallSessionWithSessionId:acceptedCall.sessionID];
+// The callee's device received the push and is ringing.
+- (void)onParticipantConnectedWithParticipant:(Participant *)participant {
+ [self setStatus:@"Ringing…"];
}
-- (void)onOutgoingCallRejectedWithRejectedCall:(Call *)rejectedCall
- error:(CometChatException *)error {
- NSLog(@"Call rejected");
- // Dismiss outgoing call UI
+// The callee answered: now the caller joins.
+- (void)onParticipantJoinedWithParticipant:(Participant *)participant {
+ if (self.answered) return;
+ self.answered = YES;
+ [self removeListeners]; // the call screen registers its own
+ [self joinCallSessionWithSessionId:self.call.sessionId
+ isVideo:[self.call.type isEqualToString:CallSessionType.video]];
}
-- (void)onIncomingCallCancelledWithCanceledCall:(Call *)canceledCall
- error:(CometChatException *)error {
- NSLog(@"Incoming call cancelled");
- // Dismiss incoming call UI
+// The prepared session closed before anyone joined: the call is over.
+- (void)onConnectionClosed {
+ [self closeOutgoingScreen];
}
-- (void)onCallEndedMessageReceivedWithEndedCall:(Call *)endedCall
- error:(CometChatException *)error {
- NSLog(@"Call ended");
+- (void)removeListeners {
+ [[CallSession shared] removeSessionStatusListener:self];
+ [[CallSession shared] removeParticipantEventListener:self];
}
+
+- (void)dealloc { [self removeListeners]; }
+
+@end
```
-| Callback | Description |
-|----------|-------------|
-| `onIncomingCallReceived` | A new incoming call is received |
-| `onOutgoingCallAccepted` | The receiver accepted your outgoing call |
-| `onOutgoingCallRejected` | The receiver rejected your outgoing call |
-| `onIncomingCallCancelled` | The caller cancelled the incoming call |
-| `onCallEndedMessageReceived` | The call has ended |
+| Event | On the prepared session it means |
+|-------|----------------------------------|
+| `onConnected` | Your own prepare finished; the join will be near-instant |
+| `onParticipantConnected(participant:)` | The callee's phone got the push and prepared the session: it is ringing there |
+| `onParticipantJoined(participant:)` | The callee answered. Call `joinSession` now |
+| `onConnectionClosed` | The call ended before anyone joined (declined, busy, rung out, cancelled, the session died, or the prepared session expired after 5 minutes). `onSessionTimedOut` is not part of this: it only reports an idle joined session |
-
-Remember to remove the call listener when it's no longer needed to prevent memory leaks:
-```swift
-CometChat.removeCallListener(listenerID)
-```
-
+The SDK holds these listeners weakly and does not remove them for you: remove them when the screen goes away, and when the call screen takes over.
-## Accept a Call
+## Listen for calls starting and ending
-When an incoming call is received, accept it using the Chat SDK:
+`CometChatCalls.addIncomingCallListener` reports calls ringing on this device and their end. The Push SDK feeds it from the VoIP push, including in a launch the push started, provided you register the listener in `application(_:didFinishLaunchingWithOptions:)`: events are delivered only to listeners registered at that moment and are not replayed. On iOS it reports **incoming** calls only; your own outgoing call ends through the prepared session, as described above.
```swift
-func acceptIncomingCall(sessionId: String) {
- CometChat.acceptCall(sessionID: sessionId, onSuccess: { call in
- print("Call accepted")
- self.joinCallSession(sessionId: call?.sessionID ?? "")
- }, onError: { error in
- print("Accept call failed: \(error?.errorDescription ?? "")")
- })
+final class CallEvents: NSObject, CometChatIncomingCallListener {
+
+ func onIncomingCallReceived(call: CometChatIncomingCall) {
+ // A call for this user is ringing. CallKit already shows the ring;
+ // use this for your own state (badge, analytics, refreshing a list).
+ }
+
+ func onIncomingCallEnded(call: CometChatIncomingCall, reason: String) {
+ // The ring ended before it was answered here: the caller cancelled (often reported
+ // as `ended`), it was answered on another device, it rang out, or the session closed.
+ }
}
+
+let callEvents = CallEvents() // the SDK holds listeners weakly: keep this reference
+CometChatCalls.addIncomingCallListener("my-screen", callEvents)
+
+// When the screen goes away:
+CometChatCalls.removeIncomingCallListener("my-screen")
```
```objectivec
-- (void)acceptIncomingCallWithSessionId:(NSString *)sessionId {
- [CometChat acceptCallWithSessionID:sessionId
- onSuccess:^(Call * call) {
- NSLog(@"Call accepted");
- [self joinCallSessionWithSessionId:call.sessionID];
- } onError:^(CometChatException * error) {
- NSLog(@"Accept call failed: %@", error.errorDescription);
- }];
+@interface CallEvents : NSObject
+@end
+
+@implementation CallEvents
+
+- (void)onIncomingCallReceivedWithCall:(CometChatIncomingCall *)call {
+ // A call for this user is ringing. CallKit already shows the ring.
+}
+
+- (void)onIncomingCallEndedWithCall:(CometChatIncomingCall *)call reason:(NSString *)reason {
+ // The ring ended before it was answered here.
}
+
+@end
+
+// Keep a strong reference: the SDK holds listeners weakly.
+self.callEvents = [[CallEvents alloc] init];
+[CometChatCalls addIncomingCallListener:@"my-screen" :self.callEvents];
+
+// When the screen goes away:
+[CometChatCalls removeIncomingCallListener:@"my-screen"];
```
-## Reject a Call
+| Reason | Meaning |
+|--------|---------|
+| `cancelled` | The caller gave up |
+| `ongoing` | Answered on another of the user's devices |
+| `unanswered` | The call rang out |
+| `rejected`, `busy` | Declined (on another device, or found declined when the session was prepared) |
+| `ended` | The session prepared for the call closed before the answer. A caller's cancel usually arrives as `ended` once the callee's device had prepared the session (the Push SDK's delegate then gets `callDidEnd` with `.declined`), so don't label it "Declined" in your UI |
+
+It fires once per call. Treat any other reason as "the ring is over". Answering on this device is not reported, and neither is a decline or ring-out on this device: those reach the Push SDK delegate's `callDidEnd(_:reason:)`.
+
+`CometChatIncomingCall` carries `sessionId`, `callerUid`, `callerName`, `callerAvatarUrl`, `callType` (`audio` / `video`), `receiverType`, `receiverUid`, `conversationId`, `initiatedAt` (milliseconds), `isVideo` and `isPeerCall`.
+
+
+Listener ids are keys. Registering the same id twice replaces the first listener instead of adding a second one, so give each screen a stable id, and do not use `cometchat-push-notifications`: the Push SDK registers under that id.
+
+
+## Answer a call
-Reject an incoming call:
+CallKit shows the Answer button. When the user answers, the Push SDK calls `presentCall(_:)` on your `CometChatPushNotificationsCallsDelegate`. It fires immediately in every app state, the phone locked included (for End & Accept, once the previous call has left): present your call screen and join at once, so audio connects before the user unlocks. The Push SDK ends an answered call with `.failed` if the session has not joined within 20 seconds. If your UI may come up later than that, for example on an answer from the lock screen after a push started the app, don't wait for a window to join: create your call screen, call `joinSession` with its container view, and present it once a window exists. There is no separate "accept" call to make.
```swift
-func rejectIncomingCall(sessionId: String) {
- let status: CometChat.callStatus = .rejected
+import CometChatPushNotificationsSwift
+import CometChatPushNotificationsCalls
- CometChat.rejectCall(sessionID: sessionId, status: status, onSuccess: { call in
- print("Call rejected")
- // Dismiss incoming call UI
- }, onError: { error in
- print("Reject call failed: \(error?.errorDescription ?? "")")
- })
+final class CallPushDelegate: NSObject, CometChatPushNotificationsCallsDelegate {
+
+ private var pending: IncomingCall?
+
+ // The user answered. Called at most once per session.
+ // Always return true: false ends the call at once. On a lock-screen or cold-launch answer
+ // the window may not exist yet, so keep the call and present it when the UI comes up.
+ func presentCall(_ call: IncomingCall) -> Bool {
+ guard let root = rootViewController() else { pending = call; return true }
+ present(call, on: root)
+ return true
+ }
+
+ /// Call when your UI comes up, for example from sceneDidBecomeActive(_:).
+ func presentPendingCall() {
+ guard let call = pending, let root = rootViewController() else { return }
+ pending = nil
+ present(call, on: root)
+ }
+
+ private func present(_ call: IncomingCall, on root: UIViewController) {
+ let screen = CallViewController(sessionId: call.sessionId, isVideo: call.isVideo)
+ screen.modalPresentationStyle = .fullScreen
+ root.present(screen, animated: false) // the screen joins in viewDidLoad
+ }
+
+ // The call is over (never called for an answer): dismiss your screen.
+ func callDidEnd(_ call: IncomingCall, reason: CallEndReason) {
+ if pending?.sessionId == call.sessionId { pending = nil }
+ dismissCallScreen(for: call.sessionId)
+ }
}
+
+// In application(_:didFinishLaunchingWithOptions:), before initialize(config:).
+// The Push SDK holds its delegate weakly, so keep this object alive.
+CometChatPushNotifications.shared.delegate = pushDelegate
```
```objectivec
-- (void)rejectIncomingCallWithSessionId:(NSString *)sessionId {
- [CometChat rejectCallWithSessionID:sessionId
- status:callStatusRejected
- onSuccess:^(Call * call) {
- NSLog(@"Call rejected");
- // Dismiss incoming call UI
- } onError:^(CometChatException * error) {
- NSLog(@"Reject call failed: %@", error.errorDescription);
- }];
-}
-```
-
-
+@import CometChatPushNotificationsSwift;
+@import CometChatPushNotificationsCalls;
-## Cancel a Call
+@interface CallPushDelegate : NSObject
+@property (nonatomic, strong, nullable) IncomingCall *pending;
+@end
-Cancel an outgoing call before it's answered:
+@implementation CallPushDelegate
-
-
-```swift
-func cancelOutgoingCall(sessionId: String) {
- let status: CometChat.callStatus = .cancelled
+// The user answered. Called at most once per session.
+// Always return YES: NO ends the call at once. On a lock-screen or cold-launch answer
+// the window may not exist yet, so keep the call and present it when the UI comes up.
+- (BOOL)presentCall:(IncomingCall *)call {
+ UIViewController *root = [self rootViewController];
+ if (root == nil) { self.pending = call; return YES; }
+ [self present:call on:root];
+ return YES;
+}
- CometChat.rejectCall(sessionID: sessionId, status: status, onSuccess: { call in
- print("Call cancelled")
- // Dismiss outgoing call UI
- }, onError: { error in
- print("Cancel call failed: \(error?.errorDescription ?? "")")
- })
+// Call when your UI comes up, for example from sceneDidBecomeActive:.
+- (void)presentPendingCall {
+ UIViewController *root = [self rootViewController];
+ if (self.pending == nil || root == nil) return;
+ IncomingCall *call = self.pending;
+ self.pending = nil;
+ [self present:call on:root];
}
-```
-
-
-```objectivec
-- (void)cancelOutgoingCallWithSessionId:(NSString *)sessionId {
- [CometChat rejectCallWithSessionID:sessionId
- status:callStatusCancelled
- onSuccess:^(Call * call) {
- NSLog(@"Call cancelled");
- // Dismiss outgoing call UI
- } onError:^(CometChatException * error) {
- NSLog(@"Cancel call failed: %@", error.errorDescription);
- }];
+
+- (void)present:(IncomingCall *)call on:(UIViewController *)root {
+ CallViewController *screen = [[CallViewController alloc] initWithSessionId:call.sessionId
+ isVideo:call.isVideo];
+ screen.modalPresentationStyle = UIModalPresentationFullScreen;
+ [root presentViewController:screen animated:NO completion:nil]; // joins in viewDidLoad
+}
+
+// The call is over (never called for an answer): dismiss your screen.
+- (void)callDidEnd:(IncomingCall *)call reason:(CallEndReason)reason {
+ if ([self.pending.sessionId isEqualToString:call.sessionId]) self.pending = nil;
+ [self dismissCallScreenForSessionId:call.sessionId];
}
+
+@end
```
-## Join the Call Session
+`IncomingCall` carries `sessionId`, `callerUid`, `callerName`, `callerAvatarUrl`, `callType`, `receiverType`, `receiverUid`, `conversationId`, `initiatedAtMs`, `ringTimeoutMs`, `isVideo` and `isPeerCall`.
+
+After `presentCall`, the Push SDK asks for the microphone (and the camera for a video call) if the user has not decided yet. On a lock-screen answer the prompt waits until the app is active; a denied microphone joins muted. To ask earlier yourself, turn this off with `requestCallPermissions(false)` on the push configuration.
-After accepting a call (or when your outgoing call is accepted), join the call session using the Calls SDK:
+## Join the session
+
+Both sides join the same way: the callee from the screen `presentCall` opened, the caller from `onParticipantJoined`. `joinSession` takes over the session that was prepared earlier, so it connects almost at once. On a prepared session the settings passed to `joinSession` are ignored: the ones frozen when the session was prepared apply. That is the caller's `options.sessionSettings` (if the prepare failed, `joinSession` joins directly with the settings it is given, so build those the same way, `setPeerCall(true)` included for a 1:1 call); the callee's session is prepared by the Push SDK with its own settings (the session type, the camera off for an audio call, the speaker, and peer call for a 1:1 call), so layout or hidden-button settings passed by the callee have no effect.
```swift
-func joinCallSession(sessionId: String) {
- let sessionSettings = CometChatCalls.sessionSettingsBuilder
- .setSessionType(.video)
+func joinCallSession(sessionId: String, isVideo: Bool) {
+ let settings = SessionSettingsBuilder()
+ .setSessionType(isVideo ? .video : .voice)
.build()
CometChatCalls.joinSession(
sessionID: sessionId,
- callSetting: sessionSettings,
+ callSetting: settings,
container: callViewContainer,
- onSuccess: { message in
- print("Joined call session")
+ onSuccess: { _ in
+ print("Joined \(sessionId)")
},
onError: { error in
- print("Failed to join: \(error?.errorDescription ?? "")")
+ print("Join failed: \(error?.errorDescription ?? "")")
+ DispatchQueue.main.async { self.dismiss(animated: true) }
}
)
}
@@ -325,149 +447,198 @@ func joinCallSession(sessionId: String) {
```objectivec
-- (void)joinCallSessionWithSessionId:(NSString *)sessionId {
- SessionSettings *sessionSettings = [[[CometChatCalls sessionSettingsBuilder]
- setSessionType:SessionTypeVideo]
+- (void)joinCallSessionWithSessionId:(NSString *)sessionId isVideo:(BOOL)isVideo {
+ SessionSettings *settings = [[[[SessionSettingsBuilder alloc] init]
+ setSessionType:(isVideo ? SessionTypeVideo : SessionTypeVoice)]
build];
[CometChatCalls joinSessionWithSessionID:sessionId
- callSetting:sessionSettings
+ callSetting:settings
container:self.callViewContainer
onSuccess:^(NSString * message) {
- NSLog(@"Joined call session");
+ NSLog(@"Joined %@", sessionId);
} onError:^(CometChatCallException * error) {
- NSLog(@"Failed to join: %@", error.errorDescription);
+ NSLog(@"Join failed: %@", error.errorDescription);
+ dispatch_async(dispatch_get_main_queue(), ^{
+ [self dismissViewControllerAnimated:YES completion:nil];
+ });
}];
}
```
-## End a Call
+See [Join Session](/calls/ios/join-session) for the container, [Session Settings](/calls/ios/session-settings) for layouts and controls, and [Events](/calls/ios/events) for everything the joined session reports.
-Properly ending a call requires coordination between both SDKs to ensure all participants are notified and call logs are recorded correctly.
+## Decline a call
-
-Always call `CometChat.endCall()` when ending a call. This notifies the other participant and ensures the call is properly logged. Without this, the other user won't know the call has ended and call logs may be incomplete.
-
+Declining is handled by the Push SDK: CallKit's Decline button sends `endCall(sessionID:status:)` with `rejected`, and with `busy` instead when the user is already in another call. The decline goes through a queue that is replayed when the app next launches, if that is within 90 seconds, in case iOS suspends the app before it is sent. A group call's decline is local: nothing is sent. On the caller, the prepared session closes (`onConnectionClosed`).
-```mermaid
-sequenceDiagram
- participant User
- participant CallsSDK
- participant ChatSDK
- participant OtherParticipant
-
- User->>CallsSDK: leaveSession()
- User->>ChatSDK: endCall(sessionId)
- ChatSDK->>OtherParticipant: onCallEndedMessageReceived
- OtherParticipant->>CallsSDK: leaveSession()
+You only write decline code when you offer a Decline button of your own, for example on an in-app banner (Swift only):
+
+```swift
+CometChatPushNotificationsCalls.decline(sessionId: sessionId)
```
-When using the default call UI, listen for the end call button click using `ButtonClickListener` and call `endCall()`:
+Without the Push SDK, decline through the Calls SDK directly: `CometChatCalls.endCall(sessionID: sessionId, status: EndCallStatus.rejected, onSuccess:onError:)`.
+
+## Cancel a call
+
+The caller ends a call that is still ringing with `endCall` and the `cancelled` status. The SDK tears the caller's prepared session down first (your `onConnectionClosed` fires), then tells the server. The callee's ring stops and a missed call is left in Recents.
```swift
-class CallViewController: UIViewController, ButtonClickListener {
-
- var currentSessionId: String = ""
-
- override func viewDidLoad() {
- super.viewDidLoad()
- CallSession.shared.addButtonClickListener(self)
- }
-
- func onLeaveSessionButtonClicked() {
- endCall(sessionId: currentSessionId)
- }
-
- func endCall(sessionId: String) {
- // 1. Leave the call session (Calls SDK)
- CallSession.shared.leaveSession()
-
- // 2. Notify other participants (Chat SDK)
- CometChat.endCall(sessionID: sessionId, onSuccess: { call in
- print("Call ended successfully")
- self.navigationController?.popViewController(animated: true)
- }, onError: { error in
- print("End call failed: \(error?.errorDescription ?? "")")
- self.navigationController?.popViewController(animated: true)
- })
- }
-
- // Other ButtonClickListener callbacks...
+func cancelOutgoingCall(sessionId: String) {
+ CometChatCalls.endCall(sessionID: sessionId, status: EndCallStatus.cancelled, onSuccess: {
+ print("Cancelled \(sessionId)")
+ }, onError: { error in
+ print("Cancel failed: \(error?.errorDescription ?? "")")
+ })
+ closeOutgoingScreen() // close locally either way
}
```
```objectivec
-@interface CallViewController ()
-@property (nonatomic, strong) NSString *currentSessionId;
-@end
+- (void)cancelOutgoingCall:(NSString *)sessionId {
+ [CometChatCalls endCallWithSessionID:sessionId
+ status:EndCallStatus.cancelled
+ onSuccess:^{
+ NSLog(@"Cancelled %@", sessionId);
+ } onError:^(CometChatCallException * error) {
+ NSLog(@"Cancel failed: %@", error.errorDescription);
+ }];
+ [self closeOutgoingScreen]; // close locally either way
+}
+```
+
+
-@implementation CallViewController
+Disable the End button after the first tap, and ignore an `onParticipantJoined` that arrives after you cancelled: the cancel wins. If the screen goes away before `initiateCall` has returned, cancel the call from its `onSuccess`, or the callee rings for a call nobody is waiting on.
-- (void)viewDidLoad {
- [super viewDidLoad];
- [[CallSession shared] addButtonClickListener:self];
-}
+## Ring timeout
-- (void)onLeaveSessionButtonClicked {
- [self endCallWithSessionId:self.currentSessionId];
-}
+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`), which closes the caller's prepared session. To close the caller's screen at the same moment even when the callee's device was never reached, run your own timer from `initiateCall` and end the call with the same status:
-- (void)endCallWithSessionId:(NSString *)sessionId {
- // 1. Leave the call session (Calls SDK)
- [[CallSession shared] leaveSession];
-
- // 2. Notify other participants (Chat SDK)
- [CometChat endCallWithSessionID:sessionId
- onSuccess:^(Call * call) {
- NSLog(@"Call ended successfully");
- [self.navigationController popViewControllerAnimated:YES];
- } onError:^(CometChatException * error) {
- NSLog(@"End call failed: %@", error.errorDescription);
- [self.navigationController popViewControllerAnimated:YES];
- }];
+
+
+```swift
+let ringTimeout = DispatchWorkItem { [weak self] in
+ guard let self = self, !self.answered else { return }
+ CometChatCalls.endCall(sessionID: sessionId, status: EndCallStatus.unanswered,
+ onSuccess: {}, onError: { _ in })
+ self.closeOutgoingScreen()
}
+DispatchQueue.main.asyncAfter(deadline: .now() + 45, execute: ringTimeout)
-@end
+// On answer, cancel or close: ringTimeout.cancel()
+```
+
+
+```objectivec
+dispatch_after(dispatch_time(DISPATCH_TIME_NOW, 45 * NSEC_PER_SEC), dispatch_get_main_queue(), ^{
+ if (self.answered || self.closed) return;
+ [CometChatCalls endCallWithSessionID:sessionId
+ status:EndCallStatus.unanswered
+ onSuccess:^{}
+ onError:^(CometChatCallException * error) {}];
+ [self closeOutgoingScreen];
+});
```
-The other participant receives `onCallEndedMessageReceived` callback and should leave the session:
+## End a call
+
+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. In a peer call (`setPeerCall(true)`), the session ends for both as soon as either leaves.
```swift
-func onCallEndedMessageReceived(endedCall: Call?, error: CometChatException?) {
- CallSession.shared.leaveSession()
- navigationController?.popViewController(animated: true)
+// The default call UI's End button leaves the session itself; only clean up here.
+func onLeaveSessionButtonClicked() {
+ dismiss(animated: true)
+}
+
+// Your own hang-up control:
+func hangUp() {
+ CallSession.shared.leave()
+ dismiss(animated: true)
}
+
+// The other participant (SessionStatusListener):
+func onSessionLeft() { dismiss(animated: true) }
+func onConnectionClosed() { dismiss(animated: true) }
```
```objectivec
-- (void)onCallEndedMessageReceivedWithEndedCall:(Call *)endedCall
- error:(CometChatException *)error {
- [[CallSession shared] leaveSession];
- [self.navigationController popViewControllerAnimated:YES];
+// The default call UI's End button leaves the session itself; only clean up here.
+- (void)onLeaveSessionButtonClicked {
+ [self dismissViewControllerAnimated:YES completion:nil];
}
+
+// Your own hang-up control:
+- (void)hangUp {
+ [[CallSession shared] leave];
+ [self dismissViewControllerAnimated:YES completion:nil];
+}
+
+// The other participant (SessionStatusListener):
+- (void)onSessionLeft { [self dismissViewControllerAnimated:YES completion:nil]; }
+- (void)onConnectionClosed { [self dismissViewControllerAnimated:YES completion:nil]; }
```
-## Call Status Values
-
-| Status | Description |
-|--------|-------------|
-| `initiated` | Call has been initiated but not yet answered |
-| `ongoing` | Call is currently in progress |
-| `busy` | Receiver is busy on another call |
-| `rejected` | Receiver rejected the call |
-| `cancelled` | Caller cancelled before receiver answered |
-| `ended` | Call ended normally |
-| `missed` | Receiver didn't answer in time |
-| `unanswered` | Call was not answered |
+Ending the call from the lock screen or CallKit's own UI makes the SDK leave the session, which reaches your screen as `onSessionLeft`.
+
+
+`CallSession.leaveSession()` is deprecated in 5.1; use `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.
+
+
+## Outgoing calls and CallKit
+
+Installing the Push SDK's Calls product hands the Calls SDK's audio to CallKit, so outgoing calls go on CallKit too: they keep their audio through a cellular call, appear in Recents and can be ended from the lock screen. The Push SDK reports an outgoing call to CallKit by itself once the Calls SDK prepares or joins its session. To show the callee's name and the call type from the start, report it yourself right after `initiateCall` succeeds (Swift only):
+
+```swift
+CometChatPushNotificationsCalls.reportOutgoingCall(sessionId: call.sessionId,
+ calleeName: calleeName,
+ hasVideo: isVideo)
+```
+
+See [Push ringing](/calls/ios/voip-calling) for `reportCallConnected` and `reportCallEnded`.
+
+## Second call while in a call
+
+Handled by CallKit and the Push SDK, the same way a phone does it. A call that arrives while the user is in another call rings with CallKit's call-waiting buttons. **End & Accept** ends the live call and answers the new one, so `presentCall(_:)` fires again with the new session: open a fresh screen for it. **Decline** sends `busy` and keeps the live call. A call that arrives while another one is still ringing replaces it.
+
+## Group calls
+
+To ring a group, call `initiateCall` with the group's GUID as `receiver` and `options.receiverType = CallReceiverType.group`, and leave `setPeerCall` off so the call outlives any one member leaving. Every member's device rings through the push. You can also start a group call without ringing: share a session id (for example in a custom message) and have each member join it with `joinSession`. When a group call rings, 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`). On that logout the Push SDK stops a call still ringing for the old user and tries to unregister the push tokens. A Chat SDK logout does the same when the Push SDK's Chat product is linked.
+
+Unregistering needs the user's auth token, which may already be gone when the logout is reported, so call `CometChatPushNotifications.shared.unregisterTokens` and wait for it before logging out of either SDK. End or cancel any call of your own before logging out: `endCall` needs a logged-in user.
+
+## 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 (`EndCallStatus.rejected`) |
+| `busy` | Callee was in another call | Callee's Push SDK (`EndCallStatus.busy`) |
+| `cancelled` | Caller ended it before an answer | Caller, `endCall` with `EndCallStatus.cancelled` |
+| `unanswered` | Nobody answered in the ring window | Callee's Push SDK for a 1:1 call (and optionally the caller's timer, `EndCallStatus.unanswered`) |
+| `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
+
+- [Push ringing](/calls/ios/voip-calling): dependencies, capabilities, `PNConfiguration`, PushKit and CallKit, the delegate, outgoing calls on CallKit, readiness checks and what happens when the app is killed.
+- [Migrating from legacy ringing](/calls/ios/migrating-from-legacy-ringing).
+- [Events](/calls/ios/events), [Actions](/calls/ios/actions), [Call Logs](/calls/ios/call-logs).
diff --git a/calls/ios/setup.mdx b/calls/ios/setup.mdx
index 9bc375393..efaa85525 100644
--- a/calls/ios/setup.mdx
+++ b/calls/ios/setup.mdx
@@ -2,7 +2,7 @@
title: "Setup"
sidebarTitle: "Setup"
sdk_version: "5.x"
-description: "Set up CometChat Calls SDK v5 for iOS with installation, app credentials, initialization, authentication, and a basic calling flow."
+description: "Set up CometChat Calls SDK v5 for iOS with installation, app credentials, initialization, authentication, and the push dependencies needed for ringing."
---
@@ -27,11 +27,11 @@ This guide walks you through installing the CometChat Calls SDK and initializing
Add the CometChat Calls SDK to your `Podfile`:
```ruby
-platform :ios, '16.0'
+platform :ios, '15.1'
use_frameworks!
target 'YourApp' do
- pod 'CometChatCallsSDK', '~> 5.0.4'
+ pod 'CometChatCallsSDK', '~> 5.1.0'
end
```
@@ -45,7 +45,14 @@ pod install
1. In Xcode, go to **File > Add Package Dependencies**
2. Enter the repository URL: `https://github.com/cometchat/calls-sdk-ios`
-3. Select the version and add to your target
+3. Select **Up to Next Major Version** from `5.1.0` and add the `CometChatCallsSDK` product to your target
+
+
+ **Ringing users?** Incoming calls ring through the Push Notifications SDK 2.0: add its Core and Calls products
+ (`CometChatPushNotificationsSwift` and `CometChatPushNotificationsCalls`) as well, and follow
+ [Push Ringing](/calls/ios/voip-calling). The Calls SDK alone can start, join and end calls, but it cannot ring a
+ device that is not running your app.
+
## Add Permissions
@@ -59,9 +66,13 @@ Add the required permissions to your `Info.plist`:
```
-iOS requires you to provide a description for why your app needs camera and microphone access. These descriptions are shown to users when requesting permissions.
+iOS requires you to provide a description for why your app needs camera and microphone access. These descriptions are shown to users when requesting permissions. Add both strings even if your app only makes audio calls: the Calls SDK links the camera.
+Request camera and microphone access before starting or joining a call. When a call is answered from the Push SDK's
+CallKit ring, the Push SDK asks for them after Answer (on a locked phone, once the user unlocks); a denied microphone
+joins muted.
+
## Enable Background Modes
For calls to continue when the app is in the background, enable the following background modes in your project's **Signing & Capabilities**:
@@ -71,7 +82,9 @@ For calls to continue when the app is in the background, enable the following ba
3. Click **+ Capability** and add **Background Modes**
4. Enable:
- **Audio, AirPlay, and Picture in Picture**
- - **Voice over IP** (if using VoIP push notifications)
+ - **Voice over IP** (required for ringing: without it PushKit delivers no VoIP push)
+
+Ringing also needs the **Push Notifications** capability. PushKit and CallKit only work on a physical device.
## Initialize CometChat Calls
@@ -134,3 +147,11 @@ CallAppSettings *callAppSettings = [[[[[CallAppSettingsBuilder alloc] init]
| `callsAppSettings` | Configuration object with App ID and Region |
| `onSuccess` | Closure called on successful initialization |
| `onError` | Closure called if initialization fails |
+
+
+ **Initialize once, at launch.** A later initialization replaces the app ID and region but does not log the user
+ out: the stored user and auth token stay, and belong to the first app. To switch to another app ID, call
+ `CometChatCalls.logout` first, then initialize with the new app ID and log in again. When a VoIP push starts your
+ app, the Push SDK may initialize the Calls SDK with the `appId` and `region` from its own configuration before your
+ code runs; your own initialization with the same values is harmless.
+
diff --git a/calls/ios/troubleshooting.mdx b/calls/ios/troubleshooting.mdx
index d5c09899a..dab25ba0a 100644
--- a/calls/ios/troubleshooting.mdx
+++ b/calls/ios/troubleshooting.mdx
@@ -49,6 +49,13 @@ sdk_version: "5.x"
| Symptom | Cause | Fix |
| --- | --- | --- |
-| VoIP push notifications not received | Missing VoIP certificate or entitlement | Enable Push Notifications and Voice over IP in Xcode capabilities. See [VoIP Calling](/calls/ios/voip-calling) |
-| CallKit UI not showing | CallKit not configured | Implement `CXProvider` delegate and report incoming calls. See [VoIP Calling](/calls/ios/voip-calling) |
+| Incoming calls do not ring | Push Notifications SDK Calls product not linked, Push Notifications capability or Voice over IP background mode missing, or no provider ID | Add `CometChatPushNotificationsCalls`, enable the capability and background mode, set `providerId`, and run `ensureRingReady`. See [Push Ringing](/calls/ios/voip-calling) |
+| A killed app does not ring, a running one does | `CometChatPushNotifications.shared.initialize(config:)` runs after launch | Call it from `application(_:didFinishLaunchingWithOptions:)`: iOS only delivers the VoIP push that launched the app when the PushKit registry exists by the end of launch |
+| A killed app does not ring, and the dashboard push log shows APNs `400` | The APNs provider's environment does not match the build, or its Bundle ID is not the app's | Turn **Enable Production Mode** off for builds run from Xcode and on for TestFlight and App Store builds; set the provider's Bundle ID to the app's. See [Push Ringing → Check the push itself](/calls/ios/voip-calling#check-the-push-itself) |
+| Answer does nothing / CallKit ends the call | `presentCall(_:)` returned `false`, or the push delegate was released | Keep a strong reference to the delegate (the Push SDK holds it weakly) and present your screen and join from `presentCall(_:)` |
+| Caller never sees "Ringing…" / answered | Caller joined right after `initiateCall`, or registered listeners through `getActiveSession()` before the session existed | Stay on the prepared session, register on `CallSession.shared`, and join on `onParticipantJoined`. See [Ringing](/calls/ios/ringing) |
+| Audio call opens the camera | Session type not set at `initiateCall` | Pass `sessionSettings` with `.setSessionType(.voice)` in `InitiateCallOptions` |
+| One-way audio after a lock-screen answer | The app activates `AVAudioSession` itself | Remove your own `setActive(true)` calls: CallKit owns the call's audio session |
+| Caller keeps ringing when the callee's phone is off | Nothing ends a ring the callee never received for 5 minutes | Run a 45 s timer on the caller and end the call with `EndCallStatus.unanswered` |
+| Ring keeps playing after logout | Only the Chat SDK was logged out, and the Push SDK's Chat product is not linked | Call `CometChatCalls.logout` too, after awaiting `unregisterTokens` |
| Call drops when app enters background | Background mode not enabled | Enable "Audio, AirPlay, and Picture in Picture" and "Voice over IP" background modes |
diff --git a/calls/ios/voip-calling.mdx b/calls/ios/voip-calling.mdx
index aac2fbf80..fb665f1b1 100644
--- a/calls/ios/voip-calling.mdx
+++ b/calls/ios/voip-calling.mdx
@@ -1,673 +1,532 @@
---
-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 iOS for call notifications, incoming call handling, and app wake flows."
+description: "Ring incoming CometChat calls on iOS with the Push Notifications SDK 2.0 Calls product: PushKit and CallKit, configuration, joining on Answer, capabilities, missed calls and logout."
---
-Implement native VoIP calling that works when your app is in the background or killed. This guide shows how to integrate Apple's CallKit framework with CometChat to display system call UI, handle calls from the lock screen, and provide a native calling experience.
+The Calls product of the Push Notifications SDK turns a CometChat VoIP push into a ringing phone. It owns the PushKit registry, reports every call to CallKit, connects to the call while it rings, and hands the answered call to your app. It works with the app in the foreground, in the background, killed, and on the lock screen. You write no `PKPushRegistryDelegate` and no `CXProviderDelegate`.
-## Overview
+On iOS the ring is always CallKit's native incoming-call screen, in every app state. There is no custom incoming-call screen; your own UI starts when the user answers.
-VoIP calling differs from [basic in-app ringing](/calls/ios/ringing) by leveraging Apple's CallKit to:
-- Show incoming calls on lock screen with system UI
-- Handle calls when app is in background or killed
-- Integrate with CarPlay, Bluetooth, and other audio accessories
-- Provide consistent call experience across iOS devices
+This page covers the push side of ringing. Starting, answering and ending calls with the Calls SDK is on the [Ringing](/calls/ios/ringing) page.
```mermaid
-flowchart TB
- subgraph "Incoming Call Flow"
- A[Push Notification] --> B[PushKit]
- B --> C{App State?}
- C -->|Background/Killed| D[CallKit]
- C -->|Foreground| E[CometChat CallListener]
- D --> F[System Call UI]
- E --> G[In-App Call UI]
- F --> H{User Action}
- G --> H
- H -->|Accept| I[CometChat.acceptCall]
- H -->|Reject| J[CometChat.rejectCall]
- I --> K[CometChatCalls.joinSession]
- end
+flowchart LR
+ V[VoIP push
type=call] --> K[PushKit registry
created by the SDK]
+ K --> D{Push SDK decides}
+ D -->|ring| C[CallKit incoming call]
+ D -->|duplicate, expired,
not for this user| X[Reported to CallKit
and ended at once]
+ D -->|in another call| W[CallKit call waiting:
Decline / End & Accept]
+ C -->|Answer| P["presentCall(call)"]
+ P --> J[Your call screen → joinSession]
+ C -->|Decline| Q["endCall(rejected)
1:1 only"]
+ C -->|45 s| T["endCall(unanswered), 1:1 only
+ missed call in Recents"]
```
## Prerequisites
-Before implementing VoIP calling, ensure you have:
+- [Calls SDK 5.1 set up](/calls/ios/setup) and the user logged in.
+- Push Notifications enabled in the CometChat dashboard with an **APNs provider that uses a `.p8` auth key**, and that provider's id. The SDK registers both the APNs device token and the VoIP token under this one provider; a `.p12` certificate provider can serve only one of them.
+ - The provider's **Bundle ID** must be your app's bundle ID. VoIP pushes go to the topic `.voip`.
+ - The provider's **APNs environment must match the build.** A build run from Xcode gets a sandbox token, so it needs a provider with **Enable Production Mode** off. TestFlight and App Store builds need it on. On a mismatch APNs rejects the push and nothing rings.
+- iOS 15.1 or later.
+- A physical device. PushKit and CallKit do not work in the Simulator.
-- [CometChat Chat SDK](/sdk/ios/overview) and [Calls SDK](/calls/ios/setup) integrated
-- Apple Push Notification service (APNs) VoIP certificate configured
-- [Push notifications enabled](/notifications/push-overview) in CometChat Dashboard
+## Step 1: Add the SDK
-
- **Most apps should not implement this by hand.** The
- [iOS Push Notifications SDK](/notifications/ios-push-notifications-sdk)
- (`CometChatPushNotifications`) already does everything on this page — PushKit registration,
- CallKit, accept/decline, missed-call handling — behind three integration steps. Follow this guide
- only if you need direct control over `PKPushRegistry` and `CXProvider`.
-
-- iOS 10.0+ for CallKit support
+The Push Notifications SDK ships as three products. For ringing you add **Core** and **Calls**; add **Chat** as well if the app shows chat notifications.
-
-This documentation builds on the [Ringing](/calls/ios/ringing) functionality. Make sure you understand basic call signaling before implementing VoIP.
-
+| Product / pod | Add it when | Depends on |
+|---|---|---|
+| `CometChatPushNotificationsSwift` (Core) | Always | No CometChat SDK |
+| `CometChatPushNotificationsCalls` (Calls) | Incoming calls should ring | Core + `CometChatCallsSDK` 5.1.0 |
+| `CometChatPushNotificationsChat` (Chat) | You show chat notifications | Core + `CometChatSDK` 4.1 |
----
+
+
+In Xcode, **File → Add Package Dependencies**, enter:
+
+```
+https://github.com/cometchat/push-notifications-sdk-ios
+```
+
+Pick version **2.0.0** and add `CometChatPushNotificationsSwift` and `CometChatPushNotificationsCalls` to your app target.
+
+
+```ruby Podfile
+platform :ios, '15.1'
+
+target 'YourApp' do
+ use_frameworks!
+ pod 'CometChatCallsSDK', '~> 5.1.0'
+ pod 'CometChatPushNotificationsSwift', '2.0.0'
+ pod 'CometChatPushNotificationsCalls', '2.0.0'
+end
+```
-## Architecture Overview
+Then run `pod install`.
+
+
-The VoIP implementation consists of several components working together:
+Linking the Calls product turns ringing on. The Chat SDK is optional: a calls-only app adds Core and Calls and never links it. `import CometChatPushNotificationsCalls` also brings the call types (`IncomingCall`, `CallEndReason`) with it.
-| Component | Purpose |
-|-----------|---------|
-| `PushKit` | Receives VoIP push notifications when app is in background |
-| `CXProvider` | CallKit provider that manages call state with the system |
-| `CXCallController` | Requests call actions (start, end, hold) from the system |
-| `CallManager` | Your custom class that coordinates between CometChat and CallKit |
+The SDK links `PushKit`, `CallKit` and `AVFAudio` itself; you add nothing for them.
----
+
+CallKit is not allowed in apps sold in China. For a China App Store build, ship a target without the Calls product; it is the only product that uses CallKit.
+
-## Step 1: Configure Push Notifications
+## Step 2: Capabilities and Info.plist
+
+These are what Android declares in its manifest. Each one, when missing, silences calls without an error.
+
+| What | Where | Why |
+|---|---|---|
+| **Push Notifications** capability (adds the `aps-environment` entitlement) | Signing & Capabilities | Without it iOS issues neither the APNs device token nor the VoIP token |
+| **Background Modes → Voice over IP** (`voip` in `UIBackgroundModes`) | Signing & Capabilities | PushKit does not deliver VoIP pushes to an app without it, so a killed app cannot ring |
+| **Background Modes → Audio, AirPlay, and Picture in Picture** (`audio`) | Signing & Capabilities | Keeps the call running when the user leaves the app; see [Background Handling](/calls/ios/background-handling) |
+| **Background Modes → Remote notifications** (`remote-notification`) | Signing & Capabilities | Only for chat: silent pushes reach the app. Not needed for ringing |
+| `NSMicrophoneUsageDescription` | `Info.plist` | The Calls SDK captures audio on join; iOS terminates an app that uses the microphone without it |
+| `NSCameraUsageDescription` | `Info.plist` | Video calls. Add it even if you only place voice calls: the Calls SDK links the camera |
+
+```xml Info.plist
+NSMicrophoneUsageDescription
+Lets you talk on calls.
+NSCameraUsageDescription
+Lets you be seen on video calls.
+UIBackgroundModes
+
+ voip
+ audio
+
+```
-VoIP push notifications are essential for receiving incoming calls when your app is not in the foreground.
+A development build carries `aps-environment` = `development`; TestFlight and App Store builds need `production`, which Xcode sets when you archive with a distribution profile.
+## Step 3: Initialise in `didFinishLaunching`
-### 1.1 Enable VoIP Capability
+Call `initialize(config:)` in `application(_:didFinishLaunchingWithOptions:)`, **on every launch**, before the user logs in. This is the one hard rule:
-In Xcode, add the following capabilities to your app:
-1. Go to your target's "Signing & Capabilities" tab
-2. Add "Push Notifications" capability
-3. Add "Background Modes" capability and enable:
- - Voice over IP
- - Background fetch
- - Remote notifications
+- The SDK creates its PushKit registry inside `initialize`. iOS delivers the VoIP push that launched a killed app only to a registry that exists by the time `didFinishLaunching` returns, so a later call loses the first call after a cold start.
+- Token registration on login and logout only runs in a launch where `initialize` ran.
+- `initialize` also sets itself as the `UNUserNotificationCenter` delegate, asks for notification permission (unless you turn it off), and saves the configuration so a launch started by a push still knows your app.
-### 1.2 Register for VoIP Push
+Set the delegate first. The SDK holds it weakly, so keep a strong reference to it, as the `AppDelegate` below does.
-```swift
-import PushKit
-
-class AppDelegate: UIResponder, UIApplicationDelegate {
-
- var voipRegistry: PKPushRegistry!
-
- func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
-
- // Initialize CometChat
- // CometChat.init(...)
-
- // Register for VoIP push notifications
- registerForVoIPPush()
-
+```swift AppDelegate.swift
+import UIKit
+import CometChatPushNotificationsSwift
+import CometChatPushNotificationsCalls
+
+@main
+final class AppDelegate: UIResponder, UIApplicationDelegate {
+
+ var window: UIWindow?
+
+ // The SDK holds its delegate weakly: the app keeps it alive.
+ let pushDelegate = PushDelegate()
+
+ func application(_ application: UIApplication,
+ didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
+ CometChatPushNotifications.shared.delegate = pushDelegate
+ CometChatPushNotifications.shared.initialize(config: PNConfiguration(
+ appId: "APP_ID",
+ region: "REGION",
+ providerId: "PROVIDER_ID", // the APNs provider from the dashboard
+ callKit: CallKitBranding(iconTemplateImageName: "CallKitIcon")
+ ))
return true
}
-
- private func registerForVoIPPush() {
- voipRegistry = PKPushRegistry(queue: DispatchQueue.main)
- voipRegistry.delegate = self
- voipRegistry.desiredPushTypes = [.voIP]
- }
}
```
-```objectivec
-#import
-
-@interface AppDelegate ()
-@property (nonatomic, strong) PKPushRegistry *voipRegistry;
-@end
-
-@implementation AppDelegate
+```objectivec AppDelegate.m
+@import CometChatPushNotificationsSwift;
+@import CometChatPushNotificationsCalls;
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
-
- // Initialize CometChat
- // [CometChat init:...]
-
- // Register for VoIP push notifications
- [self registerForVoIPPush];
-
+ // self.pushDelegate is a strong property: the SDK holds its delegate weakly.
+ CometChatPushNotifications.shared.delegate = self.pushDelegate;
+
+ PNConfiguration *config = [[[[PNConfiguration builderWithAppId:@"APP_ID" region:@"REGION"]
+ providerId:@"PROVIDER_ID"]
+ callKitWithName:nil iconTemplateImageName:@"CallKitIcon" ringtone:nil includeInRecents:YES supportsVideo:YES]
+ build];
+ [CometChatPushNotifications.shared initializeWithConfig:config];
return YES;
}
-
-- (void)registerForVoIPPush {
- self.voipRegistry = [[PKPushRegistry alloc] initWithQueue:dispatch_get_main_queue()];
- self.voipRegistry.delegate = self;
- self.voipRegistry.desiredPushTypes = [NSSet setWithObject:PKPushTypeVoIP];
-}
-
-@end
```
-### 1.3 Handle VoIP Push Registration
+In a SwiftUI app, put this in an `AppDelegate` attached with `@UIApplicationDelegateAdaptor`.
-
-
-```swift
-extension AppDelegate: PKPushRegistryDelegate {
-
- func pushRegistry(_ registry: PKPushRegistry, didUpdate pushCredentials: PKPushCredentials, for type: PKPushType) {
- let token = pushCredentials.token.map { String(format: "%02.2hhx", $0) }.joined()
-
- // Register VoIP token with CometChat
- CometChat.registerTokenForPushNotification(token: token) { success in
- print("VoIP token registered successfully")
- } onError: { error in
- print("VoIP token registration failed: \(error?.errorDescription ?? "")")
- }
+`PNConfiguration(appId:region:…)` takes every option below as a defaulted parameter. From Objective-C, or to chain setters, use `PNConfiguration.builder(appId:region:)` and finish with `build()`. If you already have the Calls SDK's `CallAppSettings`, `PNConfigurationBuilder.from(_:)` starts a builder with its app id and region (it returns `nil` if they are missing).
+
+| Option | Default | What it does |
+|--------|---------|--------------|
+| `appId`, `region` | none; **required** | Your CometChat app. Must not be blank. Used to register tokens and to bring the Calls SDK up when a push starts the app |
+| `providerId` | `nil`; **required to ring** | The dashboard APNs provider the tokens are registered under. Without it no token is registered (`PNError.Code.providerRequired`), so nothing rings, and `ensureRingReady` reports `.providerMissing` |
+| `callKit` | `CallKitBranding()` | How CallKit shows your calls: icon, ringtone, Recents, video. See [Customising CallKit](#customising-callkit) |
+| `requestCallPermissions` | `true` | Ask for the microphone (and the camera for a video call) after the user answers |
+| `requestNotificationPermission` | `true` | Show the notification prompt and register for remote notifications in `initialize`. Calls ring without notification permission, so a calls-only app can set `false` |
+| `autoRegisterTokens` | `true` | Register the tokens on login and unregister them on logout, with no code of yours |
+| `tokenRegistrar` | `.auto` | Whose session registers the tokens: `.auto` (the Chat SDK if logged in, then the Calls SDK), `.chat` or `.calls` |
+| `authToken` | `nil` | A user auth token to register with when neither SDK is logged in. Not saved to disk |
+| `pushKitOwnership` | `.sdk` | `.app` if your app keeps its own `PKPushRegistry`; see [Owning PushKit yourself](#owning-pushkit-yourself) |
+| `clientHost` (builder: `overrideClientHost(_:)`) | `nil` | Register tokens against this client host instead of the default one. Set it only if you give the Chat and Calls SDKs a client host override, and use the same value |
+| `extensionGroupID`, `enableBadgeCount`, `showInAppNotifications` | `nil`, `true`, `true` | Chat notifications; see [iOS Push Notifications](/notifications/ios-push-notifications-sdk) |
+
+A call rings for the ring timeout the push carries, or 45 seconds when it carries none, counted from when the caller started the call rather than from when the push arrived (the push's send time, or failing that its arrival, when the start time is missing), plus about 2 seconds of slack.
+
+If the app also shows chat notifications, forward the APNs device token from your `AppDelegate` as described in [iOS Push Notifications](/notifications/ios-push-notifications-sdk#4-forward-the-apns-token). The VoIP token needs nothing: the SDK receives it from its own PushKit registry.
+
+## Step 4: Join when the user answers
+
+On **Answer** the SDK calls `presentCall(_:)` on your delegate. It fires at once in every app state (foreground, background, launched from killed, and with the phone locked), on the main queue, and at most once per call. Present your call screen and join at once: the join connects the audio while the phone may still be locked, and the screen becomes visible when the user unlocks. There is nothing to "accept".
+
+Adopt `CometChatPushNotificationsCallsDelegate` on the object you set as the delegate:
+
+```swift PushDelegate.swift
+import UIKit
+import CometChatPushNotificationsSwift
+import CometChatPushNotificationsCalls
+
+final class PushDelegate: NSObject, CometChatPushNotificationsCallsDelegate {
+
+ // The user answered: show the call screen and join. Return false only when the app
+ // cannot take the call at all; the SDK then ends the CallKit call.
+ // The SDK calls both methods on the main queue.
+ func presentCall(_ call: IncomingCall) -> Bool {
+ MainActor.assumeIsolated { CallPresenter.shared.show(call) }
+ return true
}
-
- func pushRegistry(_ registry: PKPushRegistry, didReceiveIncomingPushWith payload: PKPushPayload, for type: PKPushType, completion: @escaping () -> Void) {
-
- guard type == .voIP else {
- completion()
- return
- }
-
- // Extract call data from payload
- let data = payload.dictionaryPayload
- guard let sessionId = data["sessionId"] as? String,
- let callerName = data["senderName"] as? String,
- let callerUid = data["senderUid"] as? String else {
- completion()
- return
- }
-
- let callType = data["callType"] as? String ?? "video"
- let hasVideo = callType == "video"
-
- // Report incoming call to CallKit
- CallManager.shared.reportIncomingCall(
- sessionId: sessionId,
- callerName: callerName,
- callerUid: callerUid,
- hasVideo: hasVideo
- ) { error in
- completion()
- }
+
+ // The call is over (or never started): take the screen down. CallKit has already been told.
+ func callDidEnd(_ call: IncomingCall, reason: CallEndReason) {
+ MainActor.assumeIsolated { CallPresenter.shared.end(sessionId: call.sessionId) }
}
}
```
-
-
-```objectivec
-- (void)pushRegistry:(PKPushRegistry *)registry didUpdatePushCredentials:(PKPushCredentials *)pushCredentials forType:(PKPushType)type {
- NSMutableString *token = [NSMutableString string];
- const unsigned char *data = pushCredentials.token.bytes;
- for (NSUInteger i = 0; i < pushCredentials.token.length; i++) {
- [token appendFormat:@"%02.2hhx", data[i]];
+
+A launch started by a push may not have a window when the user answers, so present on the top view controller when there is one, and otherwise as soon as your UI is up. The SDK ends an answered call with `.failed` if the session has not joined within 20 seconds, so if your UI may come up later than that (an answer from the lock screen of an app the push just started), start the join without waiting for the window: create the call screen, call `joinSession` with its container view, and present it once a window exists.
+
+```swift CallPresenter.swift
+import UIKit
+import CometChatPushNotificationsCalls
+
+@MainActor
+final class CallPresenter {
+
+ static let shared = CallPresenter()
+
+ private var pending: IncomingCall?
+ private weak var screen: CallViewController?
+
+ func show(_ call: IncomingCall) {
+ if screen?.call.sessionId == call.sessionId { return }
+ guard let top = Self.topViewController() else {
+ pending = call // no window yet: a launch started by the push
+ return
+ }
+ let screen = CallViewController(call: call)
+ self.screen = screen
+ top.present(screen, animated: false)
}
-
- // Register VoIP token with CometChat
- [CometChat registerTokenForPushNotificationWithToken:token onSuccess:^(NSString * success) {
- NSLog(@"VoIP token registered successfully");
- } onError:^(CometChatException * error) {
- NSLog(@"VoIP token registration failed: %@", error.errorDescription);
- }];
-}
-- (void)pushRegistry:(PKPushRegistry *)registry didReceiveIncomingPushWithPayload:(PKPushPayload *)payload forType:(PKPushType)type withCompletionHandler:(void (^)(void))completion {
-
- if (![type isEqualToString:PKPushTypeVoIP]) {
- completion();
- return;
+ /// Call when your UI comes up, for example from sceneDidBecomeActive(_:) or your root view's onAppear.
+ func showPending() {
+ guard let call = pending else { return }
+ pending = nil
+ show(call)
}
-
- // Extract call data from payload
- NSDictionary *data = payload.dictionaryPayload;
- NSString *sessionId = data[@"sessionId"];
- NSString *callerName = data[@"senderName"];
- NSString *callerUid = data[@"senderUid"];
-
- if (!sessionId || !callerName || !callerUid) {
- completion();
- return;
+
+ func end(sessionId: String) {
+ if pending?.sessionId == sessionId { pending = nil }
+ if screen?.call.sessionId == sessionId { screen?.dismiss(animated: true) }
+ }
+
+ private static func topViewController() -> UIViewController? {
+ let windows = UIApplication.shared.connectedScenes
+ .compactMap { $0 as? UIWindowScene }
+ .flatMap { $0.windows }
+ var top = (windows.first { $0.isKeyWindow } ?? windows.first)?.rootViewController
+ while let presented = top?.presentedViewController { top = presented }
+ return top
}
-
- NSString *callType = data[@"callType"] ?: @"video";
- BOOL hasVideo = [callType isEqualToString:@"video"];
-
- // Report incoming call to CallKit
- [[CallManager shared] reportIncomingCallWithSessionId:sessionId
- callerName:callerName
- callerUid:callerUid
- hasVideo:hasVideo
- completion:^(NSError *error) {
- completion();
- }];
}
```
-
-
----
+The call screen joins by session id as soon as it loads:
-## Step 2: Create CallManager
+```swift CallViewController.swift
+import UIKit
+import CometChatCallsSDK
+import CometChatPushNotificationsCalls
-The CallManager coordinates between CometChat and CallKit:
+final class CallViewController: UIViewController {
-
-
-```swift
-import CallKit
-
-class CallManager: NSObject {
-
- static let shared = CallManager()
-
- private let provider: CXProvider
- private let callController = CXCallController()
-
- private var activeCallUUID: UUID?
- private var activeSessionId: String?
- private var activeCallerUid: String?
-
- private override init() {
- let configuration = CXProviderConfiguration()
- configuration.supportsVideo = true
- configuration.maximumCallsPerCallGroup = 1
- configuration.supportedHandleTypes = [.generic]
- configuration.iconTemplateImageData = UIImage(named: "CallIcon")?.pngData()
-
- provider = CXProvider(configuration: configuration)
-
- super.init()
- provider.setDelegate(self, queue: nil)
- }
-
- func reportIncomingCall(sessionId: String, callerName: String, callerUid: String, hasVideo: Bool, completion: @escaping (Error?) -> Void) {
- let uuid = UUID()
- activeCallUUID = uuid
- activeSessionId = sessionId
- activeCallerUid = callerUid
-
- let update = CXCallUpdate()
- update.remoteHandle = CXHandle(type: .generic, value: callerUid)
- update.localizedCallerName = callerName
- update.hasVideo = hasVideo
- update.supportsGrouping = false
- update.supportsUngrouping = false
- update.supportsHolding = true
- update.supportsDTMF = false
-
- provider.reportNewIncomingCall(with: uuid, update: update) { error in
- if let error = error {
- print("Failed to report incoming call: \(error.localizedDescription)")
- self.activeCallUUID = nil
- self.activeSessionId = nil
- }
- completion(error)
- }
- }
-
- func startOutgoingCall(sessionId: String, receiverName: String, receiverUid: String, hasVideo: Bool) {
- let uuid = UUID()
- activeCallUUID = uuid
- activeSessionId = sessionId
-
- let handle = CXHandle(type: .generic, value: receiverUid)
- let startCallAction = CXStartCallAction(call: uuid, handle: handle)
- startCallAction.isVideo = hasVideo
- startCallAction.contactIdentifier = receiverName
-
- let transaction = CXTransaction(action: startCallAction)
- callController.request(transaction) { error in
- if let error = error {
- print("Failed to start call: \(error.localizedDescription)")
- }
- }
- }
-
- func endCall() {
- guard let uuid = activeCallUUID else { return }
-
- let endCallAction = CXEndCallAction(call: uuid)
- let transaction = CXTransaction(action: endCallAction)
-
- callController.request(transaction) { error in
- if let error = error {
- print("Failed to end call: \(error.localizedDescription)")
- }
- }
- }
-
- func reportCallEnded(reason: CXCallEndedReason) {
- guard let uuid = activeCallUUID else { return }
- provider.reportCall(with: uuid, endedAt: Date(), reason: reason)
- activeCallUUID = nil
- activeSessionId = nil
+ let call: IncomingCall
+ private let container = UIView()
+
+ init(call: IncomingCall) {
+ self.call = call
+ super.init(nibName: nil, bundle: nil)
+ modalPresentationStyle = .fullScreen
}
-
- func reportCallConnected() {
- guard let uuid = activeCallUUID else { return }
- provider.reportOutgoingCall(with: uuid, connectedAt: Date())
+
+ required init?(coder: NSCoder) { fatalError("init(coder:) is not supported") }
+
+ override func viewDidLoad() {
+ super.viewDidLoad()
+ container.frame = view.bounds
+ container.autoresizingMask = [.flexibleWidth, .flexibleHeight]
+ view.addSubview(container)
+
+ let settings = SessionSettingsBuilder()
+ .setSessionType(call.isVideo ? .video : .voice)
+ .startVideoPaused(!call.isVideo)
+ .build()
+ CometChatCalls.joinSession(sessionID: call.sessionId, callSetting: settings, container: container,
+ onSuccess: { _ in },
+ onError: { [weak self] _ in
+ DispatchQueue.main.async { self?.dismiss(animated: true) }
+ })
}
}
```
-
-
-```objectivec
-#import
-
-@interface CallManager : NSObject
-@property (class, nonatomic, readonly) CallManager *shared;
-- (void)reportIncomingCallWithSessionId:(NSString *)sessionId
- callerName:(NSString *)callerName
- callerUid:(NSString *)callerUid
- hasVideo:(BOOL)hasVideo
- completion:(void (^)(NSError *))completion;
-- (void)startOutgoingCallWithSessionId:(NSString *)sessionId
- receiverName:(NSString *)receiverName
- receiverUid:(NSString *)receiverUid
- hasVideo:(BOOL)hasVideo;
-- (void)endCall;
-- (void)reportCallEndedWithReason:(CXCallEndedReason)reason;
-- (void)reportCallConnected;
-@end
-
-@implementation CallManager {
- CXProvider *_provider;
- CXCallController *_callController;
- NSUUID *_activeCallUUID;
- NSString *_activeSessionId;
- NSString *_activeCallerUid;
-}
-static CallManager *_sharedInstance = nil;
+`IncomingCall` gives you `sessionId`, `callerUid`, `callerName`, `callerAvatarUrl`, `callType`, `isVideo`, `isPeerCall` (a 1:1 call), `receiverType`, `receiverUid` and `conversationId`.
-+ (CallManager *)shared {
- static dispatch_once_t onceToken;
- dispatch_once(&onceToken, ^{
- _sharedInstance = [[CallManager alloc] init];
- });
- return _sharedInstance;
-}
+
+While the phone rings, the SDK already connects to the call. `joinSession(sessionID:)` takes that prepared session over, so answering only adds the media. A prepared session keeps the settings it was prepared with (the session type, video paused for a voice call, speaker audio, and a peer call for 1:1), and the settings passed to `joinSession` are ignored for it.
+
+
+
+**SwiftUI: keep the delegate a plain `NSObject`.** The SDK holds its delegate as an Objective-C weak reference. Do not make an `ObservableObject` with `@Published` properties the delegate; have a small `NSObject` delegate forward to it, as `PushDelegate` forwards to `CallPresenter` above.
+
+
+**Hanging up.** Leave the session as usual (`CallSession.shared.leave()`), or let the user end the call from the CallKit screen. Either way the SDK ends the CallKit call and calls `callDidEnd`.
+
+**Answer without a join.** With no delegate adopting `CometChatPushNotificationsCallsDelegate`, or when `presentCall` returns `false`, the SDK ends the CallKit call at once and calls `callDidEnd` with `.failed`. If `presentCall` returned `true` but the session has not joined within 20 seconds of Answer, it does the same, rather than leave a silent call on screen. So return `true` and defer the presentation when your UI is not ready yet, as the sample above does, instead of returning `false`.
+
+`callDidEnd(_:reason:)` gets one of:
+
+| `CallEndReason` | Meaning |
+|---|---|
+| `.answeredElsewhere` | Answered on another of the user's devices |
+| `.cancelled` | The caller cancelled before anyone answered |
+| `.declined` | Declined on this device |
+| `.timedOut` | Nobody answered within the ring window |
+| `.remoteEnded` | The call ended: the other side or the user hung up, or the session ended |
+| `.failed` | The call could not be set up or kept, including logout while it rang |
+| `.filtered` | Dropped before ringing (not normally delivered) |
+
+`.answered` is never delivered.
+
+## Permissions and ring readiness
+
+**Microphone and camera.** The SDK asks after Answer, without holding up the join. On a lock-screen answer the prompt appears when the user unlocks. A denied microphone joins muted and does not fail the call. Ask earlier as well (at sign-in, for example) so a call is not where the user first meets the prompt. If your app asks itself, turn the SDK prompt off with `requestCallPermissions: false`.
+
+**Notifications.** Calls ring through CallKit whether or not the user allows notifications. Notification permission matters only for chat notifications.
+
+**Ring readiness.** Several build and device settings silence a call with no error anywhere. Check once after sign-in and tell the user:
-- (instancetype)init {
- self = [super init];
- if (self) {
- CXProviderConfiguration *configuration = [[CXProviderConfiguration alloc] init];
- configuration.supportsVideo = YES;
- configuration.maximumCallsPerCallGroup = 1;
- configuration.supportedHandleTypes = [NSSet setWithObject:@(CXHandleTypeGeneric)];
-
- _provider = [[CXProvider alloc] initWithConfiguration:configuration];
- [_provider setDelegate:self queue:nil];
- _callController = [[CXCallController alloc] init];
+```swift
+CometChatPushNotifications.shared.ensureRingReady { result in
+ guard case .success(let readiness) = result, readiness != .ready else { return }
+ showBanner("Incoming calls may not ring: \(readiness.name)")
+ if readiness == .microphoneDenied || readiness == .notificationsOff,
+ let url = URL(string: UIApplication.openSettingsURLString) {
+ UIApplication.shared.open(url)
}
- return self;
}
+```
-- (void)reportIncomingCallWithSessionId:(NSString *)sessionId
- callerName:(NSString *)callerName
- callerUid:(NSString *)callerUid
- hasVideo:(BOOL)hasVideo
- completion:(void (^)(NSError *))completion {
- NSUUID *uuid = [NSUUID UUID];
- _activeCallUUID = uuid;
- _activeSessionId = sessionId;
- _activeCallerUid = callerUid;
-
- CXCallUpdate *update = [[CXCallUpdate alloc] init];
- update.remoteHandle = [[CXHandle alloc] initWithType:CXHandleTypeGeneric value:callerUid];
- update.localizedCallerName = callerName;
- update.hasVideo = hasVideo;
- update.supportsGrouping = NO;
- update.supportsUngrouping = NO;
- update.supportsHolding = YES;
- update.supportsDTMF = NO;
-
- [_provider reportNewIncomingCallWithUUID:uuid update:update completion:^(NSError * _Nullable error) {
- if (error) {
- NSLog(@"Failed to report incoming call: %@", error.localizedDescription);
- self->_activeCallUUID = nil;
- self->_activeSessionId = nil;
- }
- completion(error);
- }];
-}
+The completion runs on the main queue and reports the first problem it finds, in this order:
+
+| `RingReadiness` | Meaning |
+|---|---|
+| `.ready` | Nothing in the way |
+| `.backgroundModeMissing` | `UIBackgroundModes` has no `voip`: a killed app cannot ring |
+| `.voipEntitlementMissing` | The build has no `aps-environment` entitlement: no APNs or VoIP token. Checked on a device only |
+| `.microphoneDenied` | The user refused the microphone: calls ring but join muted |
+| `.providerMissing` | No `providerId` in the configuration |
+| `.notificationsOff` | Notifications are denied. Calls still ring; chat notifications do not appear |
+
+For a bug report, `CometChatPushNotifications.shared.getDiagnostics()` returns a dictionary of everything that affects ringing (products installed, login state, cached tokens, background modes, entitlement, permissions, usage strings), with no token or auth value in it. `verifyPushSetup(completion:)` registers the tokens and reports `.notLoggedIn`, `.tokenNotRegistered` or `.testPushUnavailable`; the last means registration succeeded but a round-trip test push is not available, so it is the success result today.
+
+## How a ring ends
+
+The SDK handles every way a ring ends; you write no code for it.
+
+- **Decline, 1:1 call.** The SDK sends `rejected` to the caller, or `busy` when the user is in another call. A decline that iOS suspends the app before sending is retried when the app next launches, if that is within 90 seconds.
+- **Decline, group call.** The ring stops on this device only. Nothing is sent, and the call continues for the others.
+- **No answer, 1:1 call.** After the ring window the CallKit call ends as unanswered, which leaves a missed call in Recents, and the SDK sends `unanswered`.
+- **No answer, group call.** The ring stops and leaves a missed call; nothing is sent.
+- **Caller cancels.** The ring stops and leaves a missed call.
+- **Answered on another device.** The ring stops here, with no missed call.
+- **Late push.** A push that arrives after its ring window is not rung; it leaves a missed call.
+
+The app hears about each through `callDidEnd`.
+
+## A call during a call
+
+A call that arrives while the user is in a CometChat call rings over it on CallKit's call-waiting screen with **Decline** and **End & Accept**. There is no Hold & Accept: the Calls SDK runs one session at a time.
+
+- **Decline** sends `busy` to a 1:1 caller; the live call continues.
+- **End & Accept** ends the live call (you get `callDidEnd` for it), waits for its session to leave, then delivers `presentCall` for the new call.
+
+A call that arrives while another is still ringing replaces it: the first ring stops on this device and leaves a missed call in Recents. That first call gets no `callDidEnd`, and nothing is sent to its caller.
+
+A cellular call that arrives during a CometChat call puts it on hold: the SDK mutes the microphone and pauses the video, and restores both when the cellular call ends.
+
+## Missed calls
+
+The SDK posts no missed-call notification on iOS. CallKit writes the missed call into the Phone app's **Recents** when a call stops ringing unanswered (timed out, cancelled by the caller, or ended while still ringing), as long as `includesCallsInRecents` is `true` (the default). Declined calls and calls answered elsewhere leave no missed call.
+
+Your app gets `callDidEnd` with `.timedOut`, `.cancelled` or `.remoteEnded` (ended while still ringing), which is where to update your own missed-call UI. Call history comes from [Call Logs](/calls/ios/call-logs).
+
+## When the app is killed
+
+PushKit launches the app in the background, `didFinishLaunching` runs `initialize`, and the push reaches the SDK's registry, which reports the call to CallKit.
+
+iOS requires every VoIP push to be reported to CallKit as a call; an app that does not is terminated, and iOS stops delivering VoIP pushes to it if it keeps happening. The SDK reports **every** VoIP push: one it does not ring (a duplicate, an expired call, a call to another user, a payload that is not a call) is reported and ended at once.
+
+On a launch started by a push the SDK brings the Calls SDK up from the saved app id and region, and the Calls SDK restores the logged-in user from its own storage. Initialise the Calls SDK in your own startup code as usual as well, so the join after Answer finds it ready.
+
+## Tokens and logout
-@end
+You do not register push tokens. The SDK registers the VoIP token and, when the Chat product is linked, the APNs device token, both under your `providerId`, when the user logs in to either SDK (or in `initialize` if someone is already logged in). It registers a rotated token again, and unregisters a VoIP token that iOS invalidates. A calls-only app registers only the VoIP token.
+
+To log out, **unregister first and wait for it**: the request is authenticated with the user's session, so it has to finish before that session ends.
+
+```swift
+CometChatPushNotifications.shared.unregisterTokens(completion: { _ in
+ CometChatCalls.logout(onSuccess: { _ in }, onError: { _ in })
+ // With the Chat SDK, also log out of it: CometChat.logout(...)
+})
```
-
-
----
+Logging out of the Calls SDK (or of the Chat SDK, when the Chat product is linked) also ends a call still ringing for the old user (and an answered call in progress, as `.failed`), tries to unregister the tokens and clears the app badge. That unregister can be too late because the session is already gone, which is why you call `unregisterTokens` first. The tokens stay cached on the device and are registered again for the next user who logs in.
-## Step 3: Implement CXProviderDelegate
+## Customising CallKit
-Handle CallKit callbacks for user actions:
+The incoming screen is CallKit's, so what you can change is CallKit's configuration, through `CallKitBranding`:
-
-
```swift
-extension CallManager: CXProviderDelegate {
-
- func providerDidReset(_ provider: CXProvider) {
- // Clean up any active calls
- activeCallUUID = nil
- activeSessionId = nil
- }
-
- func provider(_ provider: CXProvider, perform action: CXAnswerCallAction) {
- // User tapped "Answer" on the call UI
- guard let sessionId = activeSessionId else {
- action.fail()
- return
- }
-
- // Accept the call via CometChat
- CometChat.acceptCall(sessionID: sessionId) { call in
- action.fulfill()
-
- // Launch call UI
- DispatchQueue.main.async {
- self.launchCallViewController(sessionId: sessionId)
- }
- } onError: { error in
- print("Failed to accept call: \(error?.errorDescription ?? "")")
- action.fail()
- }
- }
-
- func provider(_ provider: CXProvider, perform action: CXEndCallAction) {
- // User tapped "Decline" or "End Call"
- guard let sessionId = activeSessionId else {
- action.fulfill()
- return
- }
-
- // Reject or end the call via CometChat
- CometChat.rejectCall(sessionID: sessionId, status: .rejected) { call in
- action.fulfill()
- } onError: { error in
- action.fulfill()
- }
-
- activeCallUUID = nil
- activeSessionId = nil
- }
-
- func provider(_ provider: CXProvider, perform action: CXSetMutedCallAction) {
- if action.isMuted {
- CallSession.shared.muteAudio()
- } else {
- CallSession.shared.unmuteAudio()
- }
- action.fulfill()
- }
-
- func provider(_ provider: CXProvider, perform action: CXSetHeldCallAction) {
- // Handle hold/unhold if needed
- action.fulfill()
- }
-
- func provider(_ provider: CXProvider, didActivate audioSession: AVAudioSession) {
- // Configure audio session for call
- do {
- try audioSession.setCategory(.playAndRecord, mode: .voiceChat)
- try audioSession.setActive(true)
- } catch {
- print("Failed to configure audio session: \(error)")
- }
- }
-
- func provider(_ provider: CXProvider, didDeactivate audioSession: AVAudioSession) {
- // Clean up audio session
- do {
- try audioSession.setActive(false)
- } catch {
- print("Failed to deactivate audio session: \(error)")
- }
- }
-
- private func launchCallViewController(sessionId: String) {
- guard let windowScene = UIApplication.shared.connectedScenes.first as? UIWindowScene,
- let rootViewController = windowScene.windows.first?.rootViewController else {
- return
- }
-
- let callVC = CallViewController()
- callVC.sessionId = sessionId
- callVC.modalPresentationStyle = .fullScreen
-
- rootViewController.present(callVC, animated: true)
- }
-}
+let branding = CallKitBranding(
+ localizedName: nil,
+ iconTemplateImageName: "CallKitIcon", // asset-catalog image
+ ringtoneSound: "ringtone.caf", // sound file in the app bundle
+ includesCallsInRecents: true,
+ supportsVideo: true
+)
```
-
-
-```objectivec
-- (void)providerDidReset:(CXProvider *)provider {
- _activeCallUUID = nil;
- _activeSessionId = nil;
-}
-- (void)provider:(CXProvider *)provider performAnswerCallAction:(CXAnswerCallAction *)action {
- if (!_activeSessionId) {
- [action fail];
- return;
- }
-
- NSString *sessionId = _activeSessionId;
-
- [CometChat acceptCallWithSessionID:sessionId onSuccess:^(Call * call) {
- [action fulfill];
-
- dispatch_async(dispatch_get_main_queue(), ^{
- [self launchCallViewControllerWithSessionId:sessionId];
- });
- } onError:^(CometChatException * error) {
- NSLog(@"Failed to accept call: %@", error.errorDescription);
- [action fail];
- }];
-}
+| `CallKitBranding` | Default | What it does |
+|---|---|---|
+| `iconTemplateImageName` | `nil` | Asset-catalog name of a template image (40 × 40 pt, alpha only) shown on the CallKit screen |
+| `ringtoneSound` | `nil` | A sound file in the app bundle; `nil` plays the user's own ringtone |
+| `includesCallsInRecents` | `true` | Calls appear in the Phone app's Recents. Also what gives a missed call its entry |
+| `supportsVideo` | `true` | CallKit offers video controls |
+| `localizedName` | `nil` | Shown as the other party of an outgoing call the SDK puts on CallKit before the callee is known; `nil` uses your app's display name. CallKit itself shows your app's name |
-- (void)provider:(CXProvider *)provider performEndCallAction:(CXEndCallAction *)action {
- if (!_activeSessionId) {
- [action fulfill];
- return;
- }
-
- [CometChat rejectCallWithSessionID:_activeSessionId status:callStatusRejected onSuccess:^(Call * call) {
- [action fulfill];
- } onError:^(CometChatException * error) {
- [action fulfill];
- }];
-
- _activeCallUUID = nil;
- _activeSessionId = nil;
-}
+The caller's name on the ring comes from the push. Pass the branding as `callKit:` in `PNConfiguration`, or with the builder's `callKit(_:)` or `callKit(name:iconTemplateImageName:ringtone:includeInRecents:supportsVideo:)`.
-- (void)provider:(CXProvider *)provider performSetMutedCallAction:(CXSetMutedCallAction *)action {
- if (action.isMuted) {
- [[CallSession shared] muteAudio];
- } else {
- [[CallSession shared] unmuteAudio];
- }
- [action fulfill];
-}
+`CometChatPushNotificationsCalls.decline(sessionId:)` declines the ringing call exactly as CallKit's Decline does, for your own in-app controls or tests. `answer(sessionId:)` exists for tests only: it answers without going through CallKit, so the CallKit call is not updated and the audio session is not configured for the call. Answer from the CallKit screen in apps.
+
+## Outgoing calls
+
+Linking the Calls product hands the Calls SDK's audio to CallKit, so every call has to be a CallKit call, outgoing ones included. The SDK puts an outgoing call on CallKit by itself when the Calls SDK connects or joins its session, including a session you join directly, such as a meeting. It reports it under your app's name as a voice call and renames it when the other party joins.
+
+To give CallKit the callee's name and the call type from the start, report the call yourself:
+
+```swift
+// When you start the outgoing call:
+CometChatPushNotificationsCalls.reportOutgoingCall(sessionId: sessionId, calleeName: "Alice", hasVideo: true)
+// When the session joins:
+CometChatPushNotificationsCalls.reportCallConnected(sessionId: sessionId)
+// When it is over (the reason defaults to .remoteEnded):
+CometChatPushNotificationsCalls.reportCallEnded(sessionId: sessionId, reason: .remoteEnded)
```
-
-
----
+A call on CallKit survives a cellular interruption, appears in Recents and can be ended from the lock screen.
-## Step 4: Handle Call End Events
+
+**Do not create your own `CXProvider` or call `AVAudioSession.setActive`.** CallKit owns the audio session: the SDK sets the category on Answer and passes CallKit's activation to the Calls SDK. An app that activates the session itself gets one-way audio on lock-screen answers, and a second provider cannot end the SDK's calls.
+
-Update CallKit when the call ends from the remote side:
+## Knowing about a call in your own code
+
+Listen on the Calls SDK, not on this SDK. It is the same event whether the call arrived by push on a killed app or on a running one:
-
-
```swift
-class CallViewController: UIViewController, SessionStatusListener {
-
- override func viewDidLoad() {
- super.viewDidLoad()
- CallSession.shared.addSessionStatusListener(self)
- }
-
- func onSessionLeft() {
- CallManager.shared.reportCallEnded(reason: .remoteEnded)
- dismiss(animated: true)
- }
-
- func onConnectionClosed() {
- CallManager.shared.reportCallEnded(reason: .failed)
- dismiss(animated: true)
- }
-
- func onSessionJoined() {
- CallManager.shared.reportCallConnected()
- }
-
- func onSessionTimedOut() {
- CallManager.shared.reportCallEnded(reason: .unanswered)
- }
-
- func onConnectionLost() {}
- func onConnectionRestored() {}
+final class CallEvents: NSObject, CometChatIncomingCallListener {
+ func onIncomingCallReceived(call: CometChatIncomingCall) { /* badge, list refresh, analytics */ }
+ func onIncomingCallEnded(call: CometChatIncomingCall, reason: String) { /* … */ }
}
+
+let callEvents = CallEvents() // the Calls SDK holds listeners weakly: keep a strong reference
+CometChatCalls.addIncomingCallListener("my-screen", callEvents)
```
-
-
-```objectivec
-- (void)viewDidLoad {
- [super viewDidLoad];
- [[CallSession shared] addSessionStatusListener:self];
-}
-- (void)onSessionLeft {
- [[CallManager shared] reportCallEndedWithReason:CXCallEndedReasonRemoteEnded];
- [self dismissViewControllerAnimated:YES completion:nil];
-}
+Ids are keys: registering the same id again replaces the listener. Use your own id, not `cometchat-push-notifications`, which the Push Notifications SDK uses for its own listener. Remove yours with `CometChatCalls.removeIncomingCallListener("my-screen")` when the screen goes away.
-- (void)onConnectionClosed {
- [[CallManager shared] reportCallEndedWithReason:CXCallEndedReasonFailed];
- [self dismissViewControllerAnimated:YES completion:nil];
-}
+## Owning PushKit yourself
-- (void)onSessionJoined {
- [[CallManager shared] reportCallConnected];
+If your app already has a `PKPushRegistry` for other VoIP pushes, set `pushKitOwnership: .app` and forward to the SDK from your registry delegate:
+
+```swift
+func pushRegistry(_ registry: PKPushRegistry, didUpdate pushCredentials: PKPushCredentials, for type: PKPushType) {
+ CometChatPushNotifications.shared.handleVoIPToken(pushCredentials.token)
}
-- (void)onSessionTimedOut {
- [[CallManager shared] reportCallEndedWithReason:CXCallEndedReasonUnanswered];
+func pushRegistry(_ registry: PKPushRegistry, didReceiveIncomingPushWith payload: PKPushPayload,
+ for type: PKPushType, completion: @escaping () -> Void) {
+ let handled = CometChatPushNotifications.shared.handleIncomingVoIPPush(payload.dictionaryPayload, completion: completion)
+ if !handled {
+ // Not a CometChat call: report your own call to CallKit, then call completion().
+ }
}
```
-
-
-
----
-
-## Related Documentation
-- [Ringing](/calls/ios/ringing) - Basic call signaling
-- [Background Handling](/calls/ios/background-handling) - Keep calls alive in background
-- [Events](/calls/ios/events) - Session status events
+When `handleIncomingVoIPPush` returns `false` the SDK did not report anything and did not call `completion`, so reporting a call to CallKit is up to you. Your registry must still be created in `didFinishLaunching`.
+
+## Troubleshooting
+
+| Symptom | Check |
+|---------|-------|
+| Nothing rings when the app is killed | `voip` background mode; `initialize` called in `didFinishLaunching` on every launch; `ensureRingReady` is `.ready`. Then check the push itself (below) |
+| A running app rings but a killed one does not | A running app can get the call over its live connection, so only a killed app proves the push works. Check the push itself (below) |
+| Tokens never register | Push Notifications capability present; `providerId` set; the user logged in to the Chat or Calls SDK. `onPushTokenRegistrationFailed` on the delegate says why |
+| Nothing happens in the Simulator | PushKit and CallKit need a physical device |
+| Answer, then the call ends with `.failed` at once | No delegate adopting `CometChatPushNotificationsCallsDelegate`, the delegate was released (it is held weakly), or `presentCall` returned `false` |
+| Answer, then the call ends with `.failed` after about 20 seconds | `presentCall` returned `true` but the screen never called `joinSession` |
+| No audio, or the caller hears nothing after a lock-screen answer | The app calls `AVAudioSession.setActive` or has its own `CXProvider`; remove them |
+| `joinSession` settings have no effect on an answered call | Expected: the prepared session keeps the settings it was prepared with |
+| Ring keeps playing after the user logged out | The app did not log out of the Calls SDK (or the Chat SDK with the Chat product linked) |
+| The device still rings for a user who logged out | `unregisterTokens` was not awaited before logout |
+| No missed call in Recents | `includesCallsInRecents` is `false` |
+| The push log shows APNs `400` for the VoIP token | The provider's APNs environment does not match the build (**Enable Production Mode** must be off for Xcode builds, on for TestFlight and App Store), or its Bundle ID is not the app's |
+| The push log shows APNs `403` | The provider's `.p8` key, Key ID or Team ID is wrong |
+| Two incoming-call screens, or a call that cannot be ended | Legacy app-side ringing (your own `PKPushRegistry`, `CXProvider` or `CallListener` ringing UI) is still in place; remove it |
+
+### Check the push itself
+
+When a killed app does not ring, look at what the server sent before debugging the app. In the dashboard, open **Notifications → Logs**, search for the call's message ID and find the row for the device's `APNS_IOS_VOIP` token.
+
+- **Success `True`, `200`**: APNs accepted the push. Check the app (the rows above).
+- **`400` or `403`**: APNs rejected the push, so the device was never woken. Fix the provider, not the app.
+- **No row for the token**: the token is not registered. See *Tokens never register* above.
+
+## Related
+
+- [Ringing](/calls/ios/ringing): starting, answering and ending calls with the Calls SDK.
+- [Migrating from legacy ringing](/calls/ios/migrating-from-legacy-ringing).
+- [Background Handling](/calls/ios/background-handling): keeping the call running when the user leaves the app.
+- [iOS Push Notifications](/notifications/ios-push-notifications-sdk): chat notifications with the same SDK.
diff --git a/docs.json b/docs.json
index 96952f214..c3199d1b0 100644
--- a/docs.json
+++ b/docs.json
@@ -5602,7 +5602,8 @@
{
"group": "Migration Guide",
"pages": [
- "calls/ios/migration-guide-v5"
+ "calls/ios/migration-guide-v5",
+ "calls/ios/migrating-from-legacy-ringing"
]
},
{
diff --git a/notifications/ios-apns-push-notifications.mdx b/notifications/ios-apns-push-notifications.mdx
index 7b5e6930d..d873c606b 100644
--- a/notifications/ios-apns-push-notifications.mdx
+++ b/notifications/ios-apns-push-notifications.mdx
@@ -21,6 +21,15 @@ description: "Implement APNs push notifications with CometChat UIKit for iOS, in
Use this page when you need direct control over the PushKit and CallKit layer.
+
+ **This page wires legacy ringing by hand** (the Chat SDK's call flow, two providers registered with
+ `CometChatNotifications.registerPushToken`). With Push Notifications SDK 2.0 and Calls SDK 5.1, the SDK
+ registers both tokens under one provider and rings through CallKit for you: follow
+ [iOS Push Notifications](/notifications/ios-push-notifications-sdk) and
+ [Push Ringing](/calls/ios/voip-calling) instead, and do not combine them with the PushKit and CallKit
+ code on this page.
+
+
## What this guide covers
- CometChat dashboard setup (enable push, add APNs Device + APNs VoIP providers) with screenshots.
diff --git a/notifications/ios-fcm-push-notifications.mdx b/notifications/ios-fcm-push-notifications.mdx
index 9bf7987e1..408054e63 100644
--- a/notifications/ios-fcm-push-notifications.mdx
+++ b/notifications/ios-fcm-push-notifications.mdx
@@ -5,6 +5,15 @@ description: "Guide to integrating Firebase Cloud Messaging (FCM) push notificat
FCM doesn't support VoIP pushes; use APNs + PushKit for VoIP calls.
+
+ **This page wires FCM and legacy ringing by hand.** With Push Notifications SDK 2.0, forward the FCM
+ token with `registerFCMToken` and let the SDK register and unregister it. 2.0 takes a single provider id,
+ and an FCM provider cannot deliver VoIP pushes, so FCM suits chat-only apps; an app that rings calls uses an
+ APNs provider, with the SDK's PushKit and CallKit handling and Calls SDK 5.1. See
+ [iOS Push Notifications](/notifications/ios-push-notifications-sdk#fcm-instead-of-apns) and
+ [Push Ringing](/calls/ios/voip-calling).
+
+
- **This is the recommended way to add push notifications and VoIP calling to an iOS app.**
- `CometChatPushNotifications` handles APNs and PushKit token registration, foreground
- presentation, notification taps, quick reply, campaign receipts, badge counts and the whole
- PushKit + CallKit incoming-call flow. You do not write a `PKPushRegistry` or a
- `CXProviderDelegate`.
-
- It works with **or without** the UI Kit. If you would rather wire APNs, PushKit and CallKit
- yourself, see [iOS APNs Push Notifications](/notifications/ios-apns-push-notifications).
-
+
+ Source, and example apps for a calls-only app and a chat-and-calls app.
+
+
+## What this guide covers
+
+- APNs setup and the CometChat provider (dashboard, Xcode capabilities).
+- Adding the SDK's products and initialising it; the SDK registers and unregisters the push tokens itself.
+- Chat pushes: foreground banners, quick reply, delivery receipts and taps, handled by the SDK.
+- The Notification Service Extension for delivery receipts, avatars and campaign images.
+- App icon badge count from the `unreadMessageCount` field.
+- Call pushes: ringing through the SDK's Calls product, covered on [Push Ringing](/calls/ios/voip-calling).
+
+## How APNs and CometChat fit together
+
+- **Why APNs?** Apple issues the device tokens and delivers the pushes: a regular APNs token for chat notifications and, through PushKit, a VoIP token for calls.
+- **Why a CometChat provider?** The provider holds your APNs key and tells CometChat how to send to your app. The SDK registers each token against its id when the user logs in.
+- **What the SDK does with a push.** It presents chat notifications in the foreground, routes taps to your delegate, sends quick replies and campaign receipts, and (with its extension base class) marks messages delivered. VoIP pushes go to the Calls product, which rings the device through CallKit.
## Requirements
| | |
|---|---|
-| Minimum iOS | **15.1** |
-| Chat SDK | `CometChatSDK` **4.1.5+** |
-| Calls SDK | `CometChatCallsSDK` **5.0.0+** (required for the VoIP/CallKit flow) |
-| Device | **A physical device.** VoIP pushes are not delivered to the Simulator |
+| iOS | 15.1 or later |
+| Xcode | 15 or later (Swift 5.9) |
+| Chat SDK | `CometChatSDK` 4.1, for the Chat product |
+| Calls SDK | `CometChatCallsSDK` 5.1.0, for the Calls product |
+| Device | A physical device. Push notifications, PushKit and CallKit do not work in the Simulator |
+
+## 1. Prepare Apple and CometChat
+
+1. **CometChat dashboard**: go to **Notifications → Settings**, enable **Push Notifications**, click **Add Credentials** and add an APNs provider with your `.p8` auth key (Key ID, Team ID, Bundle ID). Copy its **Provider ID**.
+
+
+
+
+
+
+
+
+
+
+The SDK registers the APNs device token and the VoIP token under **one** provider id. Use a `.p8` auth key: a `.p12` certificate provider serves only one of the two tokens.
-## Install
+The provider's Bundle ID must be your app's bundle ID, and its APNs environment must match the build: **Enable Production Mode** off for builds run from Xcode, on for TestFlight and App Store builds. On a mismatch APNs rejects the pushes.
+
+
+2. **Xcode**, in your app target's **Signing & Capabilities**:
+ - **Push Notifications**.
+ - **Background Modes → Remote notifications**, so silent pushes (campaign receipts, badge updates) reach the app.
+ - **Background Modes → Voice over IP**, if calls should ring; see [Push Ringing](/calls/ios/voip-calling) for the rest of what calls need.
+ - **App Groups**, if you add the [Notification Service Extension](#7-notification-service-extension).
+
+3. **App constants**: note your CometChat App ID and Region, and the Provider ID. All three go into `PNConfiguration`.
+
+## 2. Add the SDK
+
+The SDK ships as three products, released together. Add **Core** always, and the others for what your app does:
+
+| Product / pod | Add it when | Depends on |
+|---|---|---|
+| `CometChatPushNotificationsSwift` (Core) | Always | No CometChat SDK |
+| `CometChatPushNotificationsChat` (Chat) | You show chat notifications: delivery receipts, quick reply, "open this chat" navigation, the Notification Service Extension | Core + `CometChatSDK` |
+| `CometChatPushNotificationsCalls` (Calls) | Incoming calls should ring: PushKit and CallKit | Core + `CometChatCallsSDK` |
+
+A chat-only app adds Core and Chat; a calls-only app adds Core and Calls; most apps add all three.
-In Xcode, **File → Add Package Dependencies** and add:
+In Xcode, **File → Add Package Dependencies**, enter:
```
https://github.com/cometchat/push-notifications-sdk-ios
```
-Pick version **1.0.0** or later, and add the **`CometChatPushNotificationsSwift`** library to your
-app target.
+Pick version **2.0.0** and add the products your app needs to the app target.
-```ruby
+```ruby Podfile
platform :ios, '15.1'
target 'YourApp' do
use_frameworks!
- pod 'CometChatPushNotifications', '1.0.0'
+ pod 'CometChatPushNotificationsSwift', '2.0.0' # Core, always
+ pod 'CometChatPushNotificationsChat', '2.0.0' # chat notifications
+ pod 'CometChatPushNotificationsCalls', '2.0.0' # only if incoming calls should ring
end
```
-Then `pod install`. The pod pulls `CometChatSDK` and `CometChatCallsSDK` automatically.
+Then run `pod install`. Each pod is its own module, and each brings the Chat or Calls SDK it depends on.
- The module you import is **`CometChatPushNotificationsSwift`**, not `CometChatPushNotifications`.
- The latter is the name of the class inside it.
+The Core module is **`CometChatPushNotificationsSwift`**; `CometChatPushNotifications` is the class inside it. Import the Chat and Calls modules in the files that use their delegate protocols.
-## Before you start
+## 3. Initialise the SDK
-1. In the **CometChat dashboard**, enable Push Notifications and add your APNs credentials. Add an
- **APNs Device** provider and an **APNs VoIP** provider, and copy the **Provider ID**.
-2. In Xcode, add these capabilities to your app target:
- - **Push Notifications**
- - **Background Modes** → **Voice over IP** and **Remote notifications**
-3. Add `NSMicrophoneUsageDescription` and `NSCameraUsageDescription` to your `Info.plist` — iOS
- terminates the app at the first call permission request without them.
+Call `initialize(config:)` in `application(_:didFinishLaunchingWithOptions:)`, **on every launch**, before the user logs in:
-## 1. Initialize
+- Token registration on login and logout only runs in a launch where `initialize` ran.
+- `initialize` sets the SDK as the `UNUserNotificationCenter` delegate. iOS requires that delegate before launch finishes, or the tap that launched the app is lost.
+- With the Calls product, `initialize` also creates the PushKit registry, which iOS requires by the end of launch.
+- It asks for notification permission, registers for remote notifications, installs every linked product and saves the configuration, so a launch started by a push still knows your app.
+- It sets the notification categories to the SDK's own `MessageCategory` (for quick reply). That replaces any categories your app set before; add yours back after `initialize`, including `MessageCategory`.
-Call this **after** `CometChat.init(...)`:
+Set the delegate first. The SDK holds it weakly, so keep a strong reference to it.
-```swift
+
+
+```swift AppDelegate.swift
+import UIKit
import CometChatPushNotificationsSwift
-CometChatPushNotifications.shared.initialize(config: .init(
- providerId: "YOUR_PROVIDER_ID",
- extensionGroupID: "group.com.yourcompany.yourapp" // optional — see the extension section
-))
-CometChatPushNotifications.shared.delegate = self
+@main
+final class AppDelegate: UIResponder, UIApplicationDelegate {
+
+ var window: UIWindow?
+
+ // The SDK holds its delegate weakly: the app keeps it alive.
+ let pushDelegate = PushDelegate()
+
+ func application(_ application: UIApplication,
+ didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
+ CometChatPushNotifications.shared.delegate = pushDelegate
+ CometChatPushNotifications.shared.initialize(config: PNConfiguration(
+ appId: "APP_ID",
+ region: "REGION",
+ providerId: "PROVIDER_ID",
+ extensionGroupID: "group.com.yourcompany.yourapp" // only with the service extension
+ ))
+ return true
+ }
+}
+```
+
+
+```objectivec AppDelegate.m
+@import CometChatPushNotificationsSwift;
+
+- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
+ // self.pushDelegate is a strong property: the SDK holds its delegate weakly.
+ CometChatPushNotifications.shared.delegate = self.pushDelegate;
+
+ PNConfiguration *config = [[[[PNConfiguration builderWithAppId:@"APP_ID" region:@"REGION"]
+ providerId:@"PROVIDER_ID"]
+ extensionGroupID:@"group.com.yourcompany.yourapp"]
+ build];
+ [CometChatPushNotifications.shared initializeWithConfig:config];
+ return YES;
+}
```
+
+
-That single call requests notification permission, registers for remote **and** VoIP pushes,
-listens for login and logout to re-register and unregister tokens, and takes over
-`UNUserNotificationCenter` handling.
+In a SwiftUI app, put this in an `AppDelegate` attached with `@UIApplicationDelegateAdaptor`.
-
- You do **not** call `CometChatNotifications.registerPushToken(...)` yourself. The SDK does it on
- every login, for both the APNs device token and the PushKit VoIP token.
-
+`PNConfiguration(appId:region:…)` takes every option as a defaulted parameter; from Objective-C, or to chain setters, use `PNConfiguration.builder(appId:region:)` and `build()`. With the Chat product, `PNConfigurationBuilder.from(appId:settings:)` starts a builder from the Chat SDK's `AppSettings`.
-## 2. Forward the APNs token
+| Option | Default | What it does |
+| --- | --- | --- |
+| `appId`, `region` | none; **required** | Your CometChat app. Must not be blank. The SDK registers tokens over its own connection, so it needs them even before any SDK is initialised |
+| `providerId` | `nil`; **required to register tokens** | The dashboard provider the tokens are registered under. Without it registration fails with `PNError.Code.providerRequired` and no push arrives |
+| `extensionGroupID` | `nil` | App Group shared with the [Notification Service Extension](#7-notification-service-extension), so the app and the extension do not both send a delivery receipt |
+| `enableBadgeCount` | `true` | Set the app icon badge from the payload; see [Badge count](#8-badge-count) |
+| `showInAppNotifications` | `true` | Show chat notifications while the app is in the foreground |
+| `requestNotificationPermission` | `true` | Show the notification prompt and register for remote notifications in `initialize`. Set `false` to ask later yourself |
+| `autoRegisterTokens` | `true` | Register the tokens on login and unregister them on logout, with no code of yours |
+| `tokenRegistrar` | `.auto` | Whose session registers the tokens: `.auto` (the Chat SDK if logged in, then the Calls SDK), `.chat` or `.calls` |
+| `authToken` | `nil` | A user auth token to register with when neither SDK is logged in. Not saved to disk |
+| `clientHost` (builder: `overrideClientHost(_:)`) | `nil` | Register tokens against this client host instead of the default one. Set it only if you give the Chat and Calls SDKs a client host override, and use the same value |
+| `callKit`, `requestCallPermissions`, `pushKitOwnership` | `CallKitBranding()`, `true`, `.sdk` | Calls; see [Push Ringing](/calls/ios/voip-calling#step-3-initialise-in-didfinishlaunching) |
-In your `AppDelegate`:
+## 4. Forward the APNs token
-```swift
+The APNs device token arrives in your `AppDelegate`; hand it to the SDK. The token is cached, so it does not matter whether it arrives before `initialize` or before login: the SDK registers it once someone is logged in.
+
+
+
+```swift AppDelegate.swift
func application(_ application: UIApplication,
didRegisterForRemoteNotificationsWithDeviceToken deviceToken: Data) {
CometChatPushNotifications.shared.registerDeviceToken(deviceToken)
}
-// Recommended — surfaces entitlement and provisioning problems during development.
+// Recommended: surfaces entitlement and provisioning problems during development.
func application(_ application: UIApplication,
didFailToRegisterForRemoteNotificationsWithError error: Error) {
CometChatPushNotifications.shared.handleRegistrationFailure(error)
}
-// Optional — silent/background pushes (campaign receipts, badge updates).
+// Silent pushes: campaign receipts and badge updates.
func application(_ application: UIApplication,
didReceiveRemoteNotification userInfo: [AnyHashable: Any],
fetchCompletionHandler completionHandler: @escaping (UIBackgroundFetchResult) -> Void) {
@@ -112,164 +207,292 @@ func application(_ application: UIApplication,
completionHandler(.newData)
}
```
+
+
+```objectivec AppDelegate.m
+- (void)application:(UIApplication *)application didRegisterForRemoteNotificationsWithDeviceToken:(NSData *)deviceToken {
+ [CometChatPushNotifications.shared registerDeviceToken:deviceToken completion:nil];
+}
+
+- (void)application:(UIApplication *)application didFailToRegisterForRemoteNotificationsWithError:(NSError *)error {
+ [CometChatPushNotifications.shared handleRegistrationFailure:error];
+}
+
+- (void)application:(UIApplication *)application didReceiveRemoteNotification:(NSDictionary *)userInfo
+ fetchCompletionHandler:(void (^)(UIBackgroundFetchResult))completionHandler {
+ [CometChatPushNotifications.shared handleBackgroundNotificationWithUserInfo:userInfo];
+ completionHandler(UIBackgroundFetchResultNewData);
+}
+```
+
+
-`registerDeviceToken` takes optional `onSuccess` / `onError` closures if you want the result.
+`registerDeviceToken` also takes an optional completion with a `PNResult`. The VoIP token needs no code: the Calls product receives it from its own PushKit registry.
- The token can arrive before `initialize(config:)` runs. The SDK caches it and registers it
- automatically once you initialize and the user logs in, so ordering is not your problem.
+A calls-only app (Core and Calls, no Chat) registers only the VoIP token. It can skip this step and set `requestNotificationPermission: false`, since calls ring without notification permission.
-That is the only `AppDelegate` code required. There is no `PKPushRegistryDelegate` and no
-`CXProviderDelegate` to write.
+If your app must keep its own `UNUserNotificationCenter` delegate, set it after `initialize` and forward to `handleWillPresentNotification(_:completionHandler:)` and `handleNotificationResponse(_:completionHandler:)`, or the SDK no longer sees foreground notifications and taps.
-## 3. Implement the delegate
+## 5. Handle notification taps
-Every method has a default no-op implementation — override only what you need.
+One delegate object serves every product. Adopt `CometChatPushNotificationsDelegate`, plus `CometChatPushNotificationsChatDelegate` if you link the Chat product, and implement only what you need; every method is optional.
-```swift
-extension MyRootCoordinator: CometChatPushNotificationsDelegate {
+
+
+```swift PushDelegate.swift
+import UIKit
+import CometChatSDK
+import CometChatPushNotificationsSwift
+import CometChatPushNotificationsChat
- // MARK: Navigation (notification taps)
- func navigateToChat(for user: User) { /* push your 1-1 chat screen */ }
- func navigateToChat(for group: Group) { /* push your group chat screen */ }
- func navigateToDefaultScreen() { /* fallback destination */ }
+final class PushDelegate: NSObject, CometChatPushNotificationsChatDelegate {
- // MARK: Calls
- // The call is ALREADY accepted on the server when this fires. Present your call UI
- // and start the session (generate a token, then join).
- func presentCallScreen(for call: Call, sessionId: String) { }
+ // Chat product: a tapped chat notification.
+ func navigateToChat(for user: User) { /* open the 1:1 conversation */ }
+ func navigateToChat(for group: Group) { /* open the group conversation */ }
- // The SDK has already ended the call and the CallKit session — just dismiss your UI.
- func onCallCleanupComplete() { }
+ // A tapped campaign notification sent from the dashboard (already marked clicked).
+ func navigateToNotificationsFeed() { /* open your notifications screen */ }
- // MARK: Optional observability
- func onCallMissed(call: Call, reason: CometChatMissedCallReason) { }
- func onCallMuteStateChanged(isMuted: Bool) { }
+ // A tap no product recognised, or any tap in an app without the Chat product.
+ func didTapNotification(_ info: CometChatNotificationInfo) { /* info.rawPayload has everything */ }
+
+ // Fallback when the matching method above is not implemented.
+ func navigateToDefaultScreen() { }
+
+ // Token observability. Fires for automatic registrations too: the fastest way to catch a wrong provider id.
func onPushTokenRegistered(platform: CometChatPushTokenPlatform) { }
- func onPushTokenRegistrationFailed(platform: CometChatPushTokenPlatform,
- error: CometChatException) {
- // Fires for automatic registrations too — the fastest way to catch a wrong Provider ID.
+ func onPushTokenRegistrationFailed(platform: CometChatPushTokenPlatform, error: PNError) {
+ print("Push token \(platform.rawName) not registered: \(error.code.name) \(error.message)")
}
}
```
+
+
+```objectivec PushDelegate.m
+@import CometChatSDK;
+@import CometChatPushNotificationsSwift;
+@import CometChatPushNotificationsChat;
+
+@interface PushDelegate : NSObject
+@end
+
+@implementation PushDelegate
+- (void)navigateToChatForUser:(User *)user { /* open the 1:1 conversation */ }
+- (void)navigateToChatForGroup:(Group *)group { /* open the group conversation */ }
+- (void)navigateToNotificationsFeed { /* open your notifications screen */ }
+- (void)navigateToDefaultScreen { }
+- (void)onPushTokenRegistrationFailedWithPlatform:(CometChatPushTokenPlatform)platform error:(PNError *)error {
+ NSLog(@"Push token not registered: %@", error.message);
+}
+@end
+```
+
+
+
+A tap that launches the app reaches the delegate early, possibly before your UI is up. Keep the destination and navigate once your root screen exists.
+
+
+**SwiftUI: keep the delegate a plain `NSObject`.** The SDK holds its delegate as an Objective-C weak reference. Do not make an `ObservableObject` with `@Published` properties the delegate; have a small `NSObject` delegate forward to it.
+
| Type | Values |
|---|---|
-| `CometChatPushTokenPlatform` | `.apns` · `.fcm` · `.voip` |
-| `CometChatMissedCallReason` | `.unanswered` · `.cancelled` |
+| `CometChatPushTokenPlatform` | `.apns`, `.fcm`, `.voip` |
+| `PNError.Code` | `.notInitialised`, `.notLoggedIn`, `.providerRequired`, `.productNotInstalled`, `.serverError`, `.networkError` |
+
+**Suppress notifications for the open chat.** While a conversation is on screen, its notifications are not shown in the foreground:
+
+```swift
+// In your messages screen
+override func viewWillAppear(_ animated: Bool) {
+ super.viewWillAppear(animated)
+ CometChatPushNotifications.shared.setActiveConversation(userId: user.uid ?? "") // or setActiveConversation(groupId:)
+}
+
+override func viewWillDisappear(_ animated: Bool) {
+ super.viewWillDisappear(animated)
+ CometChatPushNotifications.shared.clearActiveConversation()
+}
+```
-## Cold start: tell the SDK when the Calls SDK is ready
+## 6. Tokens, login and logout
-A VoIP push can wake your app *before* your Calls SDK is ready. The SDK buffers the
-`presentCallScreen` callback until you tell it to release.
+You do not register push tokens. When the user logs in to the Chat or Calls SDK, the SDK registers the APNs device token and, with the Calls product, the VoIP token, under your `providerId`. It registers a rotated token again, and if someone is already logged in when `initialize` runs it registers the cached tokens then.
-Call `notifyCallsSDKReady()` once **login has resolved** — not merely when init succeeds:
+To log out, **unregister first and wait for it**: the request is authenticated with the user's session, so it has to finish before that session ends.
```swift
-CometChatCalls.init(callsAppSettings: settings, onSuccess: { _ in
- CometChatCalls.login(authToken: token, onSuccess: { _ in
- // Release the buffered call only now — joining needs an authenticated session.
- CometChatPushNotifications.shared.notifyCallsSDKReady()
- }, onError: { _ in })
-}, onError: { _ in })
+CometChatPushNotifications.shared.unregisterTokens(completion: { _ in
+ CometChat.logout(onSuccess: { _ in }, onError: { _ in })
+ // With the Calls SDK, also log out of it: CometChatCalls.logout(...)
+})
```
-
- **Release it after login, not after init.** `notifyCallsSDKReady()` only lifts the buffer — it
- does not check whether anyone is logged in. Calling it at init success releases the call while
- the session is still unauthenticated, and the `joinSession` that follows `presentCallScreen`
- fails on auth.
-
+On logout the SDK also ends a call still ringing for the old user (and an answered call in progress, as `.failed`) and clears the app badge. The tokens stay cached on the device and are registered again for the next user who logs in.
-
- There is a **3-second safety timeout**: if `notifyCallsSDKReady()` has not been called by then,
- the SDK fires the buffered `presentCallScreen` anyway so the call is never silently dropped. A
- cold start whose login takes longer than 3 seconds will therefore reach `presentCallScreen`
- before login completes — handle that in your call screen rather than assuming a live session.
-
+`registerTokens(completion:)` registers the cached tokens on demand. `autoRegisterTokens: false` stops registration in `initialize` and on login, and unregistration on logout; a token delivered or rotated while a user is logged in is still registered.
-## Notification Service Extension
+## 7. Notification Service Extension
-Adds delivery receipts, sender avatars, campaign images and markdown-free notification text.
+The extension gives chat notifications delivery receipts, the sender's avatar, campaign images and plain text in place of markdown. Delivery receipts for chat messages come from the extension, so add it if your app shows read and delivered ticks.
**File → New → Target → Notification Service Extension.**
+
+ Add `CometChatPushNotificationsSwift` and `CometChatPushNotificationsChat` to the extension target, and **never** the Calls product: an extension runs under a memory limit the Calls SDK does not fit in.
+
- Add the same App Group (e.g. `group.com.yourcompany.yourapp`) to **both** the app target and the
- extension target, and pass it as `extensionGroupID` when you initialize.
+ Add the same App Group (for example `group.com.yourcompany.yourapp`) to **both** the app target and the extension target, and pass it as `extensionGroupID` in `PNConfiguration`.
- Link `CometChatPushNotificationsSwift` to the extension target and replace the generated class:
+ Replace the generated class:
-```swift
-import CometChatPushNotificationsSwift
+```swift NotificationService.swift
+import CometChatPushNotificationsChat
class NotificationService: CometChatNotificationServiceExtension {}
```
- Add a String entry `CometChatExtensionGroupID` set to your App Group ID in the **extension's**
- `Info.plist`, or override the `extensionGroupID` property instead.
+ Add a String entry `CometChatExtensionGroupID` set to your App Group id in the **extension's** `Info.plist`, or override the `extensionGroupID` property.
-Optional overrides: `stripsMarkdown`, `attachesMedia`, and `finalizeContent(_:)` for last-chance
-content customization.
+Optional overrides: `stripsMarkdown` and `attachesMedia` (both `true`), and `finalizeContent(_:)` to change the content last, after receipts, media and markdown.
+
+## 8. Badge count
+
+CometChat's push payload can carry an `unreadMessageCount` field: the total unread messages across all conversations for the logged-in user.
+
+1. Go to **CometChat Dashboard → Notifications → Settings → Preferences → Push Notification Preferences**.
+2. Scroll to the bottom and enable the **Unread Badge Count** toggle.
+
+With `enableBadgeCount` on (the default), the SDK sets the app icon badge from `unreadMessageCount` when it is a number: on a chat notification shown in the foreground (with `showInAppNotifications` on), a silent push handed to `handleBackgroundNotification`, and a tap. `clearBadgeCount()` does nothing while `enableBadgeCount` is off. Clear it with `CometChatPushNotifications.shared.clearBadgeCount()`, for example when the app becomes active; logout clears it too.
-## Configuration reference
+To set the badge on every delivered notification, including in the background, and also when the payload carries the count as a string, set it in the extension:
+
+```swift NotificationService.swift
+import UserNotifications
+import CometChatPushNotificationsChat
+
+class NotificationService: CometChatNotificationServiceExtension {
+ override func finalizeContent(_ content: UNMutableNotificationContent) -> UNMutableNotificationContent {
+ let raw = content.userInfo["unreadMessageCount"]
+ if let count = (raw as? Int) ?? (raw as? String).flatMap({ Int($0) }), count >= 0 {
+ content.badge = NSNumber(value: count)
+ }
+ return content
+ }
+}
+```
+
+## 9. Message pushes
+
+Handled by the SDK, with nothing to build:
+
+- **Foreground**: a banner, unless that conversation is open (`setActiveConversation`) or `showInAppNotifications` is `false`.
+- **Quick reply**: the SDK registers the notification category `MessageCategory` with a **Reply** action. A reply typed there is sent as a text message into the conversation by the Chat product.
+- **Delivery receipts, avatars, markdown**: from the [Notification Service Extension](#7-notification-service-extension).
+- **Taps**: routed to `navigateToChat(for:)`; see [Handle notification taps](#5-handle-notification-taps).
+
+Campaign (engagement) pushes sent from the dashboard are marked delivered (in the foreground, from `handleBackgroundNotification` and in the extension) and clicked (on tap) when the Chat product is linked; a tap calls `navigateToNotificationsFeed()`.
+
+`CometChatPushNotifications.parseNotificationInfo(from:)` reads the sender, receiver and body out of a raw payload, and `isCampaignNotification(userInfo:)` tells campaign pushes apart.
+
+## 10. Call pushes
+
+Handled by the Calls product: it owns the PushKit registry, rings through CallKit in every app state, connects to the call while it rings, and calls your `presentCall(_:)` when the user answers, where you join the call. It sends declines and timeouts, and CallKit records missed calls in Recents.
+
+Setup, capabilities, joining and customising CallKit are on [Push Ringing](/calls/ios/voip-calling). Starting and ending calls is on [Ringing](/calls/ios/ringing).
+
+## Ring readiness and diagnostics
+
+After sign-in, check what could silence a notification or a ring:
```swift
-CometChatPushNotificationsConfig(
- providerId: String, // required — from the CometChat dashboard
- callkitIconName: String? = nil, // asset name for the CallKit icon
- callkitRingtoneName: String? = nil, // custom ringtone file
- enableBadgeCount: Bool = true,
- showInAppNotifications: Bool = true, // foreground banners
- extensionGroupID: String? = nil, // must match the Notification Service Extension
- foregroundCallPresentation: .callKit, // .callKit | .inApp | .none
- incomingCallStyle: CometChatIncomingCallStyle? = nil,
- notificationContentModifier: ((UNMutableNotificationContent, CometChatNotificationInfo) -> UNMutableNotificationContent)? = nil
-)
+CometChatPushNotifications.shared.ensureRingReady { result in
+ guard case .success(let readiness) = result, readiness != .ready else { return }
+ // .notificationsOff, .providerMissing, and with the Calls product
+ // .backgroundModeMissing, .voipEntitlementMissing, .microphoneDenied
+ print("Push not ready: \(readiness.name)")
+}
```
-`foregroundCallPresentation` decides what happens when a call arrives while the app is open:
+`getDiagnostics()` returns a redacted dictionary of everything that affects delivery (products, login state, cached tokens, background modes) for bug reports. `verifyPushSetup(completion:)` registers the tokens and reports the outcome.
-| Value | Behaviour |
-|---|---|
-| `.callKit` | Full-screen system CallKit UI (default) |
-| `.inApp` | The SDK's own `CometChatIncomingCallView`, stylable via `incomingCallStyle` |
-| `.none` | Nothing — you present your own UI from the delegate |
+## FCM instead of APNs
+
+APNs is the recommended mode on iOS, and FCM mode suits chat-only apps. If your app already routes APNs through Firebase and does not ring calls, configure Firebase as usual and forward the FCM token; the SDK links nothing from Firebase:
+
+```swift
+func messaging(_ messaging: Messaging, didReceiveRegistrationToken fcmToken: String?) {
+ if let token = fcmToken {
+ CometChatPushNotifications.shared.registerFCMToken(token)
+ }
+}
+```
+
+The FCM token is registered under the same `providerId` as the other tokens; 2.0 takes a single provider id for every token. An FCM provider cannot deliver VoIP pushes, so an app that rings calls must use an APNs provider instead.
+
+## Upgrading from 1.x
-## Other APIs
+2.0 is a breaking release:
-| API | Use |
+| 1.x | 2.0 |
|---|---|
-| `setActiveConversation(userId:)` / `setActiveConversation(groupId:)` | Suppress notifications for the chat the user is already looking at |
-| `clearActiveConversation()` | Call when that screen closes |
-| `clearBadgeCount()` | Reset the app badge |
-| `CometChatPushNotifications.parseNotificationInfo(from:)` | Read sender, receiver and body out of a raw payload |
-| `CometChatPushNotifications.isCampaignNotification(userInfo:)` | Distinguish campaign pushes from chat pushes |
-| `activeCallSessionId` | The session ID of the call currently in progress, if any |
+| One pod, `CometChatPushNotifications` | Three products: Core, Chat, Calls |
+| `CometChatPushNotificationsConfig(providerId:…)` | `PNConfiguration(appId:region:providerId:…)`; `appId` and `region` are required |
+| `navigateToChat(for:)` on the core delegate | On `CometChatPushNotificationsChatDelegate` (Chat product) |
+| `presentCallScreen(for:sessionId:)` with a Chat SDK `Call` | `presentCall(_ call: IncomingCall) -> Bool` on `CometChatPushNotificationsCallsDelegate`; join with the Calls SDK |
+| `onCallCleanupComplete`, `onCallMissed` | `callDidEnd(_:reason:)` |
+| `notifyCallsSDKReady()` | Removed: the SDK brings the Calls SDK up itself |
+| `foregroundCallPresentation`, `CometChatIncomingCallView`, `incomingCallStyle` | Removed: calls always ring on CallKit |
+| `callkitIconName`, `callkitRingtoneName` | `CallKitBranding` in `callKit` |
+| `onCallMuteStateChanged`, `activeCallSessionId` | Removed |
+| `onPushTokenRegistrationFailed(platform:error: CometChatException)` | `error: PNError` |
+| `notificationContentModifier` | Still accepted, but has no effect in 2.0 |
+
+Ringing itself moved from the Chat SDK's call flow to the Calls SDK; see [Migrating from legacy ringing](/calls/ios/migrating-from-legacy-ringing).
## Testing checklist
-- Run on a **physical device** — VoIP pushes never reach the Simulator.
-- Confirm both providers exist in the dashboard and that `providerId` matches the one you passed.
-- Watch `onPushTokenRegistrationFailed` — a wrong Provider ID shows up here first.
-- Test an incoming call in all three states: foreground, background, and app terminated
- (cold start — this is what `notifyCallsSDKReady()` covers).
+1. Install on a physical device and allow notifications.
+2. Log in, and check `onPushTokenRegistered` fires (or `getDiagnostics()`).
+3. Send a message from another user:
+ - Foreground: a banner appears unless that chat is open.
+ - Background or killed: tapping it opens the right conversation.
+4. Reply from the notification: the message arrives in the conversation.
+5. With the extension: the sender sees the message delivered, and the avatar shows.
+6. With the Calls product, call from another user with the app in the foreground, in the background, killed, and with the phone locked; see [Push Ringing](/calls/ios/voip-calling).
+7. Log out and confirm no further pushes arrive; log in as another user and confirm theirs do.
+
+## Troubleshooting
+
+| Symptom | Quick checks |
+| --- | --- |
+| No notifications | Physical device; Push Notifications capability; Push Notifications enabled in the dashboard and the Provider ID correct; the user logged in (tokens register on login). `onPushTokenRegistrationFailed` says why |
+| `providerRequired` | `providerId` missing from `PNConfiguration` |
+| Tap does nothing | Delegate not set, released (it is held weakly), or not adopting `CometChatPushNotificationsChatDelegate`; another object took over `UNUserNotificationCenter`'s delegate |
+| Tap that launches the app does nothing | `initialize` not called in `didFinishLaunching` |
+| No delivery receipts or avatars | Notification Service Extension missing, or its App Group or `CometChatExtensionGroupID` does not match `extensionGroupID` |
+| Banners in the foreground for the open chat | `setActiveConversation` not called for that screen |
+| Badge not showing | **Unread Badge Count** enabled in the dashboard; `enableBadgeCount` on; set it in the extension if the count arrives as a string |
+| Still receiving pushes after logout | `unregisterTokens` not awaited before logout |
+| Calls do not ring | See [Push Ringing → Troubleshooting](/calls/ios/voip-calling#troubleshooting) |
## Related
Dashboard setup, providers and templates.
-
- The hand-wired alternative, if you need full control over PushKit and CallKit.
+
+ Incoming calls through PushKit and CallKit with the same SDK.
-
- The in-app 1:1 call signaling this builds on.
+
+ The hand-wired alternative, with the UI Kit and legacy ringing.
diff --git a/sdk/ios/all-real-time-delegates-listeners.mdx b/sdk/ios/all-real-time-delegates-listeners.mdx
index 8bd1f8358..3a591e479 100644
--- a/sdk/ios/all-real-time-delegates-listeners.mdx
+++ b/sdk/ios/all-real-time-delegates-listeners.mdx
@@ -8,7 +8,7 @@ description: "Complete reference for all CometChat iOS SDK real-time delegates i
- **User delegate:** `CometChat.userdelegate = self` (conform to `CometChatUserDelegate`) — `onUserOnline(user:)`, `onUserOffline(user:)`
- **Group delegate:** `CometChat.groupdelegate = self` (conform to `CometChatGroupDelegate`) — member joined/left/kicked/banned events
- **Message delegate:** `CometChat.messagedelegate = self` (conform to `CometChatMessageDelegate`) — message received/edited/deleted/reaction events
-- **Call delegate:** `CometChat.calldelegate = self` (conform to `CometChatCallDelegate`) — incoming/outgoing call events
+- **Call delegate (superseded):** `CometChat.calldelegate = self` (conform to `CometChatCallDelegate`) — legacy incoming/outgoing call events; new apps use the Calls SDK's `CometChatCalls.addIncomingCallListener` ([Ringing](/calls/ios/ringing))
- **Login delegate:** `CometChat.addLoginListener(_:_:)` (conform to `CometChatLoginDelegate`) — login/logout success/failure events
- **Connection delegate:** `CometChat.addConnectionListener(_:_:)` (conform to `CometChatConnectionDelegate`) — connection status events
- **AI Assistant delegate:** `CometChat.addAIAssistantListener(_:_:)` (conform to `AIAssistantEventsDelegate`) — AI events
@@ -324,6 +324,10 @@ extension ViewController: CometChatMessageDelegate {
## Call Delegate Methods
+
+**Superseded.** `CometChatCallDelegate`, `CometChat.calldelegate` and `addCallListener(_:_:)` are no longer the way calls announce themselves, and a coming Chat SDK release marks them deprecated together with the Chat SDK's other calling methods. Calls now ring through the Push Notifications SDK (PushKit and CallKit) and announce themselves on the Calls SDK's `CometChatCalls.addIncomingCallListener` (`onIncomingCallReceived(call:)`, `onIncomingCallEnded(call:reason:)`), whether the call arrived by push to a killed app or while the app was running. The caller's "answered" event is `ParticipantEventListener.onParticipantJoined(participant:)` on the prepared session. See [Ringing](/calls/ios/ringing) and [Migrating from Legacy Ringing](/calls/ios/migrating-from-legacy-ringing). The section below is kept for apps still on the legacy flow.
+
+
The `CometChatCallDelegate` provides you with live events related to calls. Below are the callback methods provided by the `CometChatCallDelegate`.
| Method | Information |
diff --git a/sdk/ios/calling-overview.mdx b/sdk/ios/calling-overview.mdx
index 9688e322e..f2a211358 100644
--- a/sdk/ios/calling-overview.mdx
+++ b/sdk/ios/calling-overview.mdx
@@ -1,10 +1,16 @@
---
title: "Calling"
sidebarTitle: "Calling"
-description: "Voice and video calling integration with CometChat for iOS, including one-on-one calls, group calls, and conferences."
+description: "Voice and video calling for iOS: the Calls SDK owns calls and ringing, the Push Notifications SDK rings the device through CallKit, and the Chat SDK's calling methods are superseded."
---
-CometChat's Calling SDK enables you to add voice and video calling to your iOS application. The Calling SDK works alongside the Chat SDK to provide a complete communication experience including 1-on-1 calls, group calls, and conference sessions.
+Voice and video calling on iOS is provided by the CometChat Calls SDK, with incoming calls rung by the Push Notifications SDK through PushKit and CallKit. The Chat SDK is used for chat; it no longer takes part in calling.
+
+
+**The Chat SDK's calling APIs are superseded.** `CometChat.initiateCall`, `acceptCall`, `rejectCall`, `endCall`, `startCall`, `getActiveCall`, `clearActiveCall`, `getCallParticipantCount`, `addCallListener` / `removeCallListener`, `CometChat.calldelegate`, `CometChatCallDelegate` and `CallSettings` still work, and a coming Chat SDK release marks them deprecated. Their replacement is the Calls SDK: `CometChatCalls.initiateCall`, `endCall(sessionID:status:)`, `joinSession`, `CallSession.shared.leave()` and `addIncomingCallListener`. Ringing the callee is done by the Push Notifications SDK's Calls product. See [Ringing](/calls/ios/ringing) and [Migrating from Legacy Ringing](/calls/ios/migrating-from-legacy-ringing).
+
+Not deprecated: the `Call` message model and its properties, and the `CometChat.callStatus` and `CometChat.CallType` values, because call messages in a conversation's history still use them.
+
## Key Features
@@ -12,7 +18,7 @@ CometChat's Calling SDK enables you to add voice and video calling to your iOS a
- **Screen Sharing** — Share your screen during calls for collaboration
- **Call Recording** — Record calls and access recordings later
- **Call Logs** — Track call history with detailed metadata
-- **Ringing & Notifications** — Built-in call ringing flow with accept/reject
+- **Ringing & Notifications** — Incoming calls ring through the Push Notifications SDK and CallKit, with the app in any state
- **Customizable UI** — Presenter mode and video view customization
## Choose Your Version
@@ -27,5 +33,5 @@ CometChat's Calling SDK enables you to add voice and video calling to your iOS a
-If you are using CometChat UI Kits, calling is already integrated. The Calling SDK is only needed for custom calling implementations.
+If you are using CometChat UI Kits, calling is already integrated; see [Calling Integration](/ui-kit/ios/calling-integration). The Calls SDK is only needed for custom calling implementations.