From 9588bb09d4522e45556808b67a29db769ab88695 Mon Sep 17 00:00:00 2001 From: Ashfaaq Ali Date: Thu, 3 Sep 2026 12:54:15 +0530 Subject: [PATCH 1/5] fix(calls/ios): 8 defects that do not compile against CometChatCallsSDK 5.0.4 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Found while building the headless iOS calling skill (ENG-38735) by extracting every Swift fence under /calls/ios/** and type-checking it against the SHIPPED CometChatCallsSDK 5.0.4 + CometChatSDK 4.1.7 frameworks. Each defect is confirmed three ways: the live PR #482 preview, the shipped .swiftinterface, and the calls-core/ios-sdk source. D1 AudioModeType does not exist — 17 occurrences across 6 pages, including a whole "AudioModeType Values" accordion. The real enum is AudioMode (calls-core CallModels.swift:305); 0 hits in the interface AND 0 in the binary's exported symbols. The fix is NOT a rename: the docs' example implements MediaEventsListener, whose real signature is onAudioModeChanged(audioMode: AudioMode) — so the parameter LABEL was wrong too, along with the ObjC selector and the AudioModeType*Speaker constants. D2 CometChat.CallStatus -> CometChat.callStatus (lowercase c). ringing x2. rejectCall(sessionID:status:) takes CometChat.callStatus (CometChatSDK 4.1.7 L549/L1816). Swift is case-sensitive; the documented line cannot compile. D3 .setType(.video) -> .setSessionType(.video) — 8 occurrences across 5 pages. SessionSettingsBuilder has no setType (calls-core SessionSettingsBuilder.swift:120). Includes migration-guide-v5, where it sat under the *v5* tab, i.e. presented as the new API. D4 The SPM URL 404s. github.com/cometchat/cometchat-calls-sdk-ios returns 404; the package is at github.com/cometchat/calls-sdk-ios (its Package.swift declares name "CometChatCallsSDK"). This is step 2 of installation, so it breaks before a reader writes a line of code. D5 region documented as "us or eu" — `in` is also valid and is what several live apps use. The SDK does not restrict it (calls-core CometChatCalls.swift only guards region.length > 0), and the CHAT SDK docs already say "us", "eu", "in". 3 occurrences. D6 .setAppId()/.setRegion() are @available(deprecated) in 5.0.4 — "Use set(appID:) instead" (calls-core CallAppSettingsBuilder.swift:70-78). Both spellings are public, so this compiles with a warning rather than failing; the docs should teach the supported pair. D7 call.callInitiator?.name does not compile. callInitiator is AppEntity? (CometChatSDK 4.1.7 L950) and `name` lives on User : AppEntity (L1864/1869), so it needs a downcast. The ObjC variant has the same bug. D8 The migration guide's init snippet says "No changes required" and then shows CallAppSettings() followed by .set(appId:)/.set(region:). CallAppSettings has ONLY init() — no setters at all (calls-core CallAppSettings.swift:12); the setters are on CallAppSettingsBuilder, and it is set(appID:) with a capital ID. Corrected to the builder + .build(), and verified to compile. Verification: all 8 classes now return 0 across all 25 /calls/ios pages, and the corrected fences type-check. Of the 50 fences on the edited pages, the only remaining failures are snippet fragments referencing reader-supplied variables (authToken, sessionID, callViewContainer, listener instances) — expected in documentation, not defects. NOT changed, deliberately: bare `AudioMode` was initially flagged as ambiguous, but that was an artifact of the test harness importing BOTH SDKs. These pages import only the Calls SDK, so bare AudioMode is correct here. The ambiguity is real for an app doing 1:1 ringing (which needs both SDKs) and is recorded in the skill instead of over-qualifying every page. Co-Authored-By: Claude Opus 5 --- calls/ios/audio-modes.mdx | 14 +++++++------- calls/ios/custom-control-panel.mdx | 2 +- calls/ios/events.mdx | 8 ++++---- calls/ios/idle-timeout.mdx | 2 +- calls/ios/join-session.mdx | 6 +++--- calls/ios/migration-guide-v5.mdx | 15 ++++++++------- calls/ios/recording.mdx | 2 +- calls/ios/ringing.mdx | 10 +++++----- calls/ios/session-settings.mdx | 14 +++++++------- calls/ios/setup.mdx | 12 ++++++------ 10 files changed, 43 insertions(+), 42 deletions(-) diff --git a/calls/ios/audio-modes.mdx b/calls/ios/audio-modes.mdx index 9f1d04c91..8219cd011 100644 --- a/calls/ios/audio-modes.mdx +++ b/calls/ios/audio-modes.mdx @@ -43,7 +43,7 @@ CometChatCalls.joinSession( ```objectivec SessionSettings *sessionSettings = [[[CometChatCalls sessionSettingsBuilder] - setAudioMode:AudioModeTypeSpeaker] + setAudioMode:AudioModeSpeaker] build]; [CometChatCalls joinSessionWithSessionID:sessionId @@ -113,8 +113,8 @@ class CallViewController: UIViewController, MediaEventsListener { CallSession.shared.removeMediaEventsListener(self) } - func onAudioModeChanged(audioModeType: AudioModeType) { - switch audioModeType { + func onAudioModeChanged(audioMode: AudioMode) { + switch audioMode { case .speaker: print("Switched to speaker") case .earpiece: @@ -127,7 +127,7 @@ class CallViewController: UIViewController, MediaEventsListener { break } // Update audio mode button icon - updateAudioModeIcon(audioModeType) + updateAudioModeIcon(audioMode) } // Other callbacks... @@ -159,9 +159,9 @@ class CallViewController: UIViewController, MediaEventsListener { [[CallSession shared] removeMediaEventsListener:self]; } -- (void)onAudioModeChangedWithAudioModeType:(AudioModeType)audioModeType { +- (void)onAudioModeChangedWithAudioMode:(AudioMode)audioMode { // Update audio mode button icon - [self updateAudioModeIcon:audioModeType]; + [self updateAudioModeIcon:audioMode]; } // Other callbacks... @@ -187,7 +187,7 @@ let sessionSettings = CometChatCalls.sessionSettingsBuilder ```objectivec SessionSettings *sessionSettings = [[[[CometChatCalls sessionSettingsBuilder] - setAudioMode:AudioModeTypeSpeaker] + setAudioMode:AudioModeSpeaker] hideAudioModeButton:YES] build]; ``` diff --git a/calls/ios/custom-control-panel.mdx b/calls/ios/custom-control-panel.mdx index e38d02327..c5c4e09c5 100644 --- a/calls/ios/custom-control-panel.mdx +++ b/calls/ios/custom-control-panel.mdx @@ -361,7 +361,7 @@ extension CallViewController: MediaEventsListener { func onRecordingStopped() {} func onScreenShareStarted() {} func onScreenShareStopped() {} - func onAudioModeChanged(audioModeType: AudioModeType) {} + func onAudioModeChanged(audioMode: AudioMode) {} func onCameraFacingChanged(cameraFacing: CameraFacing) {} } ``` diff --git a/calls/ios/events.mdx b/calls/ios/events.mdx index 9440bd09a..64e67b8a4 100644 --- a/calls/ios/events.mdx +++ b/calls/ios/events.mdx @@ -279,7 +279,7 @@ class CallViewController: UIViewController, MediaEventsListener { func onScreenShareStarted() {} func onScreenShareStopped() {} - func onAudioModeChanged(audioModeType: AudioModeType) { + func onAudioModeChanged(audioMode: AudioMode) { // Audio output device changed } @@ -329,7 +329,7 @@ class CallViewController: UIViewController, MediaEventsListener { // Call recording stopped } -- (void)onAudioModeChangedWithAudioModeType:(AudioModeType)audioModeType { +- (void)onAudioModeChangedWithAudioMode:(AudioMode)audioMode { // Audio output device changed } @@ -354,11 +354,11 @@ class CallViewController: UIViewController, MediaEventsListener { | `onRecordingStopped` | - | Call recording stopped | | `onScreenShareStarted` | - | You started screen sharing | | `onScreenShareStopped` | - | You stopped screen sharing | -| `onAudioModeChanged` | `AudioModeType` | Audio output device changed | +| `onAudioModeChanged` | `AudioMode` | Audio output device changed | | `onCameraFacingChanged` | `CameraFacing` | Camera switched between front and back | - + | Value | Description | |-------|-------------| | `.speaker` | Audio routed through device loudspeaker | diff --git a/calls/ios/idle-timeout.mdx b/calls/ios/idle-timeout.mdx index 0cd70de52..887a15f6d 100644 --- a/calls/ios/idle-timeout.mdx +++ b/calls/ios/idle-timeout.mdx @@ -36,7 +36,7 @@ Set the idle timeout period using `setIdleTimeoutPeriod()` in `SessionSettingsBu ```swift let sessionSettings = CometChatCalls.sessionSettingsBuilder .setIdleTimeoutPeriod(120) // 2 minutes - .setType(.video) + .setSessionType(.video) .build() CometChatCalls.joinSession( diff --git a/calls/ios/join-session.mdx b/calls/ios/join-session.mdx index 8c4bfd0f8..f7bda4efb 100644 --- a/calls/ios/join-session.mdx +++ b/calls/ios/join-session.mdx @@ -62,7 +62,7 @@ let sessionId = "SESSION_ID" let sessionSettings = CometChatCalls.sessionSettingsBuilder .setDisplayName("John Doe") - .setType(.video) + .setSessionType(.video) .build() CometChatCalls.joinSession( @@ -162,7 +162,7 @@ Use the generated token to join the session. This gives you control over when an ```swift let sessionSettings = CometChatCalls.sessionSettingsBuilder .setDisplayName("John Doe") - .setType(.video) + .setSessionType(.video) .build() // Use the previously generated token @@ -221,7 +221,7 @@ CometChatCalls.generateToken(sessionID: sessionId, onSuccess: { [weak self] toke // Step 2: Join with token let sessionSettings = CometChatCalls.sessionSettingsBuilder .setDisplayName("John Doe") - .setType(.video) + .setSessionType(.video) .build() CometChatCalls.joinSession( diff --git a/calls/ios/migration-guide-v5.mdx b/calls/ios/migration-guide-v5.mdx index 6f90b986e..36e29a24a 100644 --- a/calls/ios/migration-guide-v5.mdx +++ b/calls/ios/migration-guide-v5.mdx @@ -32,7 +32,7 @@ While v4 APIs will continue to work, migrating to v5 APIs gives you: - **Granular event listeners** — 5 focused listener protocols instead of one monolithic `CallsEventsDelegate` - **`CallSession` singleton** for cleaner session control — all actions on a single object instead of scattered static methods - **Dedicated `login()` method** — the Calls SDK now handles its own authentication instead of depending on the Chat SDK's auth token or REST APIs -- **Strongly-typed enums** — `AudioModeType`, `CallType`, `LayoutType`, `CameraFacing` instead of raw strings +- **Strongly-typed enums** — `AudioMode`, `CallType`, `LayoutType`, `CameraFacing` instead of raw strings --- @@ -41,9 +41,10 @@ While v4 APIs will continue to work, migrating to v5 APIs gives you: No changes required. The `init` API is the same in v5. ```swift -let callAppSettings = CallAppSettings() -callAppSettings.set(appId: "APP_ID") -callAppSettings.set(region: "REGION") +let callAppSettings = CallAppSettingsBuilder() + .set(appID: "APP_ID") + .set(region: "REGION") + .build() CometChatCalls(callsAppSettings: callAppSettings, onSuccess: { success in // Initialized @@ -118,7 +119,7 @@ let callSettings = CometChatCalls.callSettingsBuilder ```swift let sessionSettings = CometChatCalls.sessionSettingsBuilder - .setType(.audio) + .setSessionType(.audio) .startAudioMuted(false) .startVideoPaused(false) .setLayout(.tile) @@ -334,7 +335,7 @@ class MyMediaListener: MediaEventsListener { func onVideoResumed() { } func onRecordingStarted() { } func onRecordingStopped() { } - func onAudioModeChanged(audioModeType: AudioModeType) { } + func onAudioModeChanged(audioMode: AudioMode) { } func onCameraFacingChanged(cameraFacing: CameraFacing) { } } callSession.addMediaEventsListener(myMediaListener) @@ -373,7 +374,7 @@ v5 listeners use weak references internally, so they are automatically cleaned u | `onUserJoined(rtcUser:)` | `ParticipantEventListener` | `onParticipantJoined(participant:)` | | `onUserLeft(rtcUser:)` | `ParticipantEventListener` | `onParticipantLeft(participant:)` | | `onUserListChanged(rtcUsers:)` | `ParticipantEventListener` | `onParticipantListChanged(participants:)` | -| `onAudioModeChanged(mode:)` | `MediaEventsListener` | `onAudioModeChanged(audioModeType:)` | +| `onAudioModeChanged(mode:)` | `MediaEventsListener` | `onAudioModeChanged(audioMode:)` | | `onCallSwitchedToVideo(callSwitchedInfo:)` | *Removed* | — | | `onUserMuted(rtcMutedUser:)` | `ParticipantEventListener` | `onParticipantAudioMuted(participant:)` | | `onRecordingToggled(recordingInfo:)` | `MediaEventsListener` | `onRecordingStarted()` / `onRecordingStopped()` | diff --git a/calls/ios/recording.mdx b/calls/ios/recording.mdx index acc306a94..e655b34a0 100644 --- a/calls/ios/recording.mdx +++ b/calls/ios/recording.mdx @@ -131,7 +131,7 @@ class CallViewController: UIViewController, MediaEventsListener { func onVideoResumed() {} func onScreenShareStarted() {} func onScreenShareStopped() {} - func onAudioModeChanged(audioModeType: AudioModeType) {} + func onAudioModeChanged(audioMode: AudioMode) {} func onCameraFacingChanged(cameraFacing: CameraFacing) {} } ``` diff --git a/calls/ios/ringing.mdx b/calls/ios/ringing.mdx index efe23875f..3df58dcf3 100644 --- a/calls/ios/ringing.mdx +++ b/calls/ios/ringing.mdx @@ -115,7 +115,7 @@ extension CallViewController: CometChatCallDelegate { func onIncomingCallReceived(incomingCall: Call?, error: CometChatException?) { guard let call = incomingCall else { return } - print("Incoming call from: \(call.callInitiator?.name ?? "")") + print("Incoming call from: \((call.callInitiator as? User)?.name ?? "")") // Show incoming call UI with accept/reject options } @@ -150,7 +150,7 @@ NSString *listenerID = @"UNIQUE_LISTENER_ID"; // Implement CometChatCallDelegate - (void)onIncomingCallReceivedWithIncomingCall:(Call *)incomingCall error:(CometChatException *)error { - NSLog(@"Incoming call from: %@", incomingCall.callInitiator.name); + NSLog(@"Incoming call from: %@", ((User *)incomingCall.callInitiator).name); // Show incoming call UI with accept/reject options } @@ -235,7 +235,7 @@ Reject an incoming call: ```swift func rejectIncomingCall(sessionId: String) { - let status: CometChat.CallStatus = .rejected + let status: CometChat.callStatus = .rejected CometChat.rejectCall(sessionID: sessionId, status: status, onSuccess: { call in print("Call rejected") @@ -270,7 +270,7 @@ Cancel an outgoing call before it's answered: ```swift func cancelOutgoingCall(sessionId: String) { - let status: CometChat.CallStatus = .cancelled + let status: CometChat.callStatus = .cancelled CometChat.rejectCall(sessionID: sessionId, status: status, onSuccess: { call in print("Call cancelled") @@ -306,7 +306,7 @@ After accepting a call (or when your outgoing call is accepted), join the call s ```swift func joinCallSession(sessionId: String) { let sessionSettings = CometChatCalls.sessionSettingsBuilder - .setType(.video) + .setSessionType(.video) .build() CometChatCalls.joinSession( diff --git a/calls/ios/session-settings.mdx b/calls/ios/session-settings.mdx index 09ab72cd0..1e9b94b2f 100644 --- a/calls/ios/session-settings.mdx +++ b/calls/ios/session-settings.mdx @@ -19,7 +19,7 @@ These are pre-session configurations that must be set before joining a call. Onc let sessionSettings = CometChatCalls.sessionSettingsBuilder .setTitle("Team Meeting") .setDisplayName("John Doe") - .setType(.video) + .setSessionType(.video) .setLayout(.tile) .startAudioMuted(false) .startVideoPaused(false) @@ -97,7 +97,7 @@ Defines the type of call session. Choose `.video` for video calls with camera en ```swift -.setType(.video) +.setSessionType(.video) ``` @@ -220,7 +220,7 @@ Controls whether the camera is turned off when joining the session. Set to `true ### Audio Mode -**Method:** `setAudioMode(_ audioMode: AudioModeType)` +**Method:** `setAudioMode(_ audioMode: AudioMode)` Sets the initial audio output device for the call. Options include `.speaker` for loudspeaker, `.earpiece` for phone earpiece, `.bluetooth` for connected Bluetooth devices, or `.headphones` for wired headphones. @@ -232,16 +232,16 @@ Sets the initial audio output device for the call. Options include `.speaker` fo ```objectivec -[builder setAudioMode:AudioModeTypeSpeaker] +[builder setAudioMode:AudioModeSpeaker] ``` | Parameter | Type | Default | |-----------|------|---------| -| `audioMode` | AudioModeType | .speaker | +| `audioMode` | AudioMode | .speaker | - + | Value | Description | |-------|-------------| | `.speaker` | Device loudspeaker | @@ -733,7 +733,7 @@ Hides the button that opens the in-call chat interface. Set to `false` to show t | `LayoutType` | `.tile` | Grid layout showing all participants equally | | | `.spotlight` | Focus on active speaker with others in sidebar | | | `.sidebar` | Main speaker with participants in a sidebar | -| `AudioModeType` | `.speaker` | Device loudspeaker | +| `AudioMode` | `.speaker` | Device loudspeaker | | | `.earpiece` | Phone earpiece | | | `.bluetooth` | Connected Bluetooth device | | | `.headphones` | Wired headphones | diff --git a/calls/ios/setup.mdx b/calls/ios/setup.mdx index 5d7b08d8f..40e0cdd87 100644 --- a/calls/ios/setup.mdx +++ b/calls/ios/setup.mdx @@ -44,7 +44,7 @@ pod install ### Using Swift Package Manager 1. In Xcode, go to **File > Add Package Dependencies** -2. Enter the repository URL: `https://github.com/cometchat/cometchat-calls-sdk-ios` +2. Enter the repository URL: `https://github.com/cometchat/calls-sdk-ios` 3. Select the version and add to your target ## Add Permissions @@ -84,7 +84,7 @@ The `CallAppSettings` class configures the SDK initialization: | Parameter | Type | Required | Description | |-----------|------|----------|-------------| | `appId` | String | Yes | Your CometChat App ID | -| `region` | String | Yes | Your app region (`us` or `eu`) | +| `region` | String | Yes | Your app region — `us`, `eu` or `in` | @@ -92,11 +92,11 @@ The `CallAppSettings` class configures the SDK initialization: import CometChatCallsSDK let appId = "APP_ID" // Replace with your App ID -let region = "REGION" // Replace with your Region ("us" or "eu") +let region = "REGION" // Replace with your Region ("us", "eu" or "in") let callAppSettings = CallAppSettingsBuilder() - .setAppId(appId) - .setRegion(region) + .set(appID: appId) + .set(region: region) .build() CometChatCalls(callsAppSettings: callAppSettings, onSuccess: { message in @@ -111,7 +111,7 @@ CometChatCalls(callsAppSettings: callAppSettings, onSuccess: { message in @import CometChatCallsSDK; NSString *appId = @"APP_ID"; // Replace with your App ID -NSString *region = @"REGION"; // Replace with your Region ("us" or "eu") +NSString *region = @"REGION"; // Replace with your Region ("us", "eu" or "in") CallAppSettings *callAppSettings = [[[CallAppSettingsBuilder alloc] init] setAppId:appId] From b3949530912e1f98399b270e18e78ba4c7e6326d Mon Sep 17 00:00:00 2001 From: Ashfaaq Ali Date: Thu, 3 Sep 2026 13:05:38 +0530 Subject: [PATCH 2/5] fix(calls/ios): 3 more defect classes found on the deployed preview MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit A second pass against the shipped CometChatCallsSDK 5.0.4 and CometChatSDK 4.1.7 interfaces, plus the Chat SDK's generated CometChatSDK-Swift.h, turned up three classes the first pass missed. The first pass grepped for the Swift form `.setType(` with a leading dot, which never matched the Objective-C `setType:` spelling, so every ObjC tab went unchecked. D9 — phantom `CallType` on SessionSettingsBuilder (12 sites, 5 files) `CallType` does not exist in CometChatCallsSDK at all (0 occurrences in the .swiftinterface). It exists only in the Chat SDK, as the nested `CometChat.CallType` on the `Call` object. The builder's real API is `setSessionType(_ sessionType: SessionType)`. Fixed the method name, the parameter table, the accordion title and the enum-values table. `.audio` is additionally deprecated on SessionType ("Use voice instead"), so audio-only is now documented as `.voice`. D10 — Objective-C snippets that cannot compile (2 files) setup.mdx used the deprecated `setAppId:`/`setRegion:` selectors; the current ones are `setWithAppID:`/`setWithRegion:` (verified in the framework binary). Both that snippet and the session-settings builder chain also had unbalanced brackets — 3 opens for 5 messages and 7 for 8 — so neither would build. All ObjC fences across the 25 calls/ios pages now balance. D11 — invented `CometChat`-prefixed ObjC enum constants (2 files) `CometChatCallType`, `CometChatCallTypeVideo`, `CometChatReceiverType`, `CometChatReceiverTypeUser`, `CometChatCallStatusRejected` and `CometChatCallStatusCancelled` are not emitted by the Chat SDK. The header declares `CallType`/`CallTypeVideo`, `ReceiverType`/`ReceiverTypeUser` and `callStatus`/`callStatusRejected`/`callStatusCancelled` (lowercase prefix, because the Swift enum is `CometChat.callStatus`). ringing.mdx lines 45/61/99 keep `CometChat.CallType` with `.video`/`.audio` deliberately — that is the Chat SDK type on the ringing path and is correct. call-logs.mdx keeps `setWithCallType:SessionTypeVideo`, which matches `CallLogsBuilder.set(callType: SessionType)`. Co-Authored-By: Claude Opus 5 --- calls/ios/idle-timeout.mdx | 2 +- calls/ios/join-session.mdx | 6 +++--- calls/ios/migration-guide-v5.mdx | 4 ++-- calls/ios/ringing.mdx | 10 +++++----- calls/ios/session-settings.mdx | 20 ++++++++++---------- calls/ios/setup.mdx | 6 +++--- calls/ios/voip-calling.mdx | 2 +- 7 files changed, 25 insertions(+), 25 deletions(-) diff --git a/calls/ios/idle-timeout.mdx b/calls/ios/idle-timeout.mdx index 887a15f6d..1199ce13d 100644 --- a/calls/ios/idle-timeout.mdx +++ b/calls/ios/idle-timeout.mdx @@ -56,7 +56,7 @@ CometChatCalls.joinSession( ```objectivec SessionSettings *sessionSettings = [[[[CometChatCalls sessionSettingsBuilder] setIdleTimeoutPeriod:120] - setType:CallTypeVideo] + setSessionType:SessionTypeVideo] build]; [CometChatCalls joinSessionWithSessionID:sessionId diff --git a/calls/ios/join-session.mdx b/calls/ios/join-session.mdx index f7bda4efb..9ad710f6e 100644 --- a/calls/ios/join-session.mdx +++ b/calls/ios/join-session.mdx @@ -84,7 +84,7 @@ NSString *sessionId = @"SESSION_ID"; SessionSettings *sessionSettings = [[[[CometChatCalls sessionSettingsBuilder] setDisplayName:@"John Doe"] - setType:CallTypeVideo] + setSessionType:SessionTypeVideo] build]; [CometChatCalls joinSessionWithSessionID:sessionId @@ -183,7 +183,7 @@ CometChatCalls.joinSession( ```objectivec SessionSettings *sessionSettings = [[[[CometChatCalls sessionSettingsBuilder] setDisplayName:@"John Doe"] - setType:CallTypeVideo] + setSessionType:SessionTypeVideo] build]; // Use the previously generated token @@ -250,7 +250,7 @@ NSString *sessionId = @"SESSION_ID"; // Step 2: Join with token SessionSettings *sessionSettings = [[[[CometChatCalls sessionSettingsBuilder] setDisplayName:@"John Doe"] - setType:CallTypeVideo] + setSessionType:SessionTypeVideo] build]; [CometChatCalls joinSessionWithCallToken:token diff --git a/calls/ios/migration-guide-v5.mdx b/calls/ios/migration-guide-v5.mdx index 36e29a24a..1c16205df 100644 --- a/calls/ios/migration-guide-v5.mdx +++ b/calls/ios/migration-guide-v5.mdx @@ -32,7 +32,7 @@ While v4 APIs will continue to work, migrating to v5 APIs gives you: - **Granular event listeners** — 5 focused listener protocols instead of one monolithic `CallsEventsDelegate` - **`CallSession` singleton** for cleaner session control — all actions on a single object instead of scattered static methods - **Dedicated `login()` method** — the Calls SDK now handles its own authentication instead of depending on the Chat SDK's auth token or REST APIs -- **Strongly-typed enums** — `AudioMode`, `CallType`, `LayoutType`, `CameraFacing` instead of raw strings +- **Strongly-typed enums** — `AudioMode`, `SessionType`, `LayoutType`, `CameraFacing` instead of raw strings --- @@ -142,7 +142,7 @@ let sessionSettings = CometChatCalls.sessionSettingsBuilder | v4 Method | v5 Method | Notes | |-----------|-----------|-------| -| `setIsAudioOnly(true)` | `setType(.audio)` | Use `.video` for video calls | +| `setIsAudioOnly(true)` | `setSessionType(.voice)` | Use `.video` for video calls | | `setDefaultLayout(bool)` | `hideControlPanel(!bool)` + `hideHeaderPanel(!bool)` | Inverted logic | | `setEndCallButtonDisable(bool)` | `hideLeaveSessionButton(bool)` | Same logic | | `setMuteAudioButtonDisable(bool)` | `hideToggleAudioButton(bool)` | Same logic | diff --git a/calls/ios/ringing.mdx b/calls/ios/ringing.mdx index 3df58dcf3..a5fef8761 100644 --- a/calls/ios/ringing.mdx +++ b/calls/ios/ringing.mdx @@ -74,8 +74,8 @@ CometChat.initiateCall(call: call, timeout: 30, onSuccess: { call in ```objectivec NSString *receiverID = @"USER_ID"; -CometChatReceiverType receiverType = CometChatReceiverTypeUser; -CometChatCallType callType = CometChatCallTypeVideo; +ReceiverType receiverType = ReceiverTypeUser; +CallType callType = CallTypeVideo; Call *call = [[Call alloc] initWithReceiverId:receiverID callType:callType @@ -250,7 +250,7 @@ func rejectIncomingCall(sessionId: String) { ```objectivec - (void)rejectIncomingCallWithSessionId:(NSString *)sessionId { [CometChat rejectCallWithSessionID:sessionId - status:CometChatCallStatusRejected + status:callStatusRejected onSuccess:^(Call * call) { NSLog(@"Call rejected"); // Dismiss incoming call UI @@ -285,7 +285,7 @@ func cancelOutgoingCall(sessionId: String) { ```objectivec - (void)cancelOutgoingCallWithSessionId:(NSString *)sessionId { [CometChat rejectCallWithSessionID:sessionId - status:CometChatCallStatusCancelled + status:callStatusCancelled onSuccess:^(Call * call) { NSLog(@"Call cancelled"); // Dismiss outgoing call UI @@ -327,7 +327,7 @@ func joinCallSession(sessionId: String) { ```objectivec - (void)joinCallSessionWithSessionId:(NSString *)sessionId { SessionSettings *sessionSettings = [[[CometChatCalls sessionSettingsBuilder] - setType:CallTypeVideo] + setSessionType:SessionTypeVideo] build]; [CometChatCalls joinSessionWithSessionID:sessionId diff --git a/calls/ios/session-settings.mdx b/calls/ios/session-settings.mdx index 1e9b94b2f..935cf1346 100644 --- a/calls/ios/session-settings.mdx +++ b/calls/ios/session-settings.mdx @@ -28,10 +28,10 @@ let sessionSettings = CometChatCalls.sessionSettingsBuilder ```objectivec -SessionSettings *sessionSettings = [[[[[[[CometChatCalls sessionSettingsBuilder] +SessionSettings *sessionSettings = [[[[[[[[CometChatCalls sessionSettingsBuilder] setTitle:@"Team Meeting"] setDisplayName:@"John Doe"] - setType:CallTypeVideo] + setSessionType:SessionTypeVideo] setLayout:LayoutTypeTile] startAudioMuted:NO] startVideoPaused:NO] @@ -90,9 +90,9 @@ Sets the display name that will be shown to other participants in the call. This ### Session Type -**Method:** `setType(_ type: CallType)` +**Method:** `setSessionType(_ sessionType: SessionType)` -Defines the type of call session. Choose `.video` for video calls with camera enabled, or `.audio` for audio-only calls. This setting determines whether video streaming is enabled by default. +Defines the type of call session. Choose `.video` for video calls with camera enabled, or `.voice` for audio-only calls. This setting determines whether video streaming is enabled by default. @@ -102,20 +102,20 @@ Defines the type of call session. Choose `.video` for video calls with camera en ```objectivec -[builder setType:CallTypeVideo] +[builder setSessionType:SessionTypeVideo] ``` | Parameter | Type | Default | |-----------|------|---------| -| `type` | CallType | .video | +| `sessionType` | SessionType | .video | - + | Value | Description | |-------|-------------| | `.video` | Video call with camera enabled | -| `.audio` | Audio-only call | +| `.voice` | Audio-only call | ### Layout Mode @@ -728,8 +728,8 @@ Hides the button that opens the in-call chat interface. Set to `false` to show t | Enum | Value | Description | |------|-------|-------------| -| `CallType` | `.video` | Video call with camera enabled | -| | `.audio` | Audio-only call | +| `SessionType` | `.video` | Video call with camera enabled | +| | `.voice` | Audio-only call | | `LayoutType` | `.tile` | Grid layout showing all participants equally | | | `.spotlight` | Focus on active speaker with others in sidebar | | | `.sidebar` | Main speaker with participants in a sidebar | diff --git a/calls/ios/setup.mdx b/calls/ios/setup.mdx index 40e0cdd87..9bc375393 100644 --- a/calls/ios/setup.mdx +++ b/calls/ios/setup.mdx @@ -113,9 +113,9 @@ CometChatCalls(callsAppSettings: callAppSettings, onSuccess: { message in NSString *appId = @"APP_ID"; // Replace with your App ID NSString *region = @"REGION"; // Replace with your Region ("us", "eu" or "in") -CallAppSettings *callAppSettings = [[[CallAppSettingsBuilder alloc] init] - setAppId:appId] - setRegion:region] +CallAppSettings *callAppSettings = [[[[[CallAppSettingsBuilder alloc] init] + setWithAppID:appId] + setWithRegion:region] build]; [[CometChatCalls alloc] initWithCallsAppSettings:callAppSettings diff --git a/calls/ios/voip-calling.mdx b/calls/ios/voip-calling.mdx index 6cd30beff..786219fd7 100644 --- a/calls/ios/voip-calling.mdx +++ b/calls/ios/voip-calling.mdx @@ -567,7 +567,7 @@ extension CallManager: CXProviderDelegate { return; } - [CometChat rejectCallWithSessionID:_activeSessionId status:CometChatCallStatusRejected onSuccess:^(Call * call) { + [CometChat rejectCallWithSessionID:_activeSessionId status:callStatusRejected onSuccess:^(Call * call) { [action fulfill]; } onError:^(CometChatException * error) { [action fulfill]; From b8d744a38dd649dbe3ad50f56475128c65670b46 Mon Sep 17 00:00:00 2001 From: Ashfaaq Ali Date: Thu, 3 Sep 2026 14:20:46 +0530 Subject: [PATCH 3/5] =?UTF-8?q?fix(calls/ios):=20D12=20=E2=80=94=20remove?= =?UTF-8?q?=20six=20Participant=20fields=20that=20do=20not=20exist?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `Participant` has exactly 12 properties, all Optional: uid, name, avatar, mid, state, isJoined, joinedAt, leftAt, deviceID, totalAudioMinutes, totalVideoMinutes, totalDurationInMinutes. The docs used SIX properties that are not on the type at all — pid, role, audioMuted/isAudioMuted, videoPaused/isVideoPaused, isPinned, isPresenting and raisedHandTimestamp — across code samples and two "Participant Object Reference" tables in which 7 of 10 rows were fabricated. This is not a theoretical defect. Two independent review agents building from these pages produced code that swiftc rejected: value of type 'Participant' has no member 'pid' value of type 'Participant' has no member 'isPresenting' WHAT CHANGED - actions.mdx, participant-management.mdx: `participant.pid` -> `participant.uid` in the mute and pause-video samples (Swift + Objective-C). `uid` is `String?` while `muteParticipant(participantId:)` takes a non-Optional `String`, so the samples now unwrap it. Both property tables replaced with the 12 real fields, correctly typed as Optionals. - raise-hand.mdx "Check Raised Hand Status" and screen-sharing.mdx "Check Screen Share Status" were built entirely on `raisedHandTimestamp` / `isPresenting`. Rewritten to accumulate the state from onParticipantHandRaised/Lowered and onParticipantStartedScreenShare/Stopped, keyed by uid, with cleanup on onParticipantLeft (a participant who leaves mid-share never sends a stop). - custom-participant-list.mdx was the worst affected: its cell rendered five phantom flags, and Step 4 discarded every state event with the comment "Table will update via onParticipantListChanged" — which cannot work, since that payload carries no flags. Introduced a `ParticipantStatus` model (Swift struct / ObjC class) held by the view controller and driven by the events, and threaded it through configure(), the data source and the pin toggle. Also fixed, in passing: `muteParticipant(participant.uid)` was missing its argument label, and the search filter called `localizedCaseInsensitiveContains` on an Optional `name`. A on each rewritten page states the consequence honestly: these events fire only on change, so a client joining mid-call cannot recover state that was already in effect. There is no getter and no snapshot — that is a real SDK limitation, not a doc omission. NOT CHANGED (checked, correct as-is) - custom-control-panel.mdx's `isAudioMuted`/`isVideoPaused` are the sample's own local variables — already the right pattern. - migration-guide-v5.mdx's `CometChatCalls.audioMuted(true)` are genuine v4 statics shown as the OLD API; their v5 targets muteAudio()/pauseVideo() all exist. VERIFICATION - Every participant-typed member access across all 25 calls/ios pages now resolves to `uid` or `name`; nothing else. - All 13 Objective-C selectors used here confirmed present in the shipped CometChatCallsSDK 5.0.4 framework binary. - Every ObjC fence in calls/ios still has balanced brackets (0 unbalanced). - The uid + local-state pattern is compile-proven: both review emits that had failed on the phantom fields now pass swiftc and the simulator round-trip. Co-Authored-By: Claude Opus 5 --- calls/ios/actions.mdx | 43 +++-- calls/ios/custom-participant-list.mdx | 218 ++++++++++++++++++++------ calls/ios/participant-management.mdx | 43 +++-- calls/ios/raise-hand.mdx | 77 ++++++--- calls/ios/screen-sharing.mdx | 58 +++++-- 5 files changed, 327 insertions(+), 112 deletions(-) diff --git a/calls/ios/actions.mdx b/calls/ios/actions.mdx index 924e83690..96819670d 100644 --- a/calls/ios/actions.mdx +++ b/calls/ios/actions.mdx @@ -234,12 +234,15 @@ Mutes a specific participant's audio. This is a moderator action. ```swift -CallSession.shared.muteParticipant(participantId: participant.pid) +guard let uid = participant.uid else { return } +CallSession.shared.muteParticipant(participantId: uid) ``` ```objectivec -[[CallSession shared] muteParticipantWithParticipantId:participant.pid]; +if (participant.uid) { + [[CallSession shared] muteParticipantWithParticipantId:participant.uid]; +} ``` @@ -251,12 +254,15 @@ Pauses a specific participant's video. This is a moderator action. ```swift -CallSession.shared.pauseParticipantVideo(participantId: participant.pid) +guard let uid = participant.uid else { return } +CallSession.shared.pauseParticipantVideo(participantId: uid) ``` ```objectivec -[[CallSession shared] pauseParticipantVideoWithParticipantId:participant.pid]; +if (participant.uid) { + [[CallSession shared] pauseParticipantVideoWithParticipantId:participant.uid]; +} ``` @@ -463,14 +469,23 @@ CallSession.shared.hideSettingsPanel() | Property | Type | Description | |----------|------|-------------| -| `uid` | `String` | Unique identifier (CometChat user ID) | -| `name` | `String` | Display name | -| `avatar` | `String` | URL of avatar image | -| `pid` | `String` | Participant ID for this call session | -| `role` | `String` | Role in the call | -| `audioMuted` | `Bool` | Whether audio is muted | -| `videoPaused` | `Bool` | Whether video is paused | -| `isPinned` | `Bool` | Whether pinned in layout | -| `isPresenting` | `Bool` | Whether screen sharing | -| `raisedHandTimestamp` | `Int` | Timestamp when hand was raised | +| `uid` | `String?` | CometChat user ID — the identifier every moderator action takes | +| `name` | `String?` | Display name | +| `avatar` | `String?` | URL of avatar image | +| `mid` | `String?` | Media ID for this call session | +| `state` | `String?` | Participant state as reported by the server | +| `isJoined` | `Bool?` | Whether the participant is currently joined | +| `joinedAt` | `Int?` | Join timestamp | +| `leftAt` | `Int?` | Leave timestamp | +| `deviceID` | `String?` | Device identifier | +| `totalAudioMinutes` | `Double?` | Audio minutes consumed | +| `totalVideoMinutes` | `Double?` | Video minutes consumed | +| `totalDurationInMinutes` | `Double?` | Total session minutes | + + + **Every property is Optional.** `Participant` carries no mute / video / pin / hand-raise / + screen-share flags, and the SDK exposes no getter for them — track that state in your own app + from the [participant events](/calls/ios/events). Because the events fire only on change, a + client that joins late cannot recover state that was already in effect. + diff --git a/calls/ios/custom-participant-list.mdx b/calls/ios/custom-participant-list.mdx index 2b290ed8f..c9ac94934 100644 --- a/calls/ios/custom-participant-list.mdx +++ b/calls/ios/custom-participant-list.mdx @@ -59,6 +59,10 @@ class ParticipantListViewController: UIViewController { private let searchBar = UISearchBar() private var participants: [Participant] = [] private var filteredParticipants: [Participant] = [] + /// Per-participant state the app maintains itself, keyed by `Participant.uid`, because + /// `Participant` carries no such flags. Starts empty — anything that happened before this + /// client joined is not recoverable. + fileprivate var statuses: [String: ParticipantStatus] = [:] override func viewDidLoad() { super.viewDidLoad() @@ -116,12 +120,16 @@ class ParticipantListViewController: UIViewController { @property (nonatomic, strong) UISearchBar *searchBar; @property (nonatomic, strong) NSArray *participants; @property (nonatomic, strong) NSArray *filteredParticipants; +/// Per-participant state the app maintains itself, keyed by uid — `Participant` carries no +/// such flags. Starts empty; state from before this client joined is not recoverable. +@property (nonatomic, strong) NSMutableDictionary *statuses; @end @implementation ParticipantListViewController - (void)viewDidLoad { [super viewDidLoad]; + self.statuses = [NSMutableDictionary dictionary]; [self setupUI]; [self setupParticipantListener]; } @@ -177,11 +185,29 @@ class ParticipantListViewController: UIViewController { ## Step 3: Create Participant Cell -Build a custom table view cell to display participant information: +Build a custom table view cell to display participant information. + + + `Participant` carries **only** identity and timing fields — `uid`, `name`, `avatar`, `mid`, + `state`, `isJoined`, `joinedAt`, `leftAt`, `deviceID` and the `total*Minutes` counters. It has + **no** mute / video / screen-share / hand-raise / pin flags, and the SDK exposes no getter for + them. Accumulate that state in your own app from the [participant events](/calls/ios/events), + as below. Because those events fire only on change, a client that joins mid-call cannot recover + state that was already in effect. + ```swift +/// Per-participant state the app maintains itself, keyed by `Participant.uid`. +struct ParticipantStatus { + var isAudioMuted = false + var isVideoPaused = false + var isPresenting = false + var isHandRaised = false + var isPinned = false +} + class ParticipantCell: UITableViewCell { private let avatarImageView = UIImageView() @@ -257,23 +283,23 @@ class ParticipantCell: UITableViewCell { ]) } - func configure(with participant: Participant) { + func configure(with participant: Participant, status: ParticipantStatus) { self.participant = participant nameLabel.text = participant.name - - // Build status text + + // Build status text from the app's own state, not from `participant`. var statusParts: [String] = [] - if participant.isAudioMuted { statusParts.append("🔇 Muted") } - if participant.isVideoPaused { statusParts.append("📹 Video Off") } - if participant.isPresenting { statusParts.append("🖥️ Presenting") } - if participant.raisedHandTimestamp > 0 { statusParts.append("✋ Hand Raised") } - if participant.isPinned { statusParts.append("📌 Pinned") } - + if status.isAudioMuted { statusParts.append("🔇 Muted") } + if status.isVideoPaused { statusParts.append("📹 Video Off") } + if status.isPresenting { statusParts.append("🖥️ Presenting") } + if status.isHandRaised { statusParts.append("✋ Hand Raised") } + if status.isPinned { statusParts.append("📌 Pinned") } + statusLabel.text = statusParts.isEmpty ? "Active" : statusParts.joined(separator: " • ") - + // Update button states - muteButton.alpha = participant.isAudioMuted ? 0.5 : 1.0 - pinButton.tintColor = participant.isPinned ? .systemBlue : .systemGray + muteButton.alpha = status.isAudioMuted ? 0.5 : 1.0 + pinButton.tintColor = status.isPinned ? .systemBlue : .systemGray } @objc private func muteButtonTapped() { @@ -290,11 +316,23 @@ class ParticipantCell: UITableViewCell { ```objectivec +/// Per-participant state the app maintains itself, keyed by `Participant.uid`. +@interface ParticipantStatus : NSObject +@property (nonatomic, assign) BOOL isAudioMuted; +@property (nonatomic, assign) BOOL isVideoPaused; +@property (nonatomic, assign) BOOL isPresenting; +@property (nonatomic, assign) BOOL isHandRaised; +@property (nonatomic, assign) BOOL isPinned; +@end + +@implementation ParticipantStatus +@end + @interface ParticipantCell : UITableViewCell @property (nonatomic, strong) Participant *participant; @property (nonatomic, copy) void (^onMuteAction)(Participant *); @property (nonatomic, copy) void (^onPinAction)(Participant *); -- (void)configureWithParticipant:(Participant *)participant; +- (void)configureWithParticipant:(Participant *)participant status:(ParticipantStatus *)status; @end @implementation ParticipantCell { @@ -372,23 +410,23 @@ class ParticipantCell: UITableViewCell { ]]; } -- (void)configureWithParticipant:(Participant *)participant { +- (void)configureWithParticipant:(Participant *)participant status:(ParticipantStatus *)status { self.participant = participant; _nameLabel.text = participant.name; - - // Build status text + + // Build status text from the app's own state, not from `participant`. NSMutableArray *statusParts = [NSMutableArray array]; - if (participant.isAudioMuted) [statusParts addObject:@"🔇 Muted"]; - if (participant.isVideoPaused) [statusParts addObject:@"📹 Video Off"]; - if (participant.isPresenting) [statusParts addObject:@"🖥️ Presenting"]; - if (participant.raisedHandTimestamp > 0) [statusParts addObject:@"✋ Hand Raised"]; - if (participant.isPinned) [statusParts addObject:@"📌 Pinned"]; - + if (status.isAudioMuted) [statusParts addObject:@"🔇 Muted"]; + if (status.isVideoPaused) [statusParts addObject:@"📹 Video Off"]; + if (status.isPresenting) [statusParts addObject:@"🖥️ Presenting"]; + if (status.isHandRaised) [statusParts addObject:@"✋ Hand Raised"]; + if (status.isPinned) [statusParts addObject:@"📌 Pinned"]; + _statusLabel.text = statusParts.count == 0 ? @"Active" : [statusParts componentsJoinedByString:@" • "]; - + // Update button states - _muteButton.alpha = participant.isAudioMuted ? 0.5 : 1.0; - _pinButton.tintColor = participant.isPinned ? [UIColor systemBlueColor] : [UIColor systemGrayColor]; + _muteButton.alpha = status.isAudioMuted ? 0.5 : 1.0; + _pinButton.tintColor = status.isPinned ? [UIColor systemBlueColor] : [UIColor systemGrayColor]; } - (void)muteButtonTapped { @@ -437,22 +475,37 @@ extension ParticipantListViewController: ParticipantEventListener { } func onParticipantJoined(participant: Participant) { - print("\(participant.name) joined") + print("\(participant.name ?? "") joined") } - + func onParticipantLeft(participant: Participant) { - print("\(participant.name) left") + guard let uid = participant.uid else { return } + DispatchQueue.main.async { + self.statuses.removeValue(forKey: uid) // don't leak state for someone who left + self.tableView.reloadData() + } } - - func onParticipantAudioMuted(participant: Participant) { - // Table will update via onParticipantListChanged + + // `onParticipantListChanged` carries no flags, so every status below comes from these + // transition events. Each one mutates the app's own `statuses` map and redraws. + func onParticipantAudioMuted(participant: Participant) { update(participant) { $0.isAudioMuted = true } } + func onParticipantAudioUnmuted(participant: Participant) { update(participant) { $0.isAudioMuted = false } } + func onParticipantVideoPaused(participant: Participant) { update(participant) { $0.isVideoPaused = true } } + func onParticipantVideoResumed(participant: Participant) { update(participant) { $0.isVideoPaused = false } } + func onParticipantHandRaised(participant: Participant) { update(participant) { $0.isHandRaised = true } } + func onParticipantHandLowered(participant: Participant) { update(participant) { $0.isHandRaised = false } } + func onParticipantStartedScreenShare(participant: Participant) { update(participant) { $0.isPresenting = true } } + func onParticipantStoppedScreenShare(participant: Participant) { update(participant) { $0.isPresenting = false } } + + private func update(_ participant: Participant, _ change: @escaping (inout ParticipantStatus) -> Void) { + guard let uid = participant.uid else { return } + DispatchQueue.main.async { + var status = self.statuses[uid] ?? ParticipantStatus() + change(&status) + self.statuses[uid] = status + self.tableView.reloadData() + } } - - func onParticipantAudioUnmuted(participant: Participant) {} - func onParticipantVideoPaused(participant: Participant) {} - func onParticipantVideoResumed(participant: Participant) {} - func onParticipantHandRaised(participant: Participant) {} - func onParticipantHandLowered(participant: Participant) {} } ``` @@ -483,7 +536,49 @@ extension ParticipantListViewController: ParticipantEventListener { } - (void)onParticipantLeftWithParticipant:(Participant *)participant { - NSLog(@"%@ left", participant.name); + if (!participant.uid) { return; } + dispatch_async(dispatch_get_main_queue(), ^{ + [self.statuses removeObjectForKey:participant.uid]; // don't leak state + [self.tableView reloadData]; + }); +} + +// `onParticipantListChanged` carries no flags, so every status below comes from these +// transition events. self.statuses is an NSMutableDictionary. +- (void)updateParticipant:(Participant *)participant change:(void (^)(ParticipantStatus *))change { + NSString *uid = participant.uid; + if (!uid) { return; } + dispatch_async(dispatch_get_main_queue(), ^{ + ParticipantStatus *status = self.statuses[uid] ?: [ParticipantStatus new]; + change(status); + self.statuses[uid] = status; + [self.tableView reloadData]; + }); +} + +- (void)onParticipantAudioMutedWithParticipant:(Participant *)p { + [self updateParticipant:p change:^(ParticipantStatus *s) { s.isAudioMuted = YES; }]; +} +- (void)onParticipantAudioUnmutedWithParticipant:(Participant *)p { + [self updateParticipant:p change:^(ParticipantStatus *s) { s.isAudioMuted = NO; }]; +} +- (void)onParticipantVideoPausedWithParticipant:(Participant *)p { + [self updateParticipant:p change:^(ParticipantStatus *s) { s.isVideoPaused = YES; }]; +} +- (void)onParticipantVideoResumedWithParticipant:(Participant *)p { + [self updateParticipant:p change:^(ParticipantStatus *s) { s.isVideoPaused = NO; }]; +} +- (void)onParticipantHandRaisedWithParticipant:(Participant *)p { + [self updateParticipant:p change:^(ParticipantStatus *s) { s.isHandRaised = YES; }]; +} +- (void)onParticipantHandLoweredWithParticipant:(Participant *)p { + [self updateParticipant:p change:^(ParticipantStatus *s) { s.isHandRaised = NO; }]; +} +- (void)onParticipantStartedScreenShareWithParticipant:(Participant *)p { + [self updateParticipant:p change:^(ParticipantStatus *s) { s.isPresenting = YES; }]; +} +- (void)onParticipantStoppedScreenShareWithParticipant:(Participant *)p { + [self updateParticipant:p change:^(ParticipantStatus *s) { s.isPresenting = NO; }]; } ``` @@ -506,18 +601,26 @@ extension ParticipantListViewController: UITableViewDelegate, UITableViewDataSou let cell = tableView.dequeueReusableCell(withIdentifier: "ParticipantCell", for: indexPath) as! ParticipantCell let participant = filteredParticipants[indexPath.row] - cell.configure(with: participant) - - cell.onMuteAction = { [weak self] participant in - CallSession.shared.muteParticipant(participant.uid) + let status = participant.uid.flatMap { statuses[$0] } ?? ParticipantStatus() + cell.configure(with: participant, status: status) + + cell.onMuteAction = { participant in + guard let uid = participant.uid else { return } + CallSession.shared.muteParticipant(participantId: uid) } - + cell.onPinAction = { [weak self] participant in - if participant.isPinned { + guard let self, let uid = participant.uid else { return } + if self.statuses[uid]?.isPinned == true { CallSession.shared.unpinParticipant() + self.statuses[uid]?.isPinned = false } else { - CallSession.shared.pinParticipant(participantId: participant.uid, type: "pin") + // Only one participant can be pinned at a time — `unpinParticipant()` takes no id. + for key in Array(self.statuses.keys) { self.statuses[key]?.isPinned = false } + CallSession.shared.pinParticipant(participantId: uid, type: "pin") + self.statuses[uid, default: ParticipantStatus()].isPinned = true } + self.tableView.reloadData() } return cell @@ -531,7 +634,7 @@ extension ParticipantListViewController: UISearchBarDelegate { filteredParticipants = participants } else { filteredParticipants = participants.filter { - $0.name.localizedCaseInsensitiveContains(searchText) + ($0.name ?? "").localizedCaseInsensitiveContains(searchText) } } tableView.reloadData() @@ -549,19 +652,30 @@ extension ParticipantListViewController: UISearchBarDelegate { ParticipantCell *cell = [tableView dequeueReusableCellWithIdentifier:@"ParticipantCell" forIndexPath:indexPath]; Participant *participant = self.filteredParticipants[indexPath.row]; - [cell configureWithParticipant:participant]; - + ParticipantStatus *status = participant.uid ? self.statuses[participant.uid] : nil; + [cell configureWithParticipant:participant status:(status ?: [ParticipantStatus new])]; + __weak typeof(self) weakSelf = self; cell.onMuteAction = ^(Participant *p) { - [[CallSession shared] muteParticipant:p.uid]; + if (!p.uid) { return; } + [[CallSession shared] muteParticipantWithParticipantId:p.uid]; }; - + cell.onPinAction = ^(Participant *p) { - if (p.isPinned) { + __strong typeof(weakSelf) self = weakSelf; + if (!self || !p.uid) { return; } + if (self.statuses[p.uid].isPinned) { [[CallSession shared] unpinParticipant]; + self.statuses[p.uid].isPinned = NO; } else { + // Only one participant can be pinned at a time — unpinParticipant takes no id. + for (NSString *key in self.statuses) { self.statuses[key].isPinned = NO; } [[CallSession shared] pinParticipantWithParticipantId:p.uid type:@"pin"]; + ParticipantStatus *s = self.statuses[p.uid] ?: [ParticipantStatus new]; + s.isPinned = YES; + self.statuses[p.uid] = s; } + [self.tableView reloadData]; }; return cell; diff --git a/calls/ios/participant-management.mdx b/calls/ios/participant-management.mdx index 8d592485e..ca3d44b52 100644 --- a/calls/ios/participant-management.mdx +++ b/calls/ios/participant-management.mdx @@ -18,12 +18,15 @@ Mute a specific participant's audio. This affects the participant for all users ```swift -CallSession.shared.muteParticipant(participantId: participant.pid) +guard let uid = participant.uid else { return } +CallSession.shared.muteParticipant(participantId: uid) ``` ```objectivec -[[CallSession shared] muteParticipantWithParticipantId:participant.pid]; +if (participant.uid) { + [[CallSession shared] muteParticipantWithParticipantId:participant.uid]; +} ``` @@ -35,12 +38,15 @@ Pause a specific participant's video. This affects the participant for all users ```swift -CallSession.shared.pauseParticipantVideo(participantId: participant.pid) +guard let uid = participant.uid else { return } +CallSession.shared.pauseParticipantVideo(participantId: uid) ``` ```objectivec -[[CallSession shared] pauseParticipantVideoWithParticipantId:participant.pid]; +if (participant.uid) { + [[CallSession shared] pauseParticipantVideoWithParticipantId:participant.uid]; +} ``` @@ -198,16 +204,25 @@ The `Participant` object contains information about each call participant: | Property | Type | Description | |----------|------|-------------| -| `uid` | String | Unique identifier (CometChat user ID) | -| `name` | String | Display name | -| `avatar` | String | URL of avatar image | -| `pid` | String | Participant ID for this call session | -| `role` | String | Role in the call | -| `audioMuted` | Bool | Whether audio is muted | -| `videoPaused` | Bool | Whether video is paused | -| `isPinned` | Bool | Whether pinned in layout | -| `isPresenting` | Bool | Whether screen sharing | -| `raisedHandTimestamp` | Int | Timestamp when hand was raised (0 if not raised) | +| `uid` | `String?` | CometChat user ID — the identifier every moderator action takes | +| `name` | `String?` | Display name | +| `avatar` | `String?` | URL of avatar image | +| `mid` | `String?` | Media ID for this call session | +| `state` | `String?` | Participant state as reported by the server | +| `isJoined` | `Bool?` | Whether the participant is currently joined | +| `joinedAt` | `Int?` | Join timestamp | +| `leftAt` | `Int?` | Leave timestamp | +| `deviceID` | `String?` | Device identifier | +| `totalAudioMinutes` | `Double?` | Audio minutes consumed | +| `totalVideoMinutes` | `Double?` | Video minutes consumed | +| `totalDurationInMinutes` | `Double?` | Total session minutes | + + + **Every property is Optional.** `Participant` carries no mute / video / pin / hand-raise / + screen-share flags, and the SDK exposes no getter for them — track that state in your own app + from the [participant events](/calls/ios/events). Because the events fire only on change, a + client that joins late cannot recover state that was already in effect. + ## Hide Participant List Button diff --git a/calls/ios/raise-hand.mdx b/calls/ios/raise-hand.mdx index 8142c6aa0..14a564bcd 100644 --- a/calls/ios/raise-hand.mdx +++ b/calls/ios/raise-hand.mdx @@ -122,37 +122,74 @@ class CallViewController: UIViewController, ParticipantEventListener { -## Check Raised Hand Status +## Track Raised Hands -The `Participant` object includes a `raisedHandTimestamp` property to check if a participant has their hand raised: +`Participant` carries **no** raised-hand property, and the SDK exposes no getter for one. Keep an +ordered list of your own, driven by the `onParticipantHandRaised` / `onParticipantHandLowered` +callbacks. + + + These callbacks fire only when a hand goes **up or down**. A client that joins mid-call cannot + discover hands that were already raised before it joined. + ```swift -func onParticipantListChanged(participants: [Participant]) { - let raisedHands = participants - .filter { $0.raisedHandTimestamp > 0 } - .sorted { $0.raisedHandTimestamp < $1.raisedHandTimestamp } - - // Display participants with raised hands in order - updateRaisedHandsList(raisedHands) +// Raised hands, oldest first. Keyed by uid, because that is the only identifier +// `Participant` actually carries. +private var raisedHands: [(uid: String, name: String, at: Date)] = [] + +func onParticipantHandRaised(participant: Participant) { + guard let uid = participant.uid else { return } + DispatchQueue.main.async { + guard !self.raisedHands.contains(where: { $0.uid == uid }) else { return } + self.raisedHands.append((uid, participant.name ?? uid, Date())) + self.updateRaisedHandsList(self.raisedHands) + } +} + +func onParticipantHandLowered(participant: Participant) { + guard let uid = participant.uid else { return } + DispatchQueue.main.async { + self.raisedHands.removeAll { $0.uid == uid } + self.updateRaisedHandsList(self.raisedHands) + } +} + +// Someone who leaves should not stay in the queue. +func onParticipantLeft(participant: Participant) { + guard let uid = participant.uid else { return } + DispatchQueue.main.async { + self.raisedHands.removeAll { $0.uid == uid } + self.updateRaisedHandsList(self.raisedHands) + } } ``` ```objectivec -- (void)onParticipantListChangedWithParticipants:(NSArray *)participants { - NSMutableArray *raisedHands = [NSMutableArray array]; - for (Participant *p in participants) { - if (p.raisedHandTimestamp > 0) { - [raisedHands addObject:p]; +// NSMutableArray of uid strings, oldest first. +@property (nonatomic, strong) NSMutableArray *raisedHandUIDs; + +- (void)onParticipantHandRaisedWithParticipant:(Participant *)participant { + NSString *uid = participant.uid; + if (!uid) { return; } + dispatch_async(dispatch_get_main_queue(), ^{ + if (![self.raisedHandUIDs containsObject:uid]) { + [self.raisedHandUIDs addObject:uid]; + [self updateRaisedHandsList:self.raisedHandUIDs]; } - } - // Sort by timestamp and display - [raisedHands sortUsingComparator:^NSComparisonResult(Participant *a, Participant *b) { - return [@(a.raisedHandTimestamp) compare:@(b.raisedHandTimestamp)]; - }]; - [self updateRaisedHandsList:raisedHands]; + }); +} + +- (void)onParticipantHandLoweredWithParticipant:(Participant *)participant { + NSString *uid = participant.uid; + if (!uid) { return; } + dispatch_async(dispatch_get_main_queue(), ^{ + [self.raisedHandUIDs removeObject:uid]; + [self updateRaisedHandsList:self.raisedHandUIDs]; + }); } ``` diff --git a/calls/ios/screen-sharing.mdx b/calls/ios/screen-sharing.mdx index 337a9836b..410e902cb 100644 --- a/calls/ios/screen-sharing.mdx +++ b/calls/ios/screen-sharing.mdx @@ -95,29 +95,63 @@ class CallViewController: UIViewController, ParticipantEventListener { -## Check Screen Share Status +## Track Who Is Sharing -Use the `isPresenting` property on the `Participant` object to check if someone is sharing their screen: +`Participant` carries **no** screen-share property, and the SDK exposes no getter for one. Track the +current presenter yourself from the start/stop callbacks. + + + These callbacks fire only when a share **starts or stops**. A client that joins mid-call cannot + discover a share that was already in progress before it joined. + ```swift -func onParticipantListChanged(participants: [Participant]) { - if let presenter = participants.first(where: { $0.isPresenting }) { - print("\(presenter.name ?? "") is currently sharing their screen") +// uids of participants currently sharing. +private var presenterUIDs = Set() + +func onParticipantStartedScreenShare(participant: Participant) { + guard let uid = participant.uid else { return } + DispatchQueue.main.async { + self.presenterUIDs.insert(uid) + print("\(participant.name ?? uid) started sharing their screen") + } +} + +func onParticipantStoppedScreenShare(participant: Participant) { + guard let uid = participant.uid else { return } + DispatchQueue.main.async { + self.presenterUIDs.remove(uid) } } + +// A participant who leaves while sharing never sends a stop event. +func onParticipantLeft(participant: Participant) { + guard let uid = participant.uid else { return } + DispatchQueue.main.async { self.presenterUIDs.remove(uid) } +} ``` ```objectivec -- (void)onParticipantListChangedWithParticipants:(NSArray *)participants { - for (Participant *p in participants) { - if (p.isPresenting) { - NSLog(@"%@ is currently sharing their screen", p.name); - break; - } - } +@property (nonatomic, strong) NSMutableSet *presenterUIDs; + +- (void)onParticipantStartedScreenShareWithParticipant:(Participant *)participant { + NSString *uid = participant.uid; + if (!uid) { return; } + dispatch_async(dispatch_get_main_queue(), ^{ + [self.presenterUIDs addObject:uid]; + NSLog(@"%@ started sharing their screen", participant.name ?: uid); + }); +} + +- (void)onParticipantStoppedScreenShareWithParticipant:(Participant *)participant { + NSString *uid = participant.uid; + if (!uid) { return; } + dispatch_async(dispatch_get_main_queue(), ^{ + [self.presenterUIDs removeObject:uid]; + }); } ``` From 65695e81e4ce309828bd3efd6822b894aa928ec3 Mon Sep 17 00:00:00 2001 From: Ashfaaq Ali Date: Thu, 3 Sep 2026 14:59:14 +0530 Subject: [PATCH 4/5] docs(notifications/ios): document the shipping push SDK; fix deprecated + phantom Calls APIs MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit CometChatPushNotifications 1.0.0 is published (CocoaPods trunk 2026-07-24, and an SPM binaryTarget on the cometchat/push-notifications-sdk-ios v1 branch) and is the recommended way to add push + VoIP to an iOS app. It had NO documentation anywhere in this repo — a search for the SDK by any name returned zero pages — while its own podspec `documentation_url` points at /notifications/push-overview, which routes to a guide that predates it. NEW: notifications/ios-push-notifications-sdk.mdx The three-step integration, verified line by line against the SDK source: initialize(config:) + delegate, forward the APNs token, implement CometChatPushNotificationsDelegate. The app writes no PKPushRegistryDelegate and no CXProviderDelegate — the SDK owns both. Every symbol on the page was checked against the source before writing it, and the distribution facts were checked against the live public repo rather than the private one: - SPM URL github.com/cometchat/push-notifications-sdk-ios (200). NOTE: the private repo's podspec `s.source` points at cometchat/push-notifications-ios, which 404s, and its README's SPM URL is still the literal placeholder `https:///CometChatPushNotifications`. Neither is usable; the page documents the URL that actually resolves. Raised separately against the SDK repo. - The import is `CometChatPushNotificationsSwift` (the podspec's module_name), not `CometChatPushNotifications` (the class). Called out explicitly — it is the first thing that will not compile otherwise. - No subspecs are documented. The private podspec has Core/Calls/FCM and its README advertises `pod 'CometChatPushNotifications/Core'`, but the PUBLIC podspec consumers actually get is a single vendored xcframework with none of them, so that install line fails. - iOS 15.1 floor, CometChatSDK >= 4.1.5, CometChatCallsSDK >= 5.0.0, from the public podspec. FIXED in notifications/ios-apns-push-notifications.mdx - `CometChatCalls.audioMuted(_:)` and `CometChatCalls.endSession()` are @available(deprecated) in Calls SDK 5.0.4. Replaced with the successors the SDK's own deprecation messages name: CallSession.shared.muteAudio()/ unmuteAudio() and CallSession.shared.leaveSession(). - Three editorial leaks removed. Two comments read "Removed CometChatCalls.startAudioSession() as per instructions" — internal review notes shipped to public docs, naming two methods that exist in NEITHER SDK (0 occurrences in the Calls and Chat interfaces). A third read "MARK: - CRITICAL: Audio Session Delegates (MISSING IN YOUR CODE)". DELIBERATELY NOT CHANGED `CometChatCallsSDK.CallSettingsBuilder` on that page is deprecated in the Calls SDK, but CometChatOngoingCall.set(callSettingsBuilder:) takes `Any?` and force-casts to `CallSettingsBuilder` internally, so the UI Kit still requires it. Swapping in SessionSettingsBuilder would crash. That is UI Kit product debt, not a docs defect. Cross-linked from push-overview (as the recommended iOS card), from calls/ios/voip-calling, and from the manual APNs guide, so the hand-wired path is reachable but no longer the default. docs.json nav updated; JSON revalidated. Co-Authored-By: Claude Opus 5 --- calls/ios/voip-calling.mdx | 8 + docs.json | 1 + notifications/ios-apns-push-notifications.mdx | 21 +- notifications/ios-push-notifications-sdk.mdx | 260 ++++++++++++++++++ notifications/push-overview.mdx | 8 +- 5 files changed, 289 insertions(+), 9 deletions(-) create mode 100644 notifications/ios-push-notifications-sdk.mdx diff --git a/calls/ios/voip-calling.mdx b/calls/ios/voip-calling.mdx index 786219fd7..aac2fbf80 100644 --- a/calls/ios/voip-calling.mdx +++ b/calls/ios/voip-calling.mdx @@ -39,6 +39,14 @@ Before implementing VoIP calling, ensure you have: - [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 + + + **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 diff --git a/docs.json b/docs.json index 67354129b..502f47077 100644 --- a/docs.json +++ b/docs.json @@ -6516,6 +6516,7 @@ "group": "Getting Started", "pages": [ "notifications/android-push-notifications", + "notifications/ios-push-notifications-sdk", "notifications/ios-apns-push-notifications", "notifications/ios-fcm-push-notifications", "notifications/flutter-push-notifications-android", diff --git a/notifications/ios-apns-push-notifications.mdx b/notifications/ios-apns-push-notifications.mdx index b6b86055e..6931637ce 100644 --- a/notifications/ios-apns-push-notifications.mdx +++ b/notifications/ios-apns-push-notifications.mdx @@ -11,6 +11,16 @@ description: "Implement APNs push notifications with CometChat UIKit for iOS, in Reference implementation of iOS UIKit, APNs and Push Notification Setup. + + **There is a drop-in alternative to this guide.** The + [iOS Push Notifications SDK](/notifications/ios-push-notifications-sdk) + (`CometChatPushNotifications`) ships everything below — token registration, foreground + presentation, quick reply, badge counts, PushKit and CallKit — as a dependency, so you do not + copy helper files or write a `CXProviderDelegate`. It works with or without the UI Kit. + + Use this page when you need direct control over the PushKit and CallKit layer. + + ## What this guide covers - CometChat dashboard setup (enable push, add APNs Device + APNs VoIP providers) with screenshots. @@ -220,11 +230,12 @@ extension AppDelegate: PKPushRegistryDelegate, CXProviderDelegate { func provider(_ provider: CXProvider, perform action: CXSetMutedCallAction) { print("User toggled mute: \(action.isMuted)") - CometChatCalls.audioMuted(action.isMuted) + // v5: the deprecated CometChatCalls.audioMuted(_:) maps to these. + action.isMuted ? CallSession.shared.muteAudio() : CallSession.shared.unmuteAudio() action.fulfill() } - // MARK: - CRITICAL: Audio Session Delegates (MISSING IN YOUR CODE) + // MARK: - Audio Session Delegates /// Called when CallKit activates the audio session func provider(_ provider: CXProvider, didActivate audioSession: AVAudioSession) { @@ -232,15 +243,11 @@ extension AppDelegate: PKPushRegistryDelegate, CXProviderDelegate { // Configure audio session for VoIP configureAudioSession() - - // Removed CometChatCalls.startAudioSession() as per instructions } /// Called when CallKit deactivates the audio session func provider(_ provider: CXProvider, didDeactivate audioSession: AVAudioSession) { print("Audio session deactivated") - - // Removed CometChatCalls.stopAudioSession() as per instructions } // MARK: - Audio Session Configuration @@ -797,7 +804,7 @@ extension CometChatAPNsHelper { } } else { CometChat.endCall(sessionID: CometChat.getActiveCall()?.sessionID ?? "") { call in - CometChatCalls.endSession() + CallSession.shared.leaveSession() action.fulfill() print("CallKit: End call success") DispatchQueue.main.async { [self] in diff --git a/notifications/ios-push-notifications-sdk.mdx b/notifications/ios-push-notifications-sdk.mdx new file mode 100644 index 000000000..317652e12 --- /dev/null +++ b/notifications/ios-push-notifications-sdk.mdx @@ -0,0 +1,260 @@ +--- +title: "iOS Push Notifications SDK" +description: "Drop-in push notifications and VoIP calling for iOS with CometChatPushNotifications — APNs and PushKit token registration, CallKit, foreground presentation, quick reply and badge counts." +--- + + + **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). + + +## 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 | + +## Install + + + +In Xcode, **File → Add Package Dependencies** and add: + +``` +https://github.com/cometchat/push-notifications-sdk-ios +``` + +Pick version **1.0.0** or later, and add the **`CometChatPushNotificationsSwift`** library to your +app target. + + +```ruby +platform :ios, '15.1' + +target 'YourApp' do + use_frameworks! + pod 'CometChatPushNotifications', '1.0.0' +end +``` + +Then `pod install`. The pod pulls `CometChatSDK` and `CometChatCallsSDK` automatically. + + + + + The module you import is **`CometChatPushNotificationsSwift`**, not `CometChatPushNotifications`. + The latter is the name of the class inside it. + + +## Before you start + +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. + +## 1. Initialize + +Call this **after** `CometChat.init(...)`: + +```swift +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 +``` + +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. + + + 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. + + +## 2. Forward the APNs token + +In your `AppDelegate`: + +```swift +func application(_ application: UIApplication, + didRegisterForRemoteNotificationsWithDeviceToken deviceToken: Data) { + CometChatPushNotifications.shared.registerDeviceToken(deviceToken) +} + +// 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). +func application(_ application: UIApplication, + didReceiveRemoteNotification userInfo: [AnyHashable: Any], + fetchCompletionHandler completionHandler: @escaping (UIBackgroundFetchResult) -> Void) { + CometChatPushNotifications.shared.handleBackgroundNotification(userInfo: userInfo) + completionHandler(.newData) +} +``` + +`registerDeviceToken` takes optional `onSuccess` / `onError` closures if you want the result. + + + 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. + + +That is the only `AppDelegate` code required. There is no `PKPushRegistryDelegate` and no +`CXProviderDelegate` to write. + +## 3. Implement the delegate + +Every method has a default no-op implementation — override only what you need. + +```swift +extension MyRootCoordinator: CometChatPushNotificationsDelegate { + + // 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 */ } + + // 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) { } + + // The SDK has already ended the call and the CallKit session — just dismiss your UI. + func onCallCleanupComplete() { } + + // MARK: Optional observability + func onCallMissed(call: Call, reason: CometChatMissedCallReason) { } + func onCallMuteStateChanged(isMuted: Bool) { } + func onPushTokenRegistered(platform: CometChatPushTokenPlatform) { } + func onPushTokenRegistrationFailed(platform: CometChatPushTokenPlatform, + error: CometChatException) { + // Fires for automatic registrations too — the fastest way to catch a wrong Provider ID. + } +} +``` + +| Type | Values | +|---|---| +| `CometChatPushTokenPlatform` | `.apns` · `.fcm` · `.voip` | +| `CometChatMissedCallReason` | `.unanswered` · `.cancelled` | + +## Cold start: tell the SDK when the Calls SDK is ready + +A VoIP push can wake your app *before* your Calls SDK is initialized. The SDK buffers the +`presentCallScreen` callback until you say you are ready: + +```swift +CometChatCalls.init(callsAppSettings: settings, onSuccess: { _ in + CometChatPushNotifications.shared.notifyCallsSDKReady() +}, onError: { _ in }) +``` + + + Skip this and an incoming call that cold-starts the app will never reach `presentCallScreen`. + + +## Notification Service Extension + +Adds delivery receipts, sender avatars, campaign images and markdown-free notification text. + + + + **File → New → Target → Notification Service Extension.** + + + 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. + + + Link `CometChatPushNotificationsSwift` to the extension target and replace the generated class: + +```swift +import CometChatPushNotificationsSwift + +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. + + + +Optional overrides: `stripsMarkdown`, `attachesMedia`, and `finalizeContent(_:)` for last-chance +content customization. + +## Configuration reference + +```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 +) +``` + +`foregroundCallPresentation` decides what happens when a call arrives while the app is open: + +| 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 | + +## Other APIs + +| API | Use | +|---|---| +| `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 | + +## 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). + +## Related + + + Dashboard setup, providers and templates. + + + The hand-wired alternative, if you need full control over PushKit and CallKit. + + + The in-app 1:1 call signaling this builds on. + diff --git a/notifications/push-overview.mdx b/notifications/push-overview.mdx index 6d83a545f..8616d813b 100644 --- a/notifications/push-overview.mdx +++ b/notifications/push-overview.mdx @@ -47,8 +47,12 @@ CometChat listens for chat and call events, assembles payloads from your templat UI Kit implementation -} href="/notifications/ios-apns-push-notifications"> -UI Kit implementation +} href="/notifications/ios-push-notifications-sdk"> +Drop-in SDK — APNs, VoIP and CallKit handled for you. Works with or without the UI Kit. + + +} href="/notifications/ios-apns-push-notifications"> +UI Kit implementation, hand-wired PushKit + CallKit } href="/notifications/ios-fcm-push-notifications"> From 37b4c12bcd99a1816e9c086f26f59e7a9264a781 Mon Sep 17 00:00:00 2001 From: Ashfaaq Ali Date: Thu, 3 Sep 2026 15:47:53 +0530 Subject: [PATCH 5/5] fix(notifications/ios): release the buffered VoIP call after LOGIN, not init MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Caught by driving the new page through the skill pipeline: a review agent building from it flagged the cold-start sequence as a race, and the SDK source confirms it. `notifyCallsSDKReady()` only sets `isCallsSDKReady` and fires the buffered `presentCallScreen`. It does NOT check `CometChat.getLoggedInUser()` or any Calls-SDK auth state. So calling it at `CometChatCalls.init` success — which is what the SDK's own README and docstring say, and what this page said — releases the buffered call while the session is still unauthenticated, and the `joinSession` that follows `presentCallScreen` fails on auth. The example now releases from the `login` success instead. Also documents a constraint that was nowhere: `presentCallWhenReady` schedules a 3-second safety timeout that fires the buffered presentation even if `notifyCallsSDKReady()` was never called, so the call is never silently dropped. A cold start whose login takes longer than 3s therefore reaches `presentCallScreen` before login completes, and the call screen must not assume a live session. The same correction was applied to the cometchat-ios-v5-sdk skill. Co-Authored-By: Claude Opus 5 --- notifications/ios-push-notifications-sdk.mdx | 23 ++++++++++++++++---- 1 file changed, 19 insertions(+), 4 deletions(-) diff --git a/notifications/ios-push-notifications-sdk.mdx b/notifications/ios-push-notifications-sdk.mdx index 317652e12..cd5a15ba6 100644 --- a/notifications/ios-push-notifications-sdk.mdx +++ b/notifications/ios-push-notifications-sdk.mdx @@ -161,19 +161,34 @@ extension MyRootCoordinator: CometChatPushNotificationsDelegate { ## Cold start: tell the SDK when the Calls SDK is ready -A VoIP push can wake your app *before* your Calls SDK is initialized. The SDK buffers the -`presentCallScreen` callback until you say you are ready: +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. + +Call `notifyCallsSDKReady()` once **login has resolved** — not merely when init succeeds: ```swift CometChatCalls.init(callsAppSettings: settings, onSuccess: { _ in - CometChatPushNotifications.shared.notifyCallsSDKReady() + CometChatCalls.login(authToken: token, onSuccess: { _ in + // Release the buffered call only now — joining needs an authenticated session. + CometChatPushNotifications.shared.notifyCallsSDKReady() + }, onError: { _ in }) }, onError: { _ in }) ``` - Skip this and an incoming call that cold-starts the app will never reach `presentCallScreen`. + **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. + + 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. + + ## Notification Service Extension Adds delivery receipts, sender avatars, campaign images and markdown-free notification text.