diff --git a/calls/flutter/actions.mdx b/calls/flutter/actions.mdx
index 6a4776445..c37cf5770 100644
--- a/calls/flutter/actions.mdx
+++ b/calls/flutter/actions.mdx
@@ -219,12 +219,16 @@ bool? isActive = CallSession.getInstance()?.isCallSessionActive();
### 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 (`setPeerCall(true)`) this is the hang-up: the other side's session ends too. In a group call the session continues for the other participants.
```dart
-await CallSession.getInstance()?.leaveSession();
+await CallSession.getInstance()?.leave();
```
+
+`leaveSession()` and `endCallSession()` are deprecated since 5.1; use `leave()`. To end a call that is still ringing, use `CometChatCalls.endCall` with `cancelled`, as described in [Ringing](/calls/flutter/ringing#cancel-a-call); `leave()` alone doesn't tell the other side why.
+
+
### Raise Hand
Shows a hand-raised indicator to get attention from other participants.
@@ -283,7 +287,7 @@ These methods are aliases for existing actions, provided for naming convenience.
### End Call Session
-Alias for `leaveSession()`. Ends your participation and disconnects gracefully.
+Deprecated alias for `leave()`. Ends your participation and disconnects gracefully.
```dart
await CallSession.getInstance()?.endCallSession();
diff --git a/calls/flutter/background-handling.mdx b/calls/flutter/background-handling.mdx
index f7afa4623..8eae513a4 100644
--- a/calls/flutter/background-handling.mdx
+++ b/calls/flutter/background-handling.mdx
@@ -39,12 +39,24 @@ flowchart LR
| User enters Picture-in-Picture | [PiP Mode](/calls/flutter/picture-in-picture) handles this |
| User presses HOME during call | **Use CometChatOngoingCallService** |
| User switches to another app | **Use CometChatOngoingCallService** |
-| Receiving calls when app is killed | [VoIP Calling](/calls/flutter/voip-calling) handles this |
+| Receiving calls when app is killed | [Push Ringing](/calls/flutter/voip-calling) keeps the app alive from the ring until the answered call joins; from then on, use **CometChatOngoingCallService** |
-Background Handling is different from [VoIP Calling](/calls/flutter/voip-calling). VoIP handles **receiving** calls when the app is not running. Background Handling keeps an **active** call alive when the user leaves the app.
+Background Handling is different from [Push Ringing](/calls/flutter/voip-calling). They cover different phases of a call on Android:
+
+| Phase | What keeps the app alive |
+|-------|--------------------------|
+| Ringing: the push arrives, until Answer | `cometchat_voip`'s ringing service |
+| Answering: Answer tapped, until the session is joined | The same service, as "Connecting call…" |
+| In the call: joined, and the user presses Home or switches apps | **`CometChatOngoingCallService`, which your app starts** |
+
+`cometchat_voip` stops its service once the session is joined, so a joined call needs this page whether it was rung through push or not. It applies to every call: ringing calls, direct calls, group calls and meetings.
+
+**Using the CometChat UI Kit?** Its call screen starts `CometChatOngoingCallService` when the call joins and stops it when the call ends. There's nothing to add.
+
+
`CometChatOngoingCallService` starts an Android foreground service with a persistent notification showing mute/unmute and hang up actions. On iOS, this is a no-op since iOS uses CallKit for ongoing call notifications.
@@ -53,70 +65,52 @@ Background Handling is different from [VoIP Calling](/calls/flutter/voip-calling
## Implementation
-### Step 1: Add Manifest Permissions (Android)
+### Step 1: Permissions (Android)
-Add the required permissions to your `android/app/src/main/AndroidManifest.xml`:
+The Calls SDK declares the foreground service permissions it needs (`FOREGROUND_SERVICE`, `FOREGROUND_SERVICE_MICROPHONE` and `FOREGROUND_SERVICE_CAMERA`). On Android 13 and later the notification also needs `POST_NOTIFICATIONS`; declare it in `android/app/src/main/AndroidManifest.xml` and request it at runtime (`cometchat_voip` already declares and requests it if your app uses it):
```xml
-
-
-
-
-
-
-
-
-
-
+
```
-### Step 2: Start Service on Session Join
+### Step 2: Start the service on join, stop it on leave
-Start the ongoing call service when the user successfully joins a call session:
+Start the ongoing call service when the user joins the call session, and stop it when the call ends. Pass `isVideo: false` for an audio call, so the service doesn't use the camera.
```dart
-class _SessionStatusListener extends SessionStatusListeners {
- @override
- void onSessionJoined() {
- // Start the foreground service to keep call alive in background
- CometChatOngoingCallService.launch();
- }
+import 'package:cometchat_calls_sdk/cometchat_calls_sdk.dart';
+import 'package:flutter/material.dart';
- @override
- void onSessionLeft() {
- // Stop the service when call ends
- CometChatOngoingCallService.abort();
- if (mounted) Navigator.of(context).pop();
- }
+class CallScreen extends StatefulWidget {
+ final String sessionId;
+ final bool isVideo;
+
+ const CallScreen({super.key, required this.sessionId, required this.isVideo});
@override
- void onConnectionClosed() {
- CometChatOngoingCallService.abort();
- }
+ State createState() => _CallScreenState();
}
-import 'package:cometchat_calls_sdk/cometchat_calls_sdk.dart';
-
class _CallScreenState extends State {
Widget? callWidget;
-
- late final SessionStatusListeners _sessionListener;
+ late final CallScreenSessionListener _sessionListener;
@override
void initState() {
super.initState();
- _setupSessionListener();
- _joinCall();
- }
-
- void _setupSessionListener() {
- _sessionListener = _SessionStatusListener();
+ _sessionListener = CallScreenSessionListener(
+ isVideo: widget.isVideo,
+ onCallEnded: () {
+ if (mounted) Navigator.of(context).pop();
+ },
+ );
CallSession.getInstance()?.addSessionStatusListener(_sessionListener);
+ _joinCall();
}
void _joinCall() {
final sessionSettings = (SessionSettingsBuilder()
- ..setTitle("My Call"))
+ ..setType(widget.isVideo ? SessionType.video : SessionType.audio))
.build();
CometChatCalls.joinSession(
@@ -127,14 +121,14 @@ class _CallScreenState extends State {
},
onError: (CometChatCallsException e) {
debugPrint("Failed to join: ${e.message}");
- Navigator.of(context).pop();
+ if (mounted) Navigator.of(context).pop();
},
);
}
@override
void dispose() {
- // Always stop the service when screen is disposed
+ // Always stop the service when the call screen goes away
CometChatOngoingCallService.abort();
CallSession.getInstance()?.removeSessionStatusListener(_sessionListener);
super.dispose();
@@ -147,97 +141,39 @@ class _CallScreenState extends State {
);
}
}
-```
-
-
-Flutter listeners are not lifecycle-aware. You must manually remove listeners in `dispose()` and call `CometChatOngoingCallService.abort()` to clean up.
-
----
+class CallScreenSessionListener extends SessionStatusListeners {
+ final bool isVideo;
+ final VoidCallback onCallEnded;
-## Complete Example
+ CallScreenSessionListener({required this.isVideo, required this.onCallEnded});
-```dart
-class _SessionStatusListener2 extends SessionStatusListeners {
@override
void onSessionJoined() {
- // Start foreground service (Android only, no-op on iOS)
- CometChatOngoingCallService.launch();
- }
-
- @override
- void onSessionLeft() {
- CometChatOngoingCallService.abort();
- if (mounted) Navigator.of(context).pop();
- }
-
- @override
- void onConnectionClosed() {
- CometChatOngoingCallService.abort();
- }
-}
-
-import 'package:cometchat_calls_sdk/cometchat_calls_sdk.dart';
-import 'package:flutter/material.dart';
-
-class CallScreen extends StatefulWidget {
- final String sessionId;
-
- const CallScreen({super.key, required this.sessionId});
-
- @override
- State createState() => _CallScreenState();
-}
-
-class _CallScreenState extends State {
- Widget? callWidget;
-
- @override
- void initState() {
- super.initState();
- _setupSessionListener();
- _joinCall();
- }
-
- void _setupSessionListener() {
- _sessionListener = _SessionStatusListener2();
- CallSession.getInstance()?.addSessionStatusListener(_sessionListener);
- }
-
- void _joinCall() {
- final sessionSettings = (SessionSettingsBuilder()
- ..setTitle("My Call"))
- .build();
-
- CometChatCalls.joinSession(
- sessionId: widget.sessionId,
- sessionSettings: sessionSettings,
- onSuccess: (Widget? widget) {
- setState(() => callWidget = widget);
- },
- onError: (CometChatCallsException e) {
- debugPrint("Failed to join: ${e.message}");
- if (mounted) Navigator.of(context).pop();
- },
- );
+ // Start the foreground service (Android only, no-op on iOS)
+ CometChatOngoingCallService.launch(isVideo: isVideo);
}
@override
- void dispose() {
+ void onSessionLeft() {
CometChatOngoingCallService.abort();
- CallSession.getInstance()?.removeSessionStatusListener(_sessionListener);
- super.dispose();
+ onCallEnded();
}
@override
- Widget build(BuildContext context) {
- return Scaffold(
- body: callWidget ?? const Center(child: CircularProgressIndicator()),
- );
+ void onConnectionClosed() {
+ CometChatOngoingCallService.abort();
+ onCallEnded();
}
}
```
+The service and its notification stay for the whole call, whether your app is in the foreground or not, until you call `abort()`.
+
+
+Flutter listeners are not lifecycle-aware. You must manually remove listeners in `dispose()` and call `CometChatOngoingCallService.abort()` to clean up.
+
+
---
## API Reference
@@ -246,7 +182,7 @@ class _CallScreenState extends State {
| Method | Description |
|--------|-------------|
-| `launch()` | Starts the Android foreground service with an ongoing notification. No-op on iOS. |
+| `launch({bool? isVideo})` | Starts the Android foreground service with an ongoing notification. Pass `isVideo: false` for an audio call; when omitted, the service reads the type from the session. No-op on iOS. |
| `abort()` | Stops the foreground service and removes the notification. No-op on iOS. |
@@ -258,5 +194,5 @@ On iOS, ongoing call state is managed by CallKit automatically. `CometChatOngoin
## Related Documentation
- [Picture-in-Picture](/calls/flutter/picture-in-picture) - Keep call visible while using other apps
-- [VoIP Calling](/calls/flutter/voip-calling) - Receive calls when app is killed
-- [Session Status Listener](/calls/flutter/session-status-listener) - Listen for session events
+- [Push Ringing](/calls/flutter/voip-calling) - Ring calls when the app is in the background or killed, until the answered call joins
+- [Events](/calls/flutter/events) - Listen for session events
diff --git a/calls/flutter/events.mdx b/calls/flutter/events.mdx
index e7445bc54..ad24d7c78 100644
--- a/calls/flutter/events.mdx
+++ b/calls/flutter/events.mdx
@@ -15,6 +15,8 @@ The `CallSession` is a singleton that manages the active call. All event listene
CallSession? callSession = CallSession.getInstance();
```
+The session exists as soon as it is **prepared**, before anyone joins: `CometChatCalls.initiateCall` prepares it on the caller, and `cometchat_voip` prepares it on the callee while the phone rings. That is what lets a caller listen for "ringing" and "answered" without joining (see [Ringing](/calls/flutter/ringing)).
+
Flutter listeners are **not** lifecycle-aware. You must manually remove all listeners in your widget's `dispose()` method to prevent memory leaks. Failing to do so may cause callbacks to fire on disposed widgets.
@@ -66,11 +68,11 @@ CallSession.getInstance()?.addSessionStatusListener(sessionStatusListener);
| Event | Description |
|-------|-------------|
| `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. On a prepared session: nobody joined in time |
| `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; the reason arrives through `onIncomingCallEnded` |
Remove the listener when no longer needed:
@@ -90,9 +92,15 @@ Monitor participant activities including join/leave, audio/video state, hand rai
```dart
class _ParticipantEventListener extends ParticipantEventListeners {
+ @override
+ void onParticipantConnected(Participant participant) {
+ // A participant's phone prepared the session but hasn't joined yet.
+ // On an outgoing call: the callee's phone is ringing.
+ }
+
@override
void onParticipantJoined(Participant participant) {
- // A participant joined the call
+ // A participant joined the call. On an outgoing call: the callee answered.
}
@override
@@ -115,7 +123,8 @@ CallSession.getInstance()?.addParticipantEventListener(participantEventListener)
| Event | Parameter | Description |
|-------|-----------|-------------|
-| `onParticipantJoined` | `Participant` | A participant connected to the call |
+| `onParticipantConnected` | `Participant` | A participant's phone prepared the session but hasn't joined. On an outgoing call: the callee's phone is ringing |
+| `onParticipantJoined` | `Participant` | A participant joined the call. On an outgoing call: the callee answered |
| `onParticipantLeft` | `Participant` | A participant disconnected from the call |
| `onParticipantListChanged` | `List` | Participant list updated |
| `onParticipantAudioMuted` | `Participant` | A participant muted their microphone |
diff --git a/calls/flutter/migrating-from-legacy-ringing.mdx b/calls/flutter/migrating-from-legacy-ringing.mdx
new file mode 100644
index 000000000..74a99e79e
--- /dev/null
+++ b/calls/flutter/migrating-from-legacy-ringing.mdx
@@ -0,0 +1,383 @@
+---
+title: "Migrating from Legacy Ringing"
+sidebarTitle: "Migrating from Legacy Ringing"
+sdk_version: "5.x"
+description: "Move a Flutter app from Chat SDK call signalling to ringing with the Calls SDK 5.1 and cometchat_voip: what to add, what to delete, and how each method maps."
+---
+
+Legacy ringing signalled calls through the Chat SDK: `CometChat.initiateCall` created the call, a `CallListener` on the WebSocket told the callee, `acceptCall` and `rejectCall` answered, and the app rang the device itself, often with its own FCM, PushKit and CallKit code or `flutter_callkit_incoming`. Ringing now runs on the Calls SDK and `cometchat_voip`. 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/flutter/ringing); for the push side see [Push Ringing](/calls/flutter/voip-calling).
+
+{/* TODO(RC): every API on this page is from cometchat_calls_sdk 5.1.0-team and cometchat_voip release/2.0. Re-check against the released packages before publishing. */}
+
+## Why it changed
+
+| Legacy | Now |
+|--------|-----|
+| Incoming calls arrived on the Chat WebSocket, so a killed or backgrounded app needed separate push and CallKit code to ring | One path: the push rings in every app state, with the app killed included |
+| Your app rang the device: listener, incoming screen, ringtone, busy check | `cometchat_voip` rings, draws the incoming screen, and handles busy, timeout and missed calls |
+| `acceptCall`, `rejectCall` and `endCall` were requests your app had to sequence with `joinSession` | Answer means join. Decline, timeout and busy are sent by `cometchat_voip` from a queue that survives the app being killed |
+| The caller only knew "accepted" or "rejected" | The caller sees "Ringing…" (the callee's phone was reached), the answer, and the reason a call ended (`rejected`, `busy`, `unanswered`, `cancelled`) |
+| The Chat SDK was required for calling | The Chat SDK is 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 rgba(128,128,128,0.12)
+ Note over A,B: Legacy ringing
+ A->>Chat: CometChat.initiateCall(call)
+ Chat-->>B: CallListener.onIncomingCallReceived
+ B->>B: App shows its own incoming UI, plays ringtone
+ B->>Chat: CometChat.acceptCall(sessionId)
+ Chat-->>A: CallListener.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 V as Callee · cometchat_voip
+ participant C as Callee · Calls SDK
+ rect rgba(46,160,67,0.15)
+ Note over A,C: Ringing now
+ A->>BE: CometChatCalls.initiateCall(receiver, options)
+ BE-->>V: push (initiated)
+ V->>V: rings, shows the ring screen
+ V->>C: prepares the session
+ C-->>A: onParticipantConnected ("Ringing…")
+ V->>C: Answer → onCallAnswered → joinSession
+ C-->>A: onParticipantJoined → joinSession
+ A->>A: CallSession.leave() to hang up
+ end
+```
+
+## Step 1: Update dependencies
+
+```yaml
+dependencies:
+ cometchat_calls_sdk: ^5.1.0 # was 5.0.x
+ cometchat_voip: ^2.0.0 # new: rings incoming calls
+ # cometchat_sdk stays for chat; it is no longer used for calls
+```
+
+{/* TODO(RC): confirm the published versions of cometchat_calls_sdk and cometchat_voip on pub.dev. */}
+
+- `minSdk 26` on Android, `platform :ios, '15.1'` on iOS.
+- Keep `firebase_core`, `firebase_messaging` and your Firebase configuration.
+- Remove packages you used only to ring calls, such as `flutter_callkit_incoming`.
+
+## Step 2: Initialize cometchat_voip
+
+Initialize it right after the Calls SDK, from the same `CallAppSettings`:
+
+```dart
+await CometChatVoip.init(
+ CometChatVoipConfig.fromCallAppSettings(
+ callAppSettings,
+ providerId: "PROVIDER_ID",
+ ),
+);
+```
+
+`callMode` defaults to `CometChatCallMode.auto`, which is what this page assumes: `cometchat_voip` rings the call and draws the ring screen. Do this in your Android background message handler too (Step 3). The options are on [Push Ringing](/calls/flutter/voip-calling#step-2-initialize).
+
+## Step 3: Shrink your push handlers
+
+
+
+```dart
+@pragma('vm:entry-point')
+Future onBackgroundMessage(RemoteMessage message) async {
+ if (message.data["type"] == "call") {
+ // Parse the CometChat payload, then ring with your own code
+ await FlutterCallkitIncoming.showCallkitIncoming(callKitParams(message.data));
+ } else {
+ showChatNotification(message);
+ }
+}
+
+FirebaseMessaging.onMessage.listen((RemoteMessage message) {
+ // Calls were skipped in the foreground: CallListener showed them
+ if (message.data["type"] != "call") showChatNotification(message);
+});
+```
+
+
+```dart
+@pragma('vm:entry-point')
+Future onBackgroundMessage(RemoteMessage message) async {
+ // Initialize the Calls SDK and cometchat_voip here first (see Push Ringing, Step 3)
+ bool isCallPush = await CometChatVoip.handlePushNotification(
+ message.data,
+ sentTime: message.sentTime, // lets cometchat_voip drop a push delivered late
+ );
+ if (!isCallPush) showChatNotification(message);
+}
+
+FirebaseMessaging.onMessage.listen((RemoteMessage message) async {
+ bool isCallPush = await CometChatVoip.handlePushNotification(
+ message.data,
+ sentTime: message.sentTime,
+ );
+ if (!isCallPush) showChatNotification(message);
+});
+```
+
+
+
+Never skip call pushes in the foreground any more. The WebSocket no longer carries call signalling for your app; the push is the only signal, in every app state.
+
+On iOS, delete the PushKit and CallKit code in `AppDelegate`: `cometchat_voip` receives VoIP pushes and reports calls to CallKit itself. Keep no `PKPushRegistry` of your own; a second registry takes the VoIP pushes and those calls never ring.
+
+## Step 4: Change the token code
+
+
+
+```dart
+// Android
+await CometChatNotifications.registerPushToken(
+ PushPlatforms.FCM_FLUTTER_ANDROID,
+ fcmToken: fcmToken,
+ providerId: "FCM_PROVIDER_ID",
+);
+
+// iOS: the VoIP token forwarded from AppDelegate
+await CometChatNotifications.registerPushToken(
+ PushPlatforms.APNS_FLUTTER_VOIP,
+ voipToken: voipToken,
+ providerId: "APNS_VOIP_PROVIDER_ID",
+);
+```
+
+
+```dart
+// Android: after login
+await CometChatVoip.registerPushToken(token: fcmToken);
+FirebaseMessaging.instance.onTokenRefresh.listen(CometChatVoip.handleTokenRefresh);
+
+// iOS: nothing; cometchat_voip registers the PushKit token on login
+
+// Before logout, on both platforms
+await CometChatVoip.unregisterPushToken();
+```
+
+
+
+Chat notifications keep their own setup; see [Flutter push notifications](/notifications/flutter-push-notifications).
+
+## Step 5: Delete the app-side ringing
+
+Everything in this list is now done by `cometchat_voip`. Remove it, or it will fight the new ring (two ringtones, two screens, a busy reject sent on top of `cometchat_voip`'s).
+
+- `CometChat.addCallListener(...)` and its `onIncomingCallReceived` handler
+- The incoming call screen, overlay or dialog you showed from it
+- Your own ringtone for incoming calls
+- The "already in a call, reject with busy" check
+- Your call handling with `flutter_callkit_incoming` or a CallKit or ConnectionService integration of your own, with its event listeners
+- Any "app in foreground" check around call pushes
+
+Keep the microphone and camera usage descriptions in `Info.plist`.
+
+## Step 6: Rewrite the call code
+
+| Legacy | Now | Notes |
+|--------|-----|-------|
+| `CometChat.initiateCall(Call(...))` | `CometChatCalls.initiateCall(receiver:, options:)` | Set `options.type` and `options.sessionSettings`: the type defaults to video, and the session is prepared with these settings |
+| `CallListener.onIncomingCallReceived` | Nothing to write; `cometchat_voip` rings. For your own state use `CometChatVoip.onCallRinging` | The same whether the call arrived on a killed app or a running one |
+| `CallListener.onOutgoingCallAccepted` | `onParticipantJoined` for the callee, on the session `initiateCall` prepared (`CallSession.getInstance()`) | New in-between event: `onParticipantConnected` means the callee's phone is ringing |
+| `CallListener.onOutgoingCallRejected` | `IncomingCallListener.onIncomingCallEnded(call, reason)`, where `CometChatCallEndReason.fromWire(reason)` is `declined` | The caller's app needs `cometchat_voip` too. The raw reason differs per platform, and a busy decline normalises to `declined` as well |
+| `CallListener.onIncomingCallCancelled` | `CometChatVoip.onCallEnded` with `cancelled`; the ring has already stopped | |
+| `CallListener.onCallEndedMessageReceived` | `onSessionLeft` / `onConnectionClosed` on the joined session | |
+| `CometChat.acceptCall`, then `joinSession` | `joinSession` in `CometChatVoip.onCallAnswered`. Nothing else to send | `cometchat_voip` asked for the microphone and camera on Answer |
+| `CometChat.rejectCall(sessionId, rejected)` | The Decline action of `cometchat_voip`. Your own ring screen: `CometChatVoip.decline(sessionId)` | Sends `busy` by itself when the user is in another call |
+| `CometChat.rejectCall(sessionId, busy)` | Nothing; `cometchat_voip` decides busy | |
+| `CometChat.rejectCall(sessionId, cancelled)` | `CometChatCalls.endCall(sessionId:, status: EndCallStatus.cancelled)` | |
+| `leaveSession()` and `CometChat.endCall` | `CallSession.getInstance()?.leave()` | No status to send on a hang-up; a peer call ends for both people |
+| `CometChat.getActiveCall()` / `clearActiveCall()` | `CometChatCalls.getActiveSession()` | `CometChatVoip.getActiveCall()` for the call ringing on this phone |
+| `CometChat.logout` | `CometChatCalls.logout` as well | Otherwise the Calls SDK stays signed in as the old user |
+
+### Caller, before and after
+
+
+
+```dart
+class OutgoingCallListener with CallListener {
+ @override
+ void onOutgoingCallAccepted(Call call) => joinCall(call.sessionId!);
+
+ @override
+ void onOutgoingCallRejected(Call call) => closeOutgoingScreen();
+}
+
+CometChat.addCallListener("outgoing-call", OutgoingCallListener());
+
+// End button while ringing
+CometChat.rejectCall(sessionId, CometChatCallStatus.cancelled,
+ onSuccess: (Call call) {},
+ onError: (CometChatException e) {},
+);
+```
+
+
+```dart
+class ReceiverEvents extends ParticipantEventListeners {
+ final InitiatedCall call;
+ ReceiverEvents(this.call);
+
+ @override
+ void onParticipantConnected(Participant participant) {
+ if (participant.uid == call.receiver) setStatus("Ringing…");
+ }
+
+ @override
+ void onParticipantJoined(Participant participant) {
+ if (participant.uid == call.receiver) joinCall(call.sessionId, isVideo: call.isVideo);
+ }
+}
+
+class CallEndedListener extends IncomingCallListener {
+ final String sessionId;
+ CallEndedListener(this.sessionId);
+
+ @override
+ void onIncomingCallEnded(InitiatedCall call, String reason) {
+ if (call.sessionId != sessionId) return;
+ // The raw reason is worded differently on each platform: normalise it.
+ // declined, timedOut, cancelled, answeredElsewhere, remoteEnded, ...
+ closeOutgoingScreen(CometChatCallEndReason.fromWire(reason));
+ }
+}
+
+// In initiateCall's onSuccess
+CallSession.getInstance()?.addParticipantEventListener(ReceiverEvents(call));
+CometChatCalls.addIncomingCallListener("outgoing-call", CallEndedListener(call.sessionId));
+
+// End button while ringing
+CometChatCalls.endCall(
+ sessionId: sessionId,
+ status: EndCallStatus.cancelled,
+ onSuccess: () {},
+ onError: (CometChatCallsException e) {},
+);
+```
+
+
+
+### Callee, before and after
+
+
+
+```dart
+class IncomingCallHandler with CallListener {
+ @override
+ void onIncomingCallReceived(Call call) => showIncomingScreen(call);
+
+ @override
+ void onIncomingCallCancelled(Call call) => closeIncomingScreen();
+}
+
+// Accept button
+CometChat.acceptCall(sessionId,
+ onSuccess: (Call call) => joinCall(call.sessionId!),
+ onError: (CometChatException e) {},
+);
+
+// Decline button
+CometChat.rejectCall(sessionId, CometChatCallStatus.rejected,
+ onSuccess: (Call call) {},
+ onError: (CometChatException e) {},
+);
+```
+
+
+```dart
+// cometchat_voip rings the phone and handles Answer and Decline.
+CometChatVoip.onCallAnswered.listen((CometChatVoipCall call) {
+ showCallScreen();
+ joinCall(call.sessionId, isVideo: call.isVideo); // the user already answered
+});
+
+CometChatVoip.onCallEnded.listen((CometChatCallEnded ended) {
+ // Cancelled, answered elsewhere or rang out: close anything you showed
+});
+```
+
+
+
+## Step 7: Change the logout
+
+
+
+```dart
+await CometChatNotifications.unregisterPushToken();
+CometChat.logout(
+ onSuccess: (String message) {},
+ onError: (CometChatException e) {},
+);
+```
+
+
+```dart
+await CometChatVoip.unregisterPushToken();
+CometChatCalls.logout(
+ onSuccess: (String message) {},
+ onError: (CometChatCallsException e) {},
+);
+// Log out of the Chat SDK as well if your app uses chat
+```
+
+
+
+## If you drew your own incoming call screen
+
+Keep it, but drive it from `cometchat_voip` instead of the `CallListener`. Set `callMode: CometChatCallMode.autoCustomScreen` and show your screen from `CometChatVoip.onRingScreenRequested`; answer with `CometChatVoip.answer(sessionId)` and decline with `CometChatVoip.decline(sessionId)`. `cometchat_voip` still rings, times out and sends the decline; only the screen is yours. See [Your own ring screen](/calls/flutter/voip-calling#your-own-ring-screen).
+
+## What users will notice
+
+- Calls ring with the app killed and on the lock screen.
+- The caller sees "Ringing…" once the callee's phone is reached, and the reason when a call is declined or missed.
+- An unanswered call stops ringing after 45 seconds.
+- Missed calls appear as notifications on the callee's phone.
+- A call during a call is call waiting (answering ends the current call, declining sends busy), not an automatic reject.
+- The microphone and camera prompt appears on Answer the first time, on the phone that answers.
+
+## Rolling out
+
+Your users won't all update at once, so for a while some phones run your old app and some run the new one. Two things keep calls working between them.
+
+**Turn on the ringing migration setting in the dashboard** before you release the new app. It keeps calls between the two app versions working while your users update.
+
+
+**Pending for this release:** the setting's name, where it sits in the dashboard and a screenshot of it. Don't publish this page until they are filled in — without them a reader can't complete this step.
+
+
+{/* TODO(RC): replace the Warning above with the real steps — dashboard menu path, the exact toggle name, and a screenshot. */}
+
+**Keep [`acceptOnAnswer`](/calls/flutter/voip-calling#step-2-initialize) on** in `CometChatVoipConfig` (it's the default). When a user on the new app answers, it also reports the answer the legacy way, so a caller still on the old app joins the call. Turn it off once every caller has the new app.
+
+## Checklist
+
+- [ ] `cometchat_calls_sdk` and `cometchat_voip` added; `minSdk 26` and iOS 15.1
+- [ ] `cometchat_voip` initialized at app start and in the Android background message handler
+- [ ] Every push goes to `handlePushNotification` with `message.sentTime`, in the foreground too
+- [ ] Legacy token registration replaced; the token is unregistered before logout
+- [ ] `CometChat.addCallListener`, your own incoming screen, ringtone, busy check and CallKit or `flutter_callkit_incoming` code removed
+- [ ] `initiateCall` sets `type` and the session settings, with `setPeerCall(true)`
+- [ ] The caller listens on the prepared session and on `IncomingCallListener`, and joins only on `onParticipantJoined`
+- [ ] The callee joins in `onCallAnswered`
+- [ ] Hang-up uses `leave()`; no `CometChat.endCall`
+- [ ] Logout also logs out of the Calls SDK
+- [ ] `checkRingReadiness` runs after login, and anything other than `ready` is shown to the user
+- [ ] Call pushes are sent at high priority
+- [ ] The dashboard migration setting is on before the release
+- [ ] Tested on real devices: ring with the app killed, decline, cancel, no answer, a call during a call, logout while ringing
diff --git a/calls/flutter/migration-guide-v5.mdx b/calls/flutter/migration-guide-v5.mdx
index fc21aaff8..3a6c5970a 100644
--- a/calls/flutter/migration-guide-v5.mdx
+++ b/calls/flutter/migration-guide-v5.mdx
@@ -488,3 +488,18 @@ These classes still exist in v5 for backward compatibility but are deprecated:
| `CallSwitchRequestInfo` | *Removed (no replacement)* |
| `CometChatCalls.startSession()` | `CometChatCalls.joinSession()` |
| `CometChatCalls.endSession()` | `CallSession.getInstance()?.leaveSession()` |
+
+---
+
+## 5.0 to 5.1
+
+5.1 moves ringing onto the Calls SDK and `cometchat_voip`, and deprecates one more thing:
+
+| Deprecated in 5.1 | Replacement |
+|-------------------|-------------|
+| `CallSession.leaveSession()` | `CallSession.leave()` |
+| Chat SDK calling (superseded; deprecated in a coming Chat SDK release): `CometChat.initiateCall`, `acceptCall`, `rejectCall`, `endCall`, `getActiveCall`, `clearActiveCall`, `addCallListener`, `CallListener` | `CometChatCalls.initiateCall`, `endCall(sessionId:, status:)`, `addIncomingCallListener`, `cometchat_voip`, `leave()` |
+
+New in 5.1: `CometChatCalls.initiateCall`, `endCall(sessionId:, status:)`, `getActiveSession`, `addIncomingCallListener` / `IncomingCallListener`, `addLoginListener` / `LoginListener`, `ParticipantEventListeners.onParticipantConnected`.
+
+The step-by-step move from Chat SDK signalling is on [Migrating from Legacy Ringing](/calls/flutter/migrating-from-legacy-ringing).
diff --git a/calls/flutter/overview.mdx b/calls/flutter/overview.mdx
index 8542545c1..1ef75c88f 100644
--- a/calls/flutter/overview.mdx
+++ b/calls/flutter/overview.mdx
@@ -35,15 +35,20 @@ sequenceDiagram
CometChatCalls-->>App: CallSession
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`, `cometchat_voip` rings the callee, and both sides join the session once the callee answers. See [Ringing](/calls/flutter/ringing) and [Push Ringing](/calls/flutter/voip-calling).
+
## Features
- Incoming and outgoing call notifications with accept/reject functionality
+ Start a call, ring the callee through push, show "Ringing…", answer, decline, cancel and end
diff --git a/calls/flutter/ringing.mdx b/calls/flutter/ringing.mdx
index 01e3d2671..afbbab4d0 100644
--- a/calls/flutter/ringing.mdx
+++ b/calls/flutter/ringing.mdx
@@ -2,314 +2,328 @@
title: "Ringing"
sidebarTitle: "Ringing"
sdk_version: "5.x"
-description: "Configure CometChat Calls SDK v5 ringing on Flutter for incoming calls, outgoing calls, call alerts, and accept or reject flows."
+description: "Ring users on Flutter with the CometChat Calls SDK and cometchat_voip: 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, killed, or the phone is locked.
+
+| Package | Owns |
+|---------|------|
+| **Calls SDK** (`cometchat_calls_sdk`) | Creating the call, the call session, cancelling and ending, and the call events your code listens to |
+| **cometchat_voip** | Ringing the callee's phone: the call notification, the ring screen (CallKit on iOS), the ringtone, Answer and Decline, the ring timeout, missed calls and busy handling |
-Ringing functionality requires the CometChat Chat SDK for Flutter 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`, `CallListener`) 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/flutter/migrating-from-legacy-ringing) for a method-by-method mapping.
-## How Ringing Works
+{/* TODO(RC): every API on this page is from cometchat_calls_sdk 5.1.0-team and cometchat_voip release/2.0. Re-check the snippets against the released packages before publishing. */}
-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. CometChat sends a push to the callee, `cometchat_voip` rings the callee's phone, and when the callee answers, both sides join the call session.
```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 Caller as Caller app
+ participant Server as CometChat
+ participant Voip as cometchat_voip (callee)
+ participant Callee as Callee app
+
+ Caller->>Server: CometChatCalls.initiateCall()
+ Note over Caller: Shows "Calling…"
+ Server->>Voip: Call push (FCM or VoIP)
+ Voip->>Voip: Rings the phone in any app state
+ Voip-->>Caller: onParticipantConnected
+ Note over Caller: Shows "Ringing…"
+ alt Callee answers
+ Voip->>Callee: onCallAnswered
+ Callee->>Server: CometChatCalls.joinSession()
+ Server-->>Caller: onParticipantJoined
+ Caller->>Server: CometChatCalls.joinSession()
+ else Callee declines
+ Voip->>Server: endCall(rejected)
+ Server-->>Caller: Push, then onIncomingCallEnded(rejected)
+ Note over Caller: Closes the call screen
+ end
```
-
-**Ringing imports BOTH SDKs, and both declare `User`.** `cometchat_calls_sdk` and `cometchat_sdk`
-each export a `User` class, so importing both barrels makes the name ambiguous and the file will
-not compile. Hide it from one import:
-
-```dart
-import 'package:cometchat_calls_sdk/cometchat_calls_sdk.dart' hide User;
-import 'package:cometchat_sdk/cometchat_sdk.dart';
-```
+## Prerequisites
-Pick whichever side your code actually uses; the Chat SDK's `User` is the one the signalling
-callbacks hand you.
-
+- The [Calls SDK set up](/calls/flutter/setup) and the user [logged in](/calls/flutter/authentication) on both devices.
+- `cometchat_voip` set up on **both** the caller's and the callee's app. The whole push side of ringing is on the [Push Ringing](/calls/flutter/voip-calling) page; this page assumes it is done. The caller needs it too: a decline reaches the caller as a push.
-## Initiate a Call
+## Start a call
-Use the Chat SDK to initiate a call to a user or group:
+`CometChatCalls.initiateCall` creates the call, which rings the callee, and prepares the caller's call session. The caller does **not** join yet.
```dart
-String receiverID = "USER_ID";
-String receiverType = CometChatReceiverType.user;
-String callType = CometChatCallType.video;
-
-Call call = Call(
- receiverUid: receiverID,
- receiverType: receiverType,
- type: callType,
-);
-
-CometChat.initiateCall(call,
- onSuccess: (Call call) {
- debugPrint("Call initiated: ${call.sessionId}");
- // Show outgoing call UI
- },
- onError: (CometChatException e) {
- debugPrint("Call initiation failed: ${e.message}");
- },
-);
-```
+SessionSettings callSettings({required bool isVideo}) {
+ return (SessionSettingsBuilder()
+ ..setType(isVideo ? SessionType.video : SessionType.audio)
+ ..setPeerCall(true))
+ .build();
+}
-| Parameter | Type | Description |
-|-----------|------|-------------|
-| `receiverID` | String | UID of the user or GUID of the group to call |
-| `receiverType` | String | `CometChatReceiverType.user` or `CometChatReceiverType.group` |
-| `callType` | String | `CometChatCallType.video` or `CometChatCallType.audio` |
+void startCall(String receiverUid, {bool isVideo = false}) {
+ CometChatCalls.initiateCall(
+ receiver: receiverUid,
+ options: InitiateCallOptions(
+ type: isVideo ? CallType.video : CallType.audio,
+ receiverType: ReceiverType.user,
+ sessionSettings: callSettings(isVideo: isVideo),
+ ),
+ onSuccess: (InitiatedCall call) {
+ showOutgoingScreen(call); // "Calling…"
+ listenForAnswer(call);
+ },
+ onError: (CometChatCallsException e) {
+ debugPrint("Could not start the call: ${e.message}");
+ },
+ );
+}
+```
-## Call Timeout
+| Parameter | Description |
+|-----------|-------------|
+| `receiver` | UID of the user to call, or the GUID of the group |
+| `options.type` | `CallType.audio` or `CallType.video` |
+| `options.receiverType` | `ReceiverType.user` (the default) for a user UID, or `ReceiverType.group` for a group GUID |
+| `options.sessionSettings` | The settings the caller's session is prepared with. See the warning below. |
-If the receiver does not answer, the call is eventually marked `unanswered` and the caller
-receives the `onOutgoingCallRejected` callback.
+`InitiatedCall` gives you `sessionId`, `messageId` (the id of the call message in the conversation), `sender`, `receiver`, `receiverType`, `type`, `status`, `initiatedAt`, `conversationId` and `isVideo`.
-**The timeout is server-side and cannot be set from the client.** `CometChat.initiateCall` takes
-only the `Call` object plus `onSuccess` / `onError` — there is no `timeout` parameter in the
-Flutter SDK. To end an unanswered call earlier than the server does, cancel it yourself with
-`CometChat.rejectCall(sessionId, CometChatCallStatus.cancelled)` on a timer you own.
+**Always set the call type.** `options.type` defaults to video, so an audio call started without it rings the callee as a video call. The session settings are fixed when the session is prepared, so build them the way the call should start. For a 1:1 call include `setPeerCall(true)`, so the call ends for both users when either one hangs up; leave it off for a group call, which should carry on while members remain.
-## Listen for Incoming Calls
+## Show "Ringing…" and detect the answer
-Register a call listener to receive incoming call notifications:
+The caller stays on the session that `initiateCall` prepared and listens to it. Don't call `joinSession` yet: the caller joins only once the callee has joined.
```dart
-String listenerID = "UNIQUE_LISTENER_ID";
+void listenForAnswer(InitiatedCall call) {
+ final session = CallSession.getInstance();
+ session?.addParticipantEventListener(ReceiverEvents(call));
+ session?.addSessionStatusListener(PreparedSessionEvents());
+ CometChatCalls.addIncomingCallListener("outgoing-call", CallEndedListener(call.sessionId));
+}
-class _AppCallListener with CallListener {
- @override
- void onIncomingCallReceived(Call call) {
- debugPrint("Incoming call from: ${(call.callInitiator as User?)?.name}");
- // Show incoming call UI with accept/reject options
- }
+class ReceiverEvents extends ParticipantEventListeners {
+ final InitiatedCall call;
+ ReceiverEvents(this.call);
+ // The callee's phone received the push and is ringing.
@override
- void onOutgoingCallAccepted(Call call) {
- debugPrint("Call accepted, joining session...");
- final sessionId = call.sessionId;
- if (sessionId != null) joinCallSession(sessionId);
+ void onParticipantConnected(Participant participant) {
+ if (participant.uid == call.receiver) setStatus("Ringing…");
}
+ // The callee answered: now the caller joins.
@override
- void onOutgoingCallRejected(Call call) {
- debugPrint("Call rejected");
- // Dismiss outgoing call UI
+ void onParticipantJoined(Participant participant) {
+ if (participant.uid == call.receiver) joinCall(call.sessionId, isVideo: call.isVideo);
}
+}
+class PreparedSessionEvents extends SessionStatusListeners {
+ // The prepared session closed before anyone joined: the call is over.
+ // The reason (rejected, busy, …) arrives through onIncomingCallEnded, usually first.
@override
- void onIncomingCallCancelled(Call call) {
- debugPrint("Incoming call cancelled");
- // Dismiss incoming call UI
- }
+ void onConnectionClosed() => closeOutgoingScreen();
@override
- void onCallEndedMessageReceived(Call call) {
- debugPrint("Call ended");
- }
+ void onSessionTimedOut() => closeOutgoingScreen();
}
-
-CometChat.addCallListener(listenerID, _AppCallListener());
```
-| Callback | Description |
-|----------|-------------|
-| `onIncomingCallReceived` | A new incoming call is received |
-| `onOutgoingCallAccepted` | The receiver accepted your outgoing call |
-| `onOutgoingCallRejected` | The receiver rejected your outgoing call, or the call timed out as unanswered |
-| `onIncomingCallCancelled` | The caller cancelled the incoming call |
-| `onCallEndedMessageReceived` | The call has ended |
+| Event | On the prepared session it means |
+|-------|----------------------------------|
+| `onParticipantConnected` (callee) | The callee's phone got the push and is ringing |
+| `onParticipantJoined` (callee) | The callee answered. Call `joinSession` now |
+| `onConnectionClosed` / `onSessionTimedOut` | The call ended before anyone joined |
+
+Remove the listeners when the outgoing call screen closes (`removeParticipantEventListener`, `removeSessionStatusListener`, `CometChatCalls.removeIncomingCallListener`).
-Remember to remove the call listener when it's no longer needed to prevent memory leaks. In Flutter, you must manually remove listeners in your widget's `dispose()` method:
-```dart
-CometChat.removeCallListener(listenerID);
-```
+Don't treat the call's server status `ongoing` as the answer. The callee's phone prepares the session while it rings, so the server can report `ongoing` before anyone answers. Only `onParticipantJoined` means the callee answered.
-## Accept a Call
+## Listen for calls ending
-When an incoming call is received, accept it using the Chat SDK:
+The caller learns how a call ended from the Calls SDK's incoming call listener; the callee learns it from `cometchat_voip`.
```dart
-void acceptIncomingCall(String sessionId) {
- CometChat.acceptCall(sessionId,
- onSuccess: (Call call) {
- debugPrint("Call accepted");
- final sessionId = call.sessionId;
- if (sessionId != null) joinCallSession(sessionId);
- },
- onError: (CometChatException e) {
- debugPrint("Accept call failed: ${e.message}");
- },
- );
+// Caller
+class CallEndedListener extends IncomingCallListener {
+ final String sessionId;
+ CallEndedListener(this.sessionId);
+
+ @override
+ void onIncomingCallEnded(InitiatedCall call, String reason) {
+ if (call.sessionId != sessionId) return;
+ // `reason` is the native end reason and its wording differs per platform.
+ // Normalise it before comparing.
+ closeOutgoingScreen(CometChatCallEndReason.fromWire(reason));
+ }
}
+
+// Callee
+CometChatVoip.onCallEnded.listen((CometChatCallEnded ended) {
+ closeRingUi(ended.call.sessionId); // ended.reason says why
+});
```
-## Reject a Call
+| Who | How the call ended |
+|-----|--------------------|
+| Caller | `onIncomingCallEnded` hands you a native reason string. Normalised with `CometChatCallEndReason.fromWire(reason)`: `declined` (the callee declined, here or because they were in another call), `timedOut` (nobody answered), `cancelled` (you cancelled from another device), `answeredElsewhere`, `remoteEnded`, `failed`, `filtered` |
+| Callee, still ringing | `onCallEnded` reason: `cancelled` (the caller gave up), `answeredElsewhere` (answered on another device), `timedOut` (nobody answered), `declined` (declined here) |
+| Callee, answered | Nothing; the joined session reports the end through `onSessionLeft` / `onConnectionClosed` |
+
+
+**Normalise the caller's reason; don't compare the raw string.** The two platforms use different words for the same end: Android reports the call status (`cancelled`, `rejected`, `busy`, `ended`, `unanswered`, `ongoing`), iOS reports a CallKit reason (`cancelled`, `declined`, `timedOut`, `answeredElsewhere`, `remoteEnded`, `failed`, `filtered`), and an end that arrives as a push is already in the second vocabulary. So `reason == "rejected"` matches on Android only. `CometChatCallEndReason.fromWire(reason)`, from `cometchat_voip`, reads every vocabulary and returns one value — the same values the callee's `onCallEnded` reports. It folds a busy decline into `declined`, so on the caller you can't tell "declined" from "was in another call".
+
+
+## Answer a call
-Reject an incoming call:
+`cometchat_voip` rings and, on **Answer**, asks for the microphone (and the camera for a video call) and opens your app. Listen for the answer and join. There is no separate "accept" call to make.
```dart
-void rejectIncomingCall(String sessionId) {
- String status = CometChatCallStatus.rejected;
-
- CometChat.rejectCall(sessionId, status,
- onSuccess: (Call call) {
- debugPrint("Call rejected");
- // Dismiss incoming call UI
- },
- onError: (CometChatException e) {
- debugPrint("Reject call failed: ${e.message}");
- },
- );
-}
+CometChatVoip.onCallAnswered.listen((CometChatVoipCall call) {
+ showCallScreen();
+ joinCall(call.sessionId, isVideo: call.isVideo);
+});
```
-## Cancel a Call
+Subscribe as early as possible after `CometChatVoip.init`. A call answered from the push that started the app is held until something listens. On iOS, `onCallAnswered` can fire while the phone is still locked; join right away, which connects the audio before the user unlocks.
-Cancel an outgoing call before it's answered:
+## Join the session
-```dart
-void cancelOutgoingCall(String sessionId) {
- String status = CometChatCallStatus.cancelled;
+Both sides join the same way: the callee from `onCallAnswered`, the caller from `onParticipantJoined`. `joinSession` takes over the session that was prepared earlier, so it connects almost at once.
- CometChat.rejectCall(sessionId, status,
- onSuccess: (Call call) {
- debugPrint("Call cancelled");
- // Dismiss outgoing call UI
+```dart
+void joinCall(String sessionId, {required bool isVideo}) {
+ CometChatCalls.joinSession(
+ sessionId: sessionId,
+ sessionSettings: callSettings(isVideo: isVideo),
+ onSuccess: (Widget? callWidget) {
+ showCallWidget(callWidget); // place it in your widget tree
},
- onError: (CometChatException e) {
- debugPrint("Cancel call failed: ${e.message}");
+ onError: (CometChatCallsException e) {
+ debugPrint("Join failed: ${e.message}");
+ closeCallScreen();
},
);
}
```
-## Join the Call Session
+On iOS, join within 20 seconds of the answer; a call that hasn't joined by then is ended. See [Join Session](/calls/flutter/join-session) for rendering the call UI, [Session Settings](/calls/flutter/session-settings) for layouts and controls, and [Events](/calls/flutter/events) for everything the joined session reports.
+
+## Decline a call
-After accepting a call (or when your outgoing call is accepted), join the call session using the Calls SDK:
+Declining is handled by `cometchat_voip`: the Decline action on the notification, the ring screen and CallKit sends `rejected`, or `busy` when the user is already in another call, and keeps retrying until it's delivered, even if the app is killed. The caller receives `onIncomingCallEnded`, which normalises to `declined` either way — see [Listen for calls ending](#listen-for-calls-ending).
+
+You only write decline code when you draw your own ring screen:
```dart
-void joinCallSession(String sessionId) {
- SessionSettings sessionSettings = SessionSettingsBuilder()
- .setType(SessionType.video)
- .build();
+CometChatVoip.decline(sessionId);
+```
- CometChatCalls.joinSession(
+## Cancel a call
+
+The caller ends a call that is still ringing with `endCall` and the `cancelled` status. The callee's ring stops and a missed call is shown there.
+
+```dart
+void cancelCall(String sessionId) {
+ CometChatCalls.endCall(
sessionId: sessionId,
- sessionSettings: sessionSettings,
- onSuccess: (Widget? callWidget) {
- debugPrint("Joined call session");
- // Place callWidget in your widget tree to render the call UI
- },
+ status: EndCallStatus.cancelled,
+ onSuccess: () => closeOutgoingScreen(),
onError: (CometChatCallsException e) {
- debugPrint("Failed to join: ${e.message}");
+ debugPrint("Cancel failed: ${e.message}");
+ closeOutgoingScreen(); // close locally either way
},
);
}
```
-
-In Flutter, `joinSession` returns a `Widget?` through the `onSuccess` callback. You must place this widget in your Flutter widget tree to render the call UI. See [Join Session](/calls/flutter/join-session) for more details.
-
+Disable the End button after the first tap, and ignore an `onParticipantJoined` that arrives after you cancelled: the cancel wins.
-## End a Call
+## Ring timeout
-Properly ending a call requires coordination between both SDKs to ensure all participants are notified and call logs are recorded correctly.
+The callee's `cometchat_voip` ends an unanswered 1:1 call as `unanswered` when its ring time is up (45 seconds unless the push carries its own). To close the caller's screen at the same moment even if that signal is late, run your own timer from `initiateCall` and end the call with the same status:
-
-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.
-
+```dart
+Timer(const Duration(seconds: 45), () {
+ if (!answered) {
+ CometChatCalls.endCall(
+ sessionId: sessionId,
+ status: EndCallStatus.unanswered,
+ onSuccess: () {},
+ onError: (CometChatCallsException e) {},
+ );
+ closeOutgoingScreen();
+ }
+});
+```
-```mermaid
-sequenceDiagram
- participant User
- participant CallsSDK
- participant ChatSDK
- participant OtherParticipant
-
- User->>CallsSDK: leaveSession()
- User->>ChatSDK: endCall(sessionId)
- ChatSDK->>OtherParticipant: onCallEndedMessageReceived
- OtherParticipant->>CallsSDK: leaveSession()
+## End a call
+
+Hang up by leaving the session. For a peer call (`setPeerCall(true)`), the other side's session ends too, and it receives `onSessionLeft` or `onConnectionClosed`. No status needs to be sent.
+
+```dart
+Future hangUp() async {
+ await CallSession.getInstance()?.leave();
+ closeCallScreen();
+}
```
-When using the default call UI, listen for the end call button click using `ButtonClickListeners` and call `endCall()`:
+The other participant:
```dart
-class _ButtonClickListener extends ButtonClickListeners {
+class CallScreenEvents extends SessionStatusListeners {
@override
- void onLeaveSessionButtonClicked() {
- endCall(currentSessionId);
- }
+ void onSessionLeft() => closeCallScreen();
+
+ @override
+ void onConnectionClosed() => closeCallScreen();
}
+```
-CallSession? callSession = CallSession.getInstance();
+
+`leaveSession()` is deprecated; 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.
+
-// Listen for end call button click
-callSession?.addButtonClickListener(_ButtonClickListener());
+## Second call while in a call
-void endCall(String sessionId) {
- // 1. Leave the call session (Calls SDK)
- CallSession.getInstance()?.leaveSession();
+Handled by `cometchat_voip`, the way a phone does it. A call that arrives while the user is in another call rings over it with "Answering ends your current call" (CallKit's **End & Accept** on iOS). **Answer** ends the live call and `onCallAnswered` fires for the new one: close the old call screen and join the new call. **Decline** sends `busy` and keeps the live call. On Android, a call that arrives while another is already ringing, or while the user's own outgoing call is ringing, is declined as `busy` without ringing. See [A call while in a call](/calls/flutter/voip-calling#a-call-while-in-a-call).
- // 2. Notify other participants (Chat SDK)
- CometChat.endCall(sessionId,
- onSuccess: (Call call) {
- debugPrint("Call ended successfully");
- Navigator.of(context).pop();
- },
- onError: (CometChatException e) {
- debugPrint("End call failed: ${e.message}");
- Navigator.of(context).pop();
- },
- );
-}
-```
+## Group calls
-The other participant receives `onCallEndedMessageReceived` callback and should leave the session:
+`initiateCall` also rings a group: pass the group's GUID as `receiver` and set `options.receiverType` to `ReceiverType.group`. Every member of the group whose app is set up for ringing is rung, and the first member to answer joins the session; the others keep ringing until they answer, decline or the ring time runs out.
-```dart
-CometChat.addCallListener(listenerID, CallListener(
- onCallEndedMessageReceived: (Call call) {
- CallSession.getInstance()?.leaveSession();
- Navigator.of(context).pop();
- },
- // Other callbacks...
-));
-```
+A group ring ends on each member's phone on its own, and nothing is sent back to the caller: a member who declines or lets the call ring out sends no `rejected` and no `unanswered`, so the caller learns only that somebody joined, through `onParticipantJoined`. Decide yourself when to give up on a group call — a timer on the caller, or `endCall` with `EndCallStatus.cancelled`.
+
+A session every member joins without ringing is still an option: share a session id with them (for example in a custom message) and have each one call `joinSession`.
+
+## Logout
+
+Before logging the user out, unregister the push token with `CometChatVoip.unregisterPushToken()`, then log out of the Calls SDK with `CometChatCalls.logout`. End or cancel any call of your own first; the Calls SDK can't send after the session is gone.
+
+## Call status values
+
+| Status | Meaning | Sent by |
+|--------|---------|---------|
+| `initiated` | Created, ringing | CometChat, on `initiateCall` |
+| `ongoing` | Answered | CometChat, when the callee answers |
+| `rejected` | The callee declined | The callee's `cometchat_voip` |
+| `busy` | The callee was in another call | The callee's `cometchat_voip` |
+| `cancelled` | The caller ended it before an answer | The caller, `endCall` |
+| `unanswered` | Nobody answered in the ring time | The callee's `cometchat_voip` (and optionally the caller's timer) |
+| `ended` | Hung up after being answered | CometChat, when the session ends |
+| `missed` | How an unanswered or cancelled incoming call appears in call logs | Call logs |
+
+## Related
-## 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 within the timeout duration |
+- [Push Ringing](/calls/flutter/voip-calling): packages and platform setup, initializing `cometchat_voip`, handing over pushes, the push token, ring readiness, the call modes and what happens when the app is killed.
+- [Migrating from Legacy Ringing](/calls/flutter/migrating-from-legacy-ringing).
+- [Events](/calls/flutter/events), [Actions](/calls/flutter/actions), [Call Logs](/calls/flutter/call-logs).
diff --git a/calls/flutter/setup.mdx b/calls/flutter/setup.mdx
index 1686df605..cd5b04fd6 100644
--- a/calls/flutter/setup.mdx
+++ b/calls/flutter/setup.mdx
@@ -28,9 +28,15 @@ Add the `cometchat_calls_sdk` dependency to your `pubspec.yaml` file:
```yaml
dependencies:
- cometchat_calls_sdk: ^5.0.7
+ cometchat_calls_sdk: ^5.1.0
```
+{/* TODO(RC): confirm the published cometchat_calls_sdk 5.1 version on pub.dev. */}
+
+
+**Ringing calls?** Incoming calls ring through `cometchat_voip`. Add it as well and follow [Push Ringing](/calls/flutter/voip-calling). The Calls SDK alone can start, join and end calls, but it can't ring a phone that isn't running your app.
+
+
`^5.0.3` also resolves to the latest 5.0.x, but pinning the floor at **5.0.7** makes the platform
requirements explicit: 5.0.7 raised the iOS deployment target from 13.0 to **15.1** and added
@@ -67,7 +73,7 @@ a manifest-merger error, not a warning.
-For Android 6.0 (API level 23) and above, you must request camera and microphone permissions at runtime before starting a call.
+For Android 6.0 (API level 23) and above, you must request camera and microphone permissions at runtime before starting or joining a call. When a call is answered from `cometchat_voip`'s ring, it asks for them before opening your app.
### iOS
diff --git a/calls/flutter/troubleshooting.mdx b/calls/flutter/troubleshooting.mdx
index 6ebb69c97..d3cf0a5d4 100644
--- a/calls/flutter/troubleshooting.mdx
+++ b/calls/flutter/troubleshooting.mdx
@@ -51,7 +51,12 @@ sdk_version: "5.x"
| --- | --- | --- |
| iOS crash on call join | Missing usage descriptions in Info.plist | Add `NSCameraUsageDescription` and `NSMicrophoneUsageDescription` to `ios/Runner/Info.plist` |
| Android crash on call join | Missing runtime permissions | Request `CAMERA` and `RECORD_AUDIO` permissions at runtime |
-| VoIP not working on iOS | Missing VoIP entitlement | Enable Push Notifications and Voice over IP capabilities. See [VoIP Calling](/calls/flutter/voip-calling) |
+| Incoming calls don't ring on iOS | Missing VoIP entitlement | Enable Push Notifications and Voice over IP capabilities. See [Push Ringing](/calls/flutter/voip-calling) |
+| Incoming calls don't ring on Android | `cometchat_voip` missing, pushes not handed to it, or the device isn't ring-ready | Pass every push to `CometChatVoip.handlePushNotification` with `sentTime`, from both message handlers, and run `checkRingReadiness`. See [Push Ringing](/calls/flutter/voip-calling) |
+| Rings, then the app dies before Answer | Call push sent at normal priority | Send call pushes at high priority |
+| Caller never sees "Ringing…" or the answer | Caller joined right after `initiateCall` | Stay on the prepared session and join on `onParticipantJoined`. See [Ringing](/calls/flutter/ringing) |
+| Audio call rings as video | `InitiateCallOptions.type` not set; it defaults to video | Set `type` and the session type in `sessionSettings` |
+| Calls still ring after logout | The push token wasn't unregistered | Call `CometChatVoip.unregisterPushToken()` before `CometChatCalls.logout` |
| Background call drops | OS kills the app | See [Background Handling](/calls/flutter/background-handling) for platform-specific setup |
---
diff --git a/calls/flutter/voip-calling.mdx b/calls/flutter/voip-calling.mdx
index 1fd66b24e..997fa7cff 100644
--- a/calls/flutter/voip-calling.mdx
+++ b/calls/flutter/voip-calling.mdx
@@ -1,491 +1,331 @@
---
-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 Flutter for call notifications, incoming call handling, and app wake flows."
+description: "Ring incoming CometChat calls on Flutter with cometchat_voip: setup, initialization, handing over call pushes, the push token, ring readiness, the call modes, call waiting, missed calls, and behaviour when the app is killed."
---
-Implement native VoIP calling that works when your app is in the background or killed. This guide shows how to integrate platform-specific push notification services with CometChat to display system call UI and provide a native calling experience.
+`cometchat_voip` turns a CometChat call push into a ringing phone: a call notification with Answer and Decline, a full-screen ring screen over the lock screen (CallKit on iOS), the ringtone, a ring timeout, missed calls and busy handling, whether your app is in the foreground, in the background or killed.
-## Overview
-
-VoIP calling differs from [basic in-app ringing](/calls/flutter/ringing) by leveraging platform-native call frameworks to:
-- Show incoming calls on lock screen with system UI
-- Handle calls when app is in background or killed
-- Integrate with Bluetooth, car systems, and wearables
-- Provide consistent call experience across devices
+This page covers the push side of ringing. Starting, answering and ending calls with the Calls SDK is on the [Ringing](/calls/flutter/ringing) page.
```mermaid
-flowchart TB
- subgraph "Incoming Call Flow"
- A[Push Notification] --> B{Platform?}
- B -->|Android| C[FCM]
- B -->|iOS| D[APNs + CallKit]
- C --> E[Foreground Service / System UI]
- D --> F[CallKit Native UI]
- E --> G{User Action}
- F --> G
- G -->|Accept| H[CometChat.acceptCall]
- G -->|Reject| I[CometChat.rejectCall]
- H --> J[CometChatCalls.joinSession]
- end
+flowchart LR
+ FCM["FCM push (Android)"] --> S["Your onMessage / onBackgroundMessage"]
+ S --> H["CometChatVoip.handlePushNotification()"]
+ VOIP["VoIP push (iOS)"] --> D
+ H --> D{cometchat_voip decides}
+ D -->|ring| R["Notification + ring screen + ringtone
CallKit on iOS"]
+ D -->|in another call| W[Rings over the live call]
+ D -->|another call ringing| B["Declined as busy (Android)"]
+ R -->|Answer| P[Mic / camera prompt]
+ P --> A["onCallAnswered → joinSession"]
+ R -->|Decline| Q["endCall(rejected), queued until sent"]
+ R -->|45 s| T["endCall(unanswered) + missed call"]
```
+{/* TODO(RC): every API on this page is from cometchat_calls_sdk 5.1.0-team and cometchat_voip release/2.0. Re-check the snippets against the released packages before publishing. */}
+
## Prerequisites
-Before implementing VoIP calling, ensure you have:
+- The [Calls SDK set up](/calls/flutter/setup) and the user [logged in](/calls/flutter/authentication).
+- Push notifications enabled in the CometChat dashboard, with the push providers, Firebase project and Apple credentials set up. See [Getting Started](/notifications/push-getting-started). Keep each provider's **Provider ID** at hand.
+- **Android:** Firebase set up in your app with `firebase_core` and `firebase_messaging`. Call pushes reach your app through `firebase_messaging`.
+- **iOS:** an APNs provider for VoIP pushes, set up as described in [Getting Started](/notifications/push-getting-started). Test on a real device; CallKit and VoIP pushes don't run in the simulator.
-- [CometChat Chat SDK](/sdk/flutter/overview) and [Calls SDK](/calls/flutter/setup) integrated
-- Push notifications configured for both platforms:
- - [Firebase Cloud Messaging (FCM)](/notifications/android-push-notifications) for Android
- - [APNs](/notifications/ios-apns-push-notifications) for iOS
-- [Push notifications enabled](/notifications/push-overview) in CometChat Dashboard
+## Step 1: Add the packages
-
-VoIP calling in Flutter requires native platform code alongside your Dart code. The push notification handling and system call UI integration must be implemented natively on each platform, then bridged to Flutter via method channels or platform-specific plugins.
-
+```yaml
+dependencies:
+ cometchat_calls_sdk: ^5.1.0
+ cometchat_voip: ^2.0.0
+```
-
-This documentation builds on the [Ringing](/calls/flutter/ringing) functionality. Make sure you understand basic call signaling before implementing VoIP.
-
+{/* TODO(RC): confirm the published versions of cometchat_calls_sdk and cometchat_voip on pub.dev. */}
----
+
+
+Set `minSdk 26` in `android/app/build.gradle`. `cometchat_voip` adds the permissions, services and screens it needs to your manifest.
+
+
+1. Set `platform :ios, '15.1'` in the `Podfile`.
+2. In Xcode, add the **Push Notifications** capability, and under **Background Modes** check **Voice over IP** and **Remote notifications**.
+3. Add `NSMicrophoneUsageDescription` to `Info.plist`, and `NSCameraUsageDescription` for video calls.
+
+No `AppDelegate` code is needed: `cometchat_voip` creates the app's PushKit registry and reports calls to CallKit.
+
+
+
+
+Don't create a `PKPushRegistry` of your own on iOS. A second registry takes the VoIP pushes, and those calls never ring, with no error.
+
-## Architecture Overview
+## Step 2: Initialize
-The VoIP implementation consists of platform-specific components working together with your Flutter app:
+Initialize `cometchat_voip` after the Calls SDK, as early as possible at app start, and before your app handles any call push. Build its configuration from the same `CallAppSettings` you give the Calls SDK, so the app ID and region match.
-```mermaid
-flowchart LR
- subgraph "Flutter App"
- A[Dart Call Logic]
- B[Method Channel]
- end
-
- subgraph "Android Native"
- C[FirebaseMessagingService]
- D[ConnectionService]
- end
-
- subgraph "iOS Native"
- E[PushKit / APNs]
- F[CallKit]
- end
-
- subgraph "CometChat"
- G[Chat SDK]
- H[Calls SDK]
- end
-
- B <--> C
- B <--> E
- C -->|Call Push| D
- E -->|VoIP Push| F
- D -->|Accept| G
- F -->|Accept| G
- G -->|Session| H
- H --> A
+```dart
+CallAppSettings callAppSettings = (CallAppSettingBuilder()
+ ..appId = "APP_ID"
+ ..region = "REGION")
+ .build();
+
+// After CometChatCalls.init(callAppSettings, ...) succeeds:
+await CometChatVoip.init(
+ CometChatVoipConfig.fromCallAppSettings(
+ callAppSettings,
+ providerId: "PROVIDER_ID", // the FCM provider on Android, the APNs provider on iOS
+ ),
+);
```
-| Component | Platform | Purpose |
-|-----------|----------|---------|
-| `FirebaseMessagingService` | Android | Receives push notifications for incoming calls when app is in background |
-| `ConnectionService` | Android | Android Telecom framework integration — manages call state with the system |
-| `PushKit` / APNs | iOS | Receives VoIP push notifications to wake the app |
-| `CallKit` | iOS | Displays native iOS call UI on lock screen and in-app |
-| Method Channel | Flutter | Bridges native call events to Dart code |
+| Option | Default | What it does |
+|--------|---------|--------------|
+| `providerId` | none | The dashboard push provider the token is registered under |
+| `callMode` | `CometChatCallMode.auto` | Who rings the call and draws the ring screen. See [Receive a call](#receive-a-call) |
+| `defaultRingTimeout` | 45 seconds | How long a call rings when the push doesn't say |
+| `showMissedCallNotification` | `true` | Show a notification for a call that rang out or was cancelled |
+| `requestCallPermissions` | `true` | Ask for the microphone (and the camera for video) when the user answers |
+| `requestNotificationPermission` | `true` | Ask for notification permission during `init` |
+| `autoRegisterPushToken` | `true` | Register the push token again on every Calls SDK login. On Android, hand over the FCM token once first (Step 4) |
+| `dropExpiredCalls` | `true` | Don't ring a push that arrives after its call stopped ringing |
+| `callKit` | `CometChatCallKitOptions()` | iOS CallKit options: icon, ringtone, whether calls appear in Recents |
+| `acceptOnAnswer` | `true` | Also mark the call answered with CometChat (status `ongoing`) when the user answers, the way the Chat SDK's `acceptCall` did. Keep it on while any caller is still on legacy ringing. See [Rolling out](/calls/flutter/migrating-from-legacy-ringing#rolling-out) |
----
-
-## Android: FCM Integration
+## Step 3: Hand call pushes to cometchat_voip
-On Android, incoming calls are delivered via Firebase Cloud Messaging (FCM). When a call notification arrives while the app is in the background, you use Android's `ConnectionService` to show the system call UI.
+On Android, pass the data of every push your app receives to `handlePushNotification`, from both your foreground and your background message handlers. It returns `true` for a call push, which `cometchat_voip` then rings. Other pushes, such as chat messages, are left to your own handling.
-### Step 1: Add FCM Dependencies
+```dart
+// Foreground: register with FirebaseMessaging.onMessage.listen(...)
+Future onForegroundMessage(RemoteMessage message) async {
+ bool isCallPush = await CometChatVoip.handlePushNotification(
+ message.data,
+ sentTime: message.sentTime,
+ );
+ if (!isCallPush) {
+ // Not a call: handle the push as you do today.
+ }
+}
-Add Firebase Messaging to your Android `build.gradle`:
+// Background and killed: register with FirebaseMessaging.onBackgroundMessage(...)
+@pragma('vm:entry-point')
+Future onBackgroundMessage(RemoteMessage message) async {
+ // Runs in a separate isolate when the app is in the background or killed.
+ // Your app's startup code hasn't run here, so initialize the Calls SDK and
+ // cometchat_voip (Step 2) before handing over the push.
+ if (!CometChatVoip.isInitialized) {
+ // CometChatCalls.init(...), then CometChatVoip.init(...)
+ }
-```groovy
-dependencies {
- implementation 'com.google.firebase:firebase-messaging:23.4.0'
+ await CometChatVoip.handlePushNotification(
+ message.data,
+ sentTime: message.sentTime,
+ );
}
```
-### Step 2: Create FirebaseMessagingService
+
+**Pass `sentTime` and hand over every push.** Firebase can hold a push while the phone dozes; without the sent time a push delivered late rings for a call that's already over. Don't skip call pushes when the app is in the foreground: `cometchat_voip` rings them in every app state.
+
-In your Android native code (`android/app/src/main/`), create a service to handle incoming call push notifications:
+
+**Send call pushes at high priority.** Android only lets a background app stay alive through the answer when a high-priority push woke it.
+
-```kotlin
-class CallFirebaseMessagingService : FirebaseMessagingService() {
+On iOS there's nothing to hand over: VoIP pushes go straight to `cometchat_voip`.
- override fun onMessageReceived(remoteMessage: RemoteMessage) {
- val data = remoteMessage.data
+## Step 4: Register the push token
- // Check if this is a call notification
- if (data["type"] == "call") {
- handleIncomingCall(data)
- }
- }
+| Method | Platform | When |
+|--------|----------|------|
+| `registerPushToken(token:)` | Android, iOS | After login, with the FCM token on Android and the APNs VoIP (PushKit) token on iOS |
+| `handleTokenRefresh(token)` | Android, iOS | When FCM (Android) or PushKit (iOS) issues a new token |
+| `unregisterPushToken()` | Android, iOS | Before logout, while the user's session still works |
- private fun handleIncomingCall(data: Map) {
- val sessionId = data["sessionId"] ?: return
- val callerName = data["senderName"] ?: "Unknown"
- val callerUid = data["senderUid"] ?: return
- val callType = data["callType"] ?: "video"
+On iOS, `cometchat_voip` owns the `PKPushRegistry` in both `auto` modes (`autoCustomScreen` rings in CallKit on iOS, so it behaves as `auto` there): it obtains the PushKit VoIP token during `init` and registers it as soon as a user is logged in to the Calls SDK — at `init` if one already is, otherwise at the next login — so there's nothing for you to pass. Pass the token yourself only in `manual` mode, where your app owns the registry.
- // If app is in foreground, let CometChat's CallListener handle it
- if (isAppInForeground()) return
+```dart
+// Android: after login
+String? fcmToken = await FirebaseMessaging.instance.getToken();
+if (fcmToken != null) {
+ await CometChatVoip.registerPushToken(token: fcmToken);
+}
- // App is in background/killed — show system call UI
- // Use ConnectionService or a high-priority notification
- showIncomingCallNotification(sessionId, callerName, callerUid, callType)
- }
+// Android: when the token changes
+FirebaseMessaging.instance.onTokenRefresh.listen(CometChatVoip.handleTokenRefresh);
- override fun onNewToken(token: String) {
- // Register the FCM token with CometChat
- // Bridge to Flutter via method channel or shared preferences
- }
-}
+// Before logout
+await CometChatVoip.unregisterPushToken();
```
-### Step 3: Register in AndroidManifest
-
-```xml
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-```
+## Step 5: Permissions and ring readiness
-
-For the full Android `ConnectionService` implementation including `PhoneAccount` registration, `CallConnection`, and `CallNotificationManager`, refer to the [Android VoIP Calling](/calls/android/voip-calling) documentation. The native Android code is identical whether used in a native Android app or a Flutter app's Android module.
-
+**Microphone and camera.** `cometchat_voip` asks at the moment of answering (with `requestCallPermissions` on, the default): the ring stops, the system prompt appears, and the call joins whatever the user chose.
----
+**Ring readiness.** Some device settings silence a call with no error anywhere. Check once after login and tell the user:
-## iOS: APNs + CallKit Integration
-
-On iOS, incoming VoIP calls are delivered via Apple Push Notification service (APNs) with VoIP push certificates. CallKit provides the native iOS call UI.
-
-### Step 1: Enable Capabilities
-
-In Xcode, enable the following capabilities for your iOS target:
-- **Push Notifications**
-- **Background Modes** → Voice over IP, Remote notifications
-
-### Step 2: Configure PushKit and CallKit
-
-In your iOS native code (`ios/Runner/`), set up PushKit to receive VoIP pushes and CallKit to display the call UI:
-
-```swift
-import PushKit
-import CallKit
-import Flutter
-
-class CallKitManager: NSObject, PKPushRegistryDelegate, CXProviderDelegate {
- static let shared = CallKitManager()
-
- private let provider: CXProvider
- private let voipRegistry = PKPushRegistry(queue: .main)
- private var methodChannel: FlutterMethodChannel?
-
- override init() {
- let config = CXProviderConfiguration()
- config.supportsVideo = true
- config.maximumCallsPerGroup = 1
- config.supportedHandleTypes = [.generic]
- provider = CXProvider(configuration: config)
-
- super.init()
- provider.setDelegate(self, queue: nil)
- voipRegistry.delegate = self
- voipRegistry.desiredPushTypes = [.voIP]
- }
-
- func setMethodChannel(_ channel: FlutterMethodChannel) {
- methodChannel = channel
- }
-
- // MARK: - PKPushRegistryDelegate
-
- func pushRegistry(
- _ registry: PKPushRegistry,
- didUpdate pushCredentials: PKPushCredentials,
- for type: PKPushType
- ) {
- let token = pushCredentials.token
- .map { String(format: "%02x", $0) }
- .joined()
- // Register VoIP token with CometChat
- methodChannel?.invokeMethod("onVoIPToken", arguments: token)
- }
-
- func pushRegistry(
- _ registry: PKPushRegistry,
- didReceiveIncomingPushWith payload: PKPushPayload,
- for type: PKPushType,
- completion: @escaping () -> Void
- ) {
- guard type == .voIP else { completion(); return }
-
- let data = payload.dictionaryPayload
- let sessionId = data["sessionId"] as? String ?? ""
- let callerName = data["senderName"] as? String ?? "Unknown"
- let callType = data["callType"] as? String ?? "video"
- let hasVideo = callType == "video"
-
- // Report incoming call to CallKit
- let update = CXCallUpdate()
- update.remoteHandle = CXHandle(type: .generic, value: sessionId)
- update.localizedCallerName = callerName
- update.hasVideo = hasVideo
-
- let uuid = UUID()
- provider.reportNewIncomingCall(with: uuid, update: update) { error in
- if let error = error {
- print("Failed to report call: \(error)")
- }
- completion()
- }
- }
-
- // MARK: - CXProviderDelegate
-
- func provider(_ provider: CXProvider, perform action: CXAnswerCallAction) {
- // User tapped Accept — bridge to Flutter
- methodChannel?.invokeMethod("onCallAccepted", arguments: nil)
- action.fulfill()
- }
-
- func provider(_ provider: CXProvider, perform action: CXEndCallAction) {
- // User tapped Decline or End — bridge to Flutter
- methodChannel?.invokeMethod("onCallDeclined", arguments: nil)
- action.fulfill()
- }
-
- func providerDidReset(_ provider: CXProvider) {
- // Provider was reset — clean up
- }
+```dart
+CometChatRingReadiness readiness = await CometChatVoip.checkRingReadiness();
+if (readiness != CometChatRingReadiness.ready) {
+ // Explain why to the user, then open the right settings page:
+ await CometChatVoip.openSettings(readiness);
}
```
-### Step 3: Bridge to Flutter
-
-Set up a method channel in your `AppDelegate` to communicate between native iOS code and Flutter:
-
-```swift
-import Flutter
-
-@UIApplicationMain
-@objc class AppDelegate: FlutterAppDelegate {
- override func application(
- _ application: UIApplication,
- didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?
- ) -> Bool {
- let controller = window?.rootViewController as! FlutterViewController
- let channel = FlutterMethodChannel(
- name: "com.yourapp/calls",
- binaryMessenger: controller.binaryMessenger
- )
-
- CallKitManager.shared.setMethodChannel(channel)
-
- channel.setMethodCallHandler { (call, result) in
- switch call.method {
- case "endCall":
- // End CallKit call from Flutter side
- result(nil)
- default:
- result(FlutterMethodNotImplemented)
- }
- }
-
- GeneratedPluginRegistrant.register(with: self)
- return super.application(application, didFinishLaunchingWithOptions: launchOptions)
- }
-}
-```
+| Result | Meaning |
+|--------|---------|
+| `ready` | Nothing in the way |
+| `notificationsOff` | The app can't show notifications |
+| `channelMuted` | The user muted the incoming call notifications (Android) |
+| `fullScreenIntentDenied` | Full-screen alerts aren't allowed (Android 14 and later): no ring screen on a locked phone, only the notification |
+| `doNotDisturb` | Do Not Disturb is blocking calls |
+| `microphoneDenied` | The user refused the microphone: calls still ring but join muted |
+| `backgroundModeMissing` | The Voice over IP background mode is missing (iOS) |
+| `voipEntitlementMissing` | The push notifications entitlement is missing (iOS) |
----
+`CometChatVoip.verifyPushSetup()` checks the rest of the setup and reports `ok`, `notInitialized`, `notLoggedIn`, `tokenNotRegistered`, `tokenRegistrationFailed`, `notificationsBlocked` or `ringNotReady`.
-## Flutter: Handle Call Events
+## Receive a call
-On the Dart side, set up a method channel to receive call events from native code and manage the call lifecycle:
+How an incoming call rings depends on `callMode`:
-```dart
-import 'package:flutter/services.dart';
-import 'package:cometchat_calls_sdk/cometchat_calls_sdk.dart';
+| | Auto | Auto + your own ring screen | Manual |
+|---|---|---|---|
+| `callMode` | `CometChatCallMode.auto` (default) | `CometChatCallMode.autoCustomScreen` | `CometChatCallMode.manual` |
+| Ringing and the ring screen | `cometchat_voip` | `cometchat_voip` rings; the ring screen is your Flutter screen | Your app |
+| Platforms | Android, iOS | Android (iOS behaves as Auto) | Android, iOS (on iOS your app owns the `PKPushRegistry` and forwards pushes with `handlePushNotification`) |
-class VoIPCallManager {
- static const _channel = MethodChannel('com.yourapp/calls');
+In every mode, when the user answers, your app joins the call (see [Answer a call](/calls/flutter/ringing#answer-a-call)).
- static void initialize() {
- _channel.setMethodCallHandler(_handleMethodCall);
- }
+### Auto
- static Future _handleMethodCall(MethodCall call) async {
- switch (call.method) {
- case 'onCallAccepted':
- // User accepted the call from native UI
- await _acceptAndJoinCall();
- break;
- case 'onCallDeclined':
- // User declined the call from native UI
- await _rejectCall();
- break;
- case 'onVoIPToken':
- // Register VoIP token with CometChat (iOS)
- final token = call.arguments as String;
- _registerVoIPToken(token);
- break;
- }
- }
+`cometchat_voip` does all of the ringing, including its own ring screen. Listen for an answer, and for a call that stops ringing:
- static Future _acceptAndJoinCall() async {
- // Accept via CometChat Chat SDK, then join via Calls SDK
- // Navigate to call screen
- }
+```dart
+CometChatVoip.onCallAnswered.listen((CometChatVoipCall call) {
+ // The user answered: show your call screen and join call.sessionId.
+});
+
+CometChatVoip.onCallEnded.listen((CometChatCallEnded ended) {
+ // The call stopped ringing without an answer on this device.
+ // ended.reason says why; ended.reason.isMissed is true for a missed call.
+});
+```
- static Future _rejectCall() async {
- // Reject via CometChat Chat SDK
- }
+### Your own ring screen
- static void _registerVoIPToken(String token) {
- // Register token with CometChat push notification service
- }
-}
+With `callMode: CometChatCallMode.autoCustomScreen`, the full-screen alert and a tap on the notification open your Flutter ring screen instead of `cometchat_voip`'s. The notification, its ringtone, its Answer and Decline buttons and the ring timeout are still `cometchat_voip`'s. Android only.
+
+```dart
+CometChatVoip.onRingScreenRequested.listen((CometChatRingRequest request) {
+ // Show your ring screen for request.call.
+ // If request.answerEndsActiveCall is true, tell the user that answering ends their current call.
+ // Answer button: CometChatVoip.answer(request.call.sessionId)
+ // Decline button: CometChatVoip.decline(request.call.sessionId)
+});
+
+CometChatVoip.onCallAnswered.listen((CometChatVoipCall call) {
+ // Close your ring screen, then show your call screen and join.
+});
+
+CometChatVoip.onCallEnded.listen((CometChatCallEnded ended) {
+ // The call stopped ringing: close your ring screen.
+});
```
----
+`answer` asks for the microphone (and the camera for a video call) by itself. A call that arrives while your app is in the foreground shows a notification instead of the full-screen alert; to show your ring screen then too, also listen to `CometChatVoip.onCallRinging`.
-## Register FCM Token
+### Manual
-Register the FCM token with CometChat to receive push notifications on Android:
+With `callMode: CometChatCallMode.manual`, `cometchat_voip` only reads the push: it drops duplicates, expired calls, calls this user placed and calls meant for someone else, and passes every new call to `onCallRinging`. Every other step is yours. Android only.
```dart
-import 'package:firebase_messaging/firebase_messaging.dart';
-
-Future registerPushToken() async {
- final token = await FirebaseMessaging.instance.getToken();
- if (token != null) {
- // Register with CometChat Chat SDK
- // CometChat.registerTokenForPushNotification(token, ...)
- }
-}
+CometChatVoip.onCallRinging.listen((CometChatVoipCall call) async {
+ await CometChatVoip.ringSurfaces.showIncomingNotification(call); // ring, keep alive, stop at call.expiresAt
+ await CometChatVoip.ringSurfaces.verify(call); // the caller sees "Ringing…"
+});
+
+CometChatVoip.onCallEnded.listen((CometChatCallEnded ended) {
+ CometChatVoip.ringSurfaces.stopRinging(ended.call.sessionId);
+});
```
----
+Report what the user chose with `CometChatVoip.accept(sessionId)` (fires `onCallAnswered`) or `CometChatVoip.decline(sessionId)`. For your own ring UI without `showIncomingNotification`, keep the app alive with `ringSurfaces.keepAlive(call)`, stop at `call.expiresAt` and show a missed call with `ringSurfaces.showMissedCall(call)`. A second call while one is ringing or live is yours to refuse with `decline(sessionId, busy: true)`. In Manual mode, `decline` retries a few times and then throws; it doesn't survive the app being killed.
-## Initiate Outgoing Calls
+
+In Manual mode with nothing built, the call is silently lost: no notification, no ringtone, no screen, nothing sent to the caller, who rings out to `unanswered`. Use Manual only if you are building the whole ring yourself.
+
-To make an outgoing VoIP call, use the CometChat Chat SDK to initiate the call, then join the session:
+## Incoming call events
-```dart
-Future initiateVoIPCall({
- required String receiverId,
- required String callType,
-}) async {
- // Initiate call via CometChat Chat SDK
- // This sends a push notification to the receiver
- // On success, navigate to call screen and join session
-
- final sessionSettings = (SessionSettingsBuilder()
- ..setType(callType == "video" ? SessionType.video : SessionType.audio))
- .build();
-
- CometChatCalls.joinSession(
- sessionId: sessionId,
- sessionSettings: sessionSettings,
- onSuccess: (Widget? widget) {
- // Navigate to call screen with the widget
- },
- onError: (CometChatCallsException e) {
- debugPrint("Failed to join: ${e.message}");
- },
- );
-}
-```
+Listen to `cometchat_voip`'s events: `onCallAnswered` to join, `onCallEnded` to close. The Calls SDK's `IncomingCallListener` (`CometChatCalls.addIncomingCallListener`) reports the same rings and ends if you prefer it, and the caller uses it to learn that a call was declined.
----
+## How a ring ends
-## Complete Flow Diagram
+Every way out is handled by `cometchat_voip` and needs no code in your app.
-This diagram shows the complete flow for both incoming and outgoing VoIP calls:
+| What happened | On the callee's phone | What the caller learns |
+|---------------|-----------------------|------------------------|
+| The user answers | The ring stops; `onCallAnswered` fires | `onParticipantJoined` |
+| The user declines | The ring stops; `rejected` is sent and retried until delivered | `onIncomingCallEnded`, normalising to `declined` |
+| The user declines while in another call | The same, with `busy` | `onIncomingCallEnded`, also normalising to `declined` |
+| The caller cancels | The ring stops; missed call | Nothing; the caller ended it |
+| Nobody answers a 1:1 call | At the ring time (45 seconds by default) the ring stops, a missed call is shown and `unanswered` is sent | `onIncomingCallEnded`, normalising to `timedOut` |
+| Answered on another device | The ring stops, no missed call | `onParticipantJoined` |
+| Nobody answers a group call, or the user declines it | The ring stops here only; nothing is sent | Nothing; a group ring reports nothing back |
-```mermaid
-sequenceDiagram
- participant Caller
- participant CallerApp
- participant CometChat
- participant PushService
- participant ReceiverApp
- participant NativeUI
- participant Receiver
-
- Note over Caller,Receiver: Outgoing Call Flow
- Caller->>CallerApp: Tap call button
- CallerApp->>CometChat: initiateCall()
- CometChat-->>CallerApp: onSuccess(sessionId)
- CallerApp->>CallerApp: Navigate to call screen
- CallerApp->>CometChat: joinSession()
- CometChat->>PushService: Send push (FCM / APNs)
-
- Note over Caller,Receiver: Incoming Call Flow
- PushService->>ReceiverApp: Push notification received
-
- alt Android (FCM)
- ReceiverApp->>NativeUI: ConnectionService / Notification
- else iOS (APNs + CallKit)
- ReceiverApp->>NativeUI: CallKit native call UI
- end
-
- NativeUI->>Receiver: Show incoming call
- Receiver->>NativeUI: Accept / Reject
-
- alt Call Accepted
- NativeUI->>ReceiverApp: Bridge to Flutter
- ReceiverApp->>CometChat: acceptCall()
- ReceiverApp->>CometChat: joinSession()
- Note over CallerApp,ReceiverApp: Call Connected
- else Call Rejected
- NativeUI->>ReceiverApp: Bridge to Flutter
- ReceiverApp->>CometChat: rejectCall()
- end
-```
+The reason `onIncomingCallEnded` hands the caller is the native end reason, worded differently on each platform. Pass it through `CometChatCallEndReason.fromWire(reason)` and compare the result, as [Listen for calls ending](/calls/flutter/ringing#listen-for-calls-ending) describes; `busy` normalises to `declined`, so the caller can't tell a busy decline from a plain one.
----
+## A call while in a call
-## Troubleshooting
+Call waiting, the way the phone app does it. "In a call" means the Calls SDK is joined to a session.
-| Issue | Solution |
-|-------|----------|
-| Calls not received in background (Android) | Verify [FCM configuration](/notifications/android-push-notifications) and ensure high-priority notifications are enabled |
-| Calls not received in background (iOS) | Ensure VoIP push certificate is configured and PushKit is registered |
-| CallKit UI not showing (iOS) | Verify Background Modes capability includes "Voice over IP" |
-| System call UI not showing (Android) | Ensure PhoneAccount is registered and enabled in system settings |
-| Audio routing issues | Ensure `audioModeIsVoip` is set on the Android Connection |
-| Method channel not responding | Verify channel name matches between native and Dart code |
+| Situation | Android | iOS |
+|-----------|---------|-----|
+| A call while in a call | Rings over the live call; the ring adds "Answering ends your current call". The live call is untouched | CallKit call waiting |
+| Answer | Ends the live call, then `onCallAnswered` fires for the new call | The same, through CallKit's **End & Accept** |
+| Decline | `busy` to the caller; the live call continues | The same |
+| No action | `unanswered` to the caller, missed call; the live call continues | The same |
+| A call while another one is already ringing here | Declined as `busy` without ringing, missed call. The first keeps ringing | Shown by CallKit |
+| A call while the user's own outgoing call is ringing | Declined as `busy` without ringing, missed call | Shown by CallKit |
----
+In Manual mode, your app decides how to handle these calls.
-## Related Documentation
+## Missed calls
+
+With `showMissedCallNotification` on (the default), a missed call is shown when nobody answers in the ring time, or when the caller cancels before an answer. It isn't shown when the call is answered here or on another device, or declined here. On `onCallEnded`, `ended.reason.isMissed` is `true` for these calls.
+
+## When the app is killed or the phone is locked
+
+1. The call push wakes the app: a high-priority FCM push runs your background message handler on Android; a VoIP push wakes `cometchat_voip` on iOS.
+2. On Android, the handler initializes the Calls SDK and `cometchat_voip` and hands over the push (Step 3).
+3. `cometchat_voip` rings: on Android on a foreground service that keeps the app alive through the answer, with the ring screen over the lock screen; on iOS through CallKit.
+4. On **Answer**, it asks for the microphone (and camera), opens the app, and `onCallAnswered` fires as soon as your code listens. Join the call there.
+5. A decline or a timeout is sent from a queue, so it goes out even if the app is killed right after the tap.
+
+Initialize the Calls SDK and `cometchat_voip`, and log the user in, at app start from stored credentials, not only after a screen opens, so an answered call finds a ready SDK.
+
+## Logout
+
+Before logging the user out, call `CometChatVoip.unregisterPushToken()` while the session that owns the token still works, then log out of the Calls SDK with `CometChatCalls.logout`.
+
+## Troubleshooting
-- [Ringing](/calls/flutter/ringing) - Basic in-app call signaling
-- [Actions](/calls/flutter/actions) - Control call functionality
-- [Events](/calls/flutter/events) - Listen for call state changes
-- [Background Handling](/calls/flutter/background-handling) - Keep active calls alive in background
+| Symptom | Check |
+|---------|-------|
+| The phone doesn't ring when the app is killed (Android) | The background handler is registered, initializes the Calls SDK and `cometchat_voip`, and calls `handlePushNotification` |
+| The call rings, but the app is killed before the answer (Android) | Call pushes are sent at normal priority; send them at high priority |
+| The phone doesn't ring at all | `CometChatVoip.verifyPushSetup()` and `CometChatVoip.checkRingReadiness()` say what's missing |
+| iOS never rings | Push Notifications and the Voice over IP background mode are on, the APNs provider is set up as in Getting Started, and no other `PKPushRegistry` exists |
+| The caller never sees "Ringing…" | The callee's app has `cometchat_voip` initialized and a push token registered, and the caller doesn't join right after `initiateCall` |
+| The caller doesn't learn about a decline | The caller's app has `cometchat_voip` initialized and a push token registered |
+| An audio call rings as video | `InitiateCallOptions.type` isn't set; it defaults to video |
+| Two ringtones or two ring screens | Earlier ringing code is still in the app: your own CallKit or `flutter_callkit_incoming` handling, or a Chat SDK `CallListener` that shows an incoming call screen. Remove it |
+| Calls still ring after sign-out | `unregisterPushToken()` wasn't called before logout |
+
+## Related
+
+- [Ringing](/calls/flutter/ringing): start, answer, decline, cancel and end calls with the Calls SDK.
+- [Migrating from Legacy Ringing](/calls/flutter/migrating-from-legacy-ringing).
+- [Background Handling](/calls/flutter/background-handling): keep a joined call running when the app goes to the background.
diff --git a/docs.json b/docs.json
index 96952f214..34f81b6d9 100644
--- a/docs.json
+++ b/docs.json
@@ -2172,7 +2172,8 @@
"ui-kit/flutter/getting-started",
"ui-kit/flutter/flutter-conversation",
"ui-kit/flutter/flutter-one-to-one-chat",
- "ui-kit/flutter/flutter-tab-based-chat"
+ "ui-kit/flutter/flutter-tab-based-chat",
+ "ui-kit/flutter/calling-integration"
]
},
{
@@ -5833,7 +5834,8 @@
{
"group": "Migration Guide",
"pages": [
- "calls/flutter/migration-guide-v5"
+ "calls/flutter/migration-guide-v5",
+ "calls/flutter/migrating-from-legacy-ringing"
]
},
{
@@ -6464,6 +6466,7 @@
"tab": "Push",
"pages": [
"notifications/push-overview",
+ "notifications/push-getting-started",
{
"group": "Getting Started",
"pages": [
@@ -6471,8 +6474,7 @@
"notifications/ios-push-notifications-sdk",
"notifications/ios-apns-push-notifications",
"notifications/ios-fcm-push-notifications",
- "notifications/flutter-push-notifications-android",
- "notifications/flutter-push-notifications-ios",
+ "notifications/flutter-push-notifications",
"notifications/react-native-push-notifications-android",
"notifications/react-native-push-notifications-ios",
"notifications/web-push-notifications"
@@ -7093,7 +7095,7 @@
},
{
"source": "/extensions/flutter-push-notifications",
- "destination": "/notifications/flutter-push-notifications-android"
+ "destination": "/notifications/flutter-push-notifications"
},
{
"source": "/extensions/react-native-push-notifications",
@@ -8479,10 +8481,6 @@
"destination": "https://assets.cometchat.io/legacy-docs/ai-chatbots/ai-bots/bots.html",
"source": "/ai-chatbots/ai-bots/bots"
},
- {
- "destination": "https://assets.cometchat.io/legacy-docs/notifications/push-notifications-extension-legacy.html",
- "source": "/notifications/flutter-push-notifications"
- },
{
"destination": "https://assets.cometchat.io/legacy-docs/sdk/ionic-legacy/additional-message-filtering.html",
"source": "/sdk/ionic-legacy/additional-message-filtering"
@@ -10570,6 +10568,14 @@
{
"source": "/chat-builder/react-native/builder-dir-structure",
"destination": "/ui-kit/react-native/overview"
+ },
+ {
+ "source": "/notifications/flutter-push-notifications-android",
+ "destination": "/notifications/flutter-push-notifications"
+ },
+ {
+ "source": "/notifications/flutter-push-notifications-ios",
+ "destination": "/notifications/flutter-push-notifications"
}
],
"seo": {
diff --git a/notifications.mdx b/notifications.mdx
index 1422e862a..66a8f670c 100644
--- a/notifications.mdx
+++ b/notifications.mdx
@@ -71,8 +71,7 @@ canonical: "https://cometchat.com/docs"
} href="/notifications/ios-apns-push-notifications" horizontal />
} href="/notifications/ios-fcm-push-notifications" horizontal />
- } href="/notifications/flutter-push-notifications-android" horizontal />
- } href="/notifications/flutter-push-notifications-ios" horizontal />
+ } href="/notifications/flutter-push-notifications" horizontal />
} href="/notifications/react-native-push-notifications-android" horizontal />
} href="/notifications/react-native-push-notifications-ios" horizontal />
} href="/notifications/web-push-notifications" horizontal />
diff --git a/notifications/flutter-push-notifications-android.mdx b/notifications/flutter-push-notifications-android.mdx
deleted file mode 100644
index 622630316..000000000
--- a/notifications/flutter-push-notifications-android.mdx
+++ /dev/null
@@ -1,360 +0,0 @@
----
-title: "Flutter Push Notifications (Android)"
-description: "CometChat push notifications in Flutter apps on Android using Firebase Cloud Messaging (FCM)."
----
-
-
- Reference implementation of Flutter UI Kit, FCM and Push Notification Setup.
-
-
-## What this guide covers
-
-- CometChat dashboard setup (enable push, add FCM provider) with screenshots.
-- Firebase/FCM + Flutter wiring (credentials, pubspec, Firebase init).
-- Copying the sample notification stack and aligning package IDs/provider IDs.
-- Native Android glue (manifest, MethodChannel, lock-screen call activity/receiver).
-- Token registration, notification/call handling, navigation, testing, and troubleshooting.
-- App icon badge count and grouped notifications using `unreadMessageCount` from the CometChat push payload.
-
-{/* ## What you need first
-
-- Firebase project with an Android app configured (package name matches your `applicationId`) and Cloud Messaging enabled; `google-services.json` inside `android/app`.
-- CometChat app credentials (App ID, Region, Auth Key) plus Push Notifications enabled with an **FCM provider** for Flutter Android.
-- Flutter 3.24+ / Dart 3+, the latest CometChat UI Kit (`cometchat_chat_uikit`) and Calls UI Kit (`cometchat_calls_uikit`) packages.
-- Physical Android device for testing—full-screen call notifications and background delivery are unreliable on emulators. */}
-
-## How FCM + CometChat work together
-
-- **FCM’s role:** Issues the Android registration token and delivers the push payload to the device.
-- **CometChat’s role:** The FCM provider you add in the CometChat dashboard stores your Firebase service account. When `PNRegistry.registerPNService(token, true, false)` runs after login, CometChat binds that token to the logged-in user and sends pushes to FCM on your behalf.
-- **Flow:** Permission (Android 13+ `POST_NOTIFICATIONS`) → Firebase returns FCM token → after `CometChatUIKit.login`, register with `PNRegistry` (uses `AppCredentials.fcmProviderId`) → CometChat sends to FCM → FCM delivers to the device → `NotificationLaunchHandler` / `VoipNotificationHandler` route taps and call actions.
-
-## 1. Enable push and add providers (CometChat Dashboard)
-
-1. Go to **Notifications → Settings** and enable **Push Notifications**.
-
-
-
-
-
-2. Click **Add Credentials**, choose **FCM**, upload the Firebase service account JSON (Firebase → Project settings → Service accounts → Generate new private key), and copy the Provider ID.
-
-
-
-
-
-Keep the provider ID—you’ll use it in `AppCredentials.fcmProviderId`.
-
-## 2. Prepare Firebase and credentials
-
-### 2.1 Firebase Console
-
-1. Register your Android package name (the same as `applicationId` in `android/app/build.gradle`) and download `google-services.json` into `android/app`.
-2. Enable Cloud Messaging and copy the Server key if you want to send test messages manually.
-
-
-
-
-
-### 2.3 Local configuration file
-
-Update [`lib/app_credentials.dart`](https://github.com/cometchat/cometchat-uikit-flutter/blob/v5/sample_app_push_notifications/lib/app_credentials.dart) so it exposes your credentials and provider IDs:
-
-```dart lines
-class AppCredentials {
- static String _appId = "YOUR_APP_ID";
- static String _authKey = "YOUR_AUTH_KEY";
- static String _region = "YOUR_REGION";
- static String _fcmProviderId = "FCM-PROVIDER-ID";
-}
-```
-
-The sample persists these values to `SharedPreferences`; `saveAppSettingsToNative()` passes them to Android so `CallActionReceiver` can reject calls even if Flutter is not running.
-
-## 3. Bring the notification stack into Flutter
-
-### 3.1 Copy [`lib/notifications`](https://github.com/cometchat/cometchat-uikit-flutter/tree/v5/sample_app_push_notifications/lib/notifications)
-
-- Clone or download the sample once.
-- Copy the entire `lib/notifications` directory (models, Android/iOS services, helpers) into your app.
-- Update the import prefixes (for example replace `package:sample_app_push_notifications/...` with your own package name). Keeping the same folder names avoids manual refactors later.
-
-### 3.2 Wire the entry points
-
-**[`lib/main.dart`](https://github.com/cometchat/cometchat-uikit-flutter/blob/v5/sample_app_push_notifications/lib/main.dart)**
-
-- Initialize `SharedPreferencesClass`, `FlutterLocalNotificationsPlugin`, and Firebase before calling `runApp`.
-- Cache `NotificationLaunchHandler.pendingNotificationResponse` when the app is launched from a tapped notification while terminated.
-- Keep the `callMain` entrypoint; `CallActivity` uses it to render the ongoing-call UI over the lock screen.
-
-**[`lib/guard_screen.dart`](https://github.com/cometchat/cometchat-uikit-flutter/blob/v5/sample_app_push_notifications/lib/guard_screen.dart) / [`lib/dashboard.dart`](https://github.com/cometchat/cometchat-uikit-flutter/blob/v5/sample_app_push_notifications/lib/dashboard.dart) (or your first screen after login)**
-
-- Ensure `CometChatUIKit.init()` and `CometChatUIKit.login()` finish before rendering the dashboard.
-- On Android, instantiate `FirebaseService` and call `notificationService.init(context)` once; on iOS, keep `APNSService`.
-- Replay `NotificationLaunchHandler.pendingNotificationResponse` after the widget tree builds so taps from a killed app still navigate to `MessagesScreen`.
-- Forward lifecycle changes to `IncomingCallOverlay` / `BoolSingleton` to hide stale overlays when the app resumes.
-- `VoipNotificationHandler.handleNativeCallIntent(context)` runs after the first frame to act on accept/decline actions that were tapped from the Android notification before Flutter started.
-
-### 3.3 Align dependencies and configuration
-
-Mirror the sample `pubspec.yaml` versions (update as needed when newer releases ship):
-
-```yaml lines
-dependencies:
- firebase_core: ^3.9.0
- firebase_messaging: ^15.1.6
- flutter_local_notifications: ^18.0.0
- flutter_callkit_incoming:
- path: ../sample_app_push_notifications/flutter_callkit_incoming
- cometchat_chat_uikit: ^5.2.5
- cometchat_calls_uikit: ^5.0.11
- permission_handler: ^11.3.1
- shared_preferences: ^2.2.1
-```
-
-Run `flutter pub get`, then `flutterfire configure` if you still need to generate `firebase_options.dart`.
-
-## 4. Configure the native Android layer
-
-### 4.1 Gradle + Firebase
-
-1. Add `google-services.json` to `android/app`.
-2. Ensure `android/app/build.gradle` applies the plugins used in the sample:
-
-```gradle lines
-plugins {
- id "com.android.application"
- id "com.google.gms.google-services"
- id "kotlin-android"
- id "dev.flutter.flutter-gradle-plugin"
-}
-```
-
-Set `applicationId` to your package name and keep `minSdk 24` or higher. `compileSdk 36` / `targetSdk 35` match the sample but can be raised if your project already targets a newer API.
-
-### 4.2 Manifest permissions and components
-
-Use the sample [`AndroidManifest.xml`](https://github.com/cometchat/cometchat-uikit-flutter/blob/v5/sample_app_push_notifications/android/app/src/main/AndroidManifest.xml) as a baseline:
-
-- Permissions for notifications, audio/video, and lock-screen call UI: `POST_NOTIFICATIONS`, `RECORD_AUDIO`, `CAMERA`, `FOREGROUND_SERVICE`, `USE_FULL_SCREEN_INTENT`, `WAKE_LOCK`, `SHOW_WHEN_LOCKED`, `TURN_SCREEN_ON`, and `SYSTEM_ALERT_WINDOW`.
-- `MainActivity` uses `launchMode="singleTask"` with `android:showWhenLocked="true"` / `android:turnScreenOn="true"` so incoming calls can wake the screen.
-- `CallActivity` is a dedicated entrypoint (uses `callMain`) to render the ongoing call over the lock screen and is excluded from recents.
-- `CallActionReceiver` listens to `flutter_callkit_incoming` actions (and mirrored app-specific actions) so Accept/Decline from the native notification reach Flutter.
-- Set `default_notification_icon` meta-data to your icon if you change the launcher asset.
-
-### 4.3 Kotlin bridge for call intents
-
-- [`MainActivity.kt`](https://github.com/cometchat/cometchat-uikit-flutter/blob/v5/sample_app_push_notifications/android/app/src/main/kotlin/com/cometchat/sampleapp/flutter/android/MainActivity.kt) exposes a `MethodChannel("com.cometchat.sampleapp")` that supports:
- - `get_initial_call_intent` – read and clear any call intent extras so `VoipNotificationHandler.handleNativeCallIntent` in Dart can react after Flutter launches.
- - `setupLockScreenForCall` / `restoreLockScreenAfterCall` – temporarily bypass and then restore the lock screen when a call is accepted.
- - `saveAppSettings` – stores your App ID and Region for the broadcast receiver.
-- [`CallActionReceiver.kt`](https://github.com/cometchat/cometchat-uikit-flutter/blob/v5/sample_app_push_notifications/android/app/src/main/kotlin/com/cometchat/sampleapp/flutter/android/CallActionReceiver.kt) wakes the app for Accept/Decline actions. On decline, it can initialize the CometChat SDK headlessly (using the saved App ID/Region) to reject the call as busy even if Flutter is not running.
-- [`CallActivity.kt`](https://github.com/cometchat/cometchat-uikit-flutter/blob/v5/sample_app_push_notifications/android/app/src/main/kotlin/com/cometchat/sampleapp/flutter/android/CallActivity.kt) overrides `getDartEntrypointFunctionName` to `callMain`, letting the ongoing-call UI render in its own activity with lock-screen flags.
-
-If you change the MethodChannel name in Kotlin, update `voipPlatformChannel` inside [`lib/notifications/services/save_settings_to_native.dart`](https://github.com/cometchat/cometchat-uikit-flutter/blob/v5/sample_app_push_notifications/lib/notifications/services/save_settings_to_native.dart) to match.
-
-## 5. Token registration and runtime events
-
-### 5.1 FCM tokens
-
-`FirebaseService.init` requests notification permission, sets the background handler (`firebaseMessagingBackgroundHandler`), and registers tokens:
-
-```dart lines
-final token = await FirebaseMessaging.instance.getToken();
-if (token != null) {
- PNRegistry.registerPNService(token, true, false); // platform: FCM_FLUTTER_ANDROID
-}
-FirebaseMessaging.instance.onTokenRefresh.listen(
- (token) => PNRegistry.registerPNService(token, true, false),
-);
-```
-
-`PNRegistry` pulls the provider ID from `AppCredentials.fcmProviderId`. Call this only after `CometChatUIKit.login` succeeds.
-
-### 5.2 Local notifications and navigation
-
-- `LocalNotificationService.showNotification` renders a high-priority local notification when the incoming CometChat message does not belong to the currently open conversation.
-- `NotificationLaunchHandler.pendingNotificationResponse` caches taps triggered while the app is terminated; `dashboard.dart` replays it after navigation is ready.
-- `LocalNotificationService.handleNotificationTap` fetches the user/group and pushes `MessagesScreen` when a notification is tapped from foreground, background, or terminated states.
-
-### 5.3 Call events (VoIP-like pushes)
-
-- The top-level `firebaseMessagingBackgroundHandler` shows the incoming-call UI by calling `VoipNotificationHandler.displayIncomingCall`, which uses `flutter_callkit_incoming` to render a full-screen notification.
-- `FirebaseService.initializeCallKitListeners` binds `FlutterCallkitIncoming.onEvent` so Accept/Decline/Timeout actions map to `VoipNotificationHandler.acceptVoipCall`, `declineVoipCall`, or `endCall`.
-- `VoipNotificationHandler.handleNativeCallIntent` reads Accept/Decline extras passed from `CallActionReceiver` via the MethodChannel if the user acted before Flutter started.
-- `saveAppSettingsToNative()` runs during `FirebaseService.init` to persist App ID/Region for the native receiver; keep it in place or `CallActionReceiver` cannot initialize CometChat when rejecting a call from the lock screen.
-
-## 6. Badge count and grouped notifications
-
-CometChat's Enhanced Push Notification payload includes an `unreadMessageCount` field (a string) representing the total unread messages across all conversations for the logged-in user. You can use this to set the app icon badge and enrich local notifications.
-
-### 6.1 Enable unread badge count on the CometChat Dashboard
-
-1. Go to **CometChat Dashboard → Notifications → Settings → Preferences → Push Notification Preferences**.
-2. Scroll to the bottom and enable the **Unread Badge Count** toggle.
-
-This ensures CometChat includes the `unreadMessageCount` field in every push payload sent to your app.
-
-### 6.2 Add the `app_badge_plus` dependency
-
-```yaml lines
-dependencies:
- app_badge_plus: ^1.2.6
-```
-
-Run `flutter pub get`.
-
-### 6.3 Expected payload format
-
-CometChat sends FCM data messages with this structure (relevant fields):
-
-```jsonc
-{
- "data": {
- "unreadMessageCount": "5",
- "title": "New Message",
- "alert": "John: Hello!",
- "conversationId": "user_abc123",
- "parentId": "176001", // Optional - parent message ID; sent only for threaded notifications
- "conversationType": "user"
- }
-}
-```
-
-`unreadMessageCount` is a string representing the total unread messages across all conversations for the logged-in user.
-
-### 6.4 Update the app badge from the push payload
-
-Inside your local notification handler (for example `LocalNotificationService.showNotification`), parse `unreadMessageCount` and update the badge:
-
-```dart lines
-import 'package:app_badge_plus/app_badge_plus.dart';
-
-// Inside showNotification, after receiving notificationData:
-final unreadCountRaw = notificationData['unreadMessageCount'];
-int unreadMessageCount = int.tryParse(unreadCountRaw?.toString() ?? '') ?? 0;
-
-if (unreadMessageCount >= 0) {
- try {
- await AppBadgePlus.updateBadge(unreadMessageCount);
- } catch (e) {
- debugPrint('Error updating badge count: $e');
- }
-}
-```
-
-On Android, `app_badge_plus` uses launcher-specific APIs (Samsung, Huawei, etc.) to display a badge number on the app icon. Passing `0` clears the badge.
-
-### 6.5 Show grouped notifications with unread count
-
-Use `conversationId.hashCode` as the notification ID so each new message from the same conversation replaces the previous notification instead of creating a new one. Accumulate message lines per conversation for inbox-style display:
-
-```dart lines
-// Stable notification ID per conversation
-final int notificationId = conversationId.isNotEmpty
- ? conversationId.hashCode
- : DateTime.now().microsecondsSinceEpoch.hashCode;
-
-// Accumulate messages for inbox-style display
-static final Map> _conversationMessages = {};
-_conversationMessages.putIfAbsent(conversationId, () => []);
-_conversationMessages[conversationId]!.add(messageBody);
-
-final messages = _conversationMessages[conversationId]!;
-
-// Choose notification style based on message count
-StyleInformation styleInformation;
-if (messages.length == 1) {
- styleInformation = const DefaultStyleInformation(false, false);
-} else {
- styleInformation = InboxStyleInformation(
- messages,
- contentTitle: title,
- summaryText: unreadMessageCount > 0
- ? '$unreadMessageCount unread ${unreadMessageCount == 1 ? 'message' : 'messages'}'
- : '${messages.length} messages',
- );
-}
-
-final androidDetails = AndroidNotificationDetails(
- notificationChannelId,
- notificationChannelName,
- importance: Importance.max,
- priority: Priority.high,
- icon: 'ic_launcher',
- styleInformation: styleInformation,
- subText: unreadMessageCount > 0
- ? '$unreadMessageCount unread ${unreadMessageCount == 1 ? 'message' : 'messages'}'
- : null,
- number: messages.length > 1 ? messages.length : null,
-);
-```
-
-- `subText` shows the unread count below the notification title on most Android devices.
-- `number` displays a count badge on the notification icon (Samsung One UI).
-- When the user opens a conversation, clear its accumulated messages by removing the key from `_conversationMessages`.
-
-### 6.6 Clear badge and notifications when the app opens
-
-Clear the badge count and dismiss all local notifications when the app launches and every time it resumes from the background. Add `WidgetsBindingObserver` to your root widget and register/remove the observer in `initState()` and `dispose()`:
-
-```dart lines
-Future _clearBadge() async {
- try {
- LocalNotificationService.flutterLocalNotificationsPlugin.cancelAll();
- await AppBadgePlus.updateBadge(0);
- debugPrint("The badge was cleared");
- } catch (e) {
- debugPrint("Error in clearing the badge value $e");
- }
-}
-
-@override
-void initState() {
- super.initState();
- WidgetsBinding.instance.addObserver(this);
- _clearBadge();
-}
-
-@override
-void didChangeAppLifecycleState(AppLifecycleState state) {
- if (state == AppLifecycleState.resumed) {
- _clearBadge();
- }
-}
-
-@override
-void dispose() {
- WidgetsBinding.instance.removeObserver(this);
- super.dispose();
-}
-```
-
-`cancelAll()` removes stale notification banners from the tray so they stay in sync with the badge count.
-
-## 7. Testing checklist
-
-1. Run on a physical Android device. Grant notification, microphone, and camera permissions when prompted (Android 13+ requires `POST_NOTIFICATIONS`).
-2. Send a message from another user:
- - Foreground: a local notification banner shows (unless you are in that chat).
- - Background: FCM notification appears; tapping opens the right conversation.
-3. Force-quit the app, send another message push, tap it, and confirm `NotificationLaunchHandler` launches `MessagesScreen`.
-4. Trigger an incoming CometChat call. Ensure:
- - The full-screen call UI shows caller name/type with Accept/Decline.
- - Accepting on the lock screen notifies Flutter (`handleNativeCallIntent`), starts the call session, and dismisses the native UI when the call ends.
- - Declining from the notification triggers `CallActionReceiver` to reject the call server-side.
-5. Rotate through Wi-Fi/cellular and reinstall the app to confirm token registration works after refresh events.
-
-## 8. Troubleshooting tips
-
-| Symptom | Quick checks |
-| --- | --- |
-| No notifications received | Confirm `google-services.json` is in `android/app`, the package name matches Firebase, and notification permission is granted (Android 13+). |
-| Token registration errors | Double-check `AppCredentials.fcmProviderId` and that `PNRegistry.registerPNService` runs after login. |
-| Call actions never reach Flutter | Ensure `CallActionReceiver` is declared in the manifest, MethodChannel names match `voipPlatformChannel`, and `VoipNotificationHandler.handleNativeCallIntent` is called from `dashboard.dart`. |
-| Full-screen call UI not showing | Verify `USE_FULL_SCREEN_INTENT`, `WAKE_LOCK`, and `SHOW_WHEN_LOCKED` permissions plus `android:showWhenLocked="true"` / `android:turnScreenOn="true"` on `MainActivity` and `CallActivity`. |
-| Tapping notification from killed app does nothing | Keep the `NotificationLaunchHandler` logic in `main.dart` and replay it after the navigator key is ready (post-frame callback). |
diff --git a/notifications/flutter-push-notifications-ios.mdx b/notifications/flutter-push-notifications-ios.mdx
deleted file mode 100644
index 6be293514..000000000
--- a/notifications/flutter-push-notifications-ios.mdx
+++ /dev/null
@@ -1,368 +0,0 @@
----
-title: "Flutter Push Notifications (iOS)"
-description: "CometChat push notifications in Flutter apps on iOS using Apple Push Notification service (APNs)."
----
-
-
- Reference implementation of Flutter UI Kit, APNs and Push Notification Setup.
-
-
-## What this guide covers
-
-- CometChat dashboard setup (enable push, create APNs + optional VoIP/FCM providers) with screenshots.
-- Apple + Firebase setup (entitlements, APNs key, `GoogleService-Info.plist`).
-- Copying the sample notification stack and aligning package IDs/provider IDs.
-- Token registration, navigation from pushes, testing, and troubleshooting.
-- App icon badge count using `unreadMessageCount` from the CometChat push payload.
-
-{/* ## What you need first
-
-- Apple Developer account with Push Notifications, Background Modes, and VoIP entitlements for your bundle ID.
-- Firebase project with an iOS app configured (`GoogleService-Info.plist` inside the Runner target) and Cloud Messaging enabled.
-- CometChat app credentials (App ID, Region, Auth Key) plus Push Notification extension enabled with at least an **APNs provider** (add VoIP and FCM providers if you use them).
-- Flutter 3.24+ / Dart 3+, the latest CometChat UI Kit (`cometchat_chat_uikit`) and Calls UI Kit (`cometchat_calls_uikit`) packages.
-- Physical iPhone or iPad for testing—simulators cannot receive VoIP pushes or present CallKit UI. */}
-
-## How FCM + CometChat work together
-
-- **APNs is primary on iOS:** FCM can be used only as a bridge. Firebase hands the payload to APNs using the APNs key you uploaded in Firebase.
-- **CometChat providers:** The APNs/VoIP/FCM providers you create in the CometChat dashboard hold your Apple/FCM credentials. `CometChatPushRegistry.register(token, isFcm: false, isVoip: ...)` binds the APNs/VoIP tokens; `isFcm: true` uses the FCM provider if you decide to register FCM tokens too.
-- **Flow:** Permission prompt → APNs (and/or FCM) token issued → after `CometChatUIKit.login` succeeds, register tokens with `CometChatPushRegistry` using the matching provider IDs → CometChat sends to FCM/APNs → APNs delivers to the device → Flutter handles taps via `NotificationLaunchHandler` and `APNSService`.
-
-## 1. Enable push and add providers (CometChat Dashboard)
-
-1. Go to **Notifications → Settings** and enable **Push Notifications**.
-
-
-
-
-
-2. Click **Add Credentials**, choose **APNs** (and **APNs VoIP** if you want in-call pushes), upload your `.p8` key or certificate, and copy each Provider ID.
-
-
-
-
-
-3. (Optional) Add an **FCM** provider if you plan to register FCM tokens on iOS.
-
-
-
-
-
-Keep the provider IDs—you’ll set them in `CometChatConfig`.
-
-## 2. Prepare Apple + Firebase credentials
-
-### 2.1 Apple Developer portal
-
-1. Generate an APNs Auth Key (`.p8`) and note the **Key ID** and **Team ID**.
-2. Enable Push Notifications plus Background Modes → *Remote notifications* and *Voice over IP* on the bundle ID.
-3. Create a VoIP Services certificate/key if you want separate credentials.
-
-
-**`.p12` certificates are deprecated.** Apple recommends using `.p8` Auth Keys for push notifications. `.p8` keys never expire and work across all your apps. Migrate to `.p8` if you haven't already.
-
-
-### 2.2 Firebase Console
-
-1. Register the same bundle ID and download `GoogleService-Info.plist` into `ios/Runner`.
-2. Enable Cloud Messaging and upload the APNs key under *Project Settings → Cloud Messaging*.
-
-
-
-
-
-## 3. Local configuration file
-
-Update [`lib/app_credentials.dart`](https://github.com/cometchat/cometchat-uikit-flutter/blob/v5/sample_app_push_notifications/lib/app_credentials.dart) (or your own config file) so it exposes:
-
-```dart lines
-class CometChatConfig {
- static const appId = "YOUR_APP_ID";
- static const region = "YOUR_APP_REGION";
- static const authKey = "YOUR_AUTH_KEY";
- static const fcmProviderId = "FCM-PROVIDER-ID";
- static const apnProviderId = "APNS-PROVIDER-ID";
- static const apnVoipProviderId = "APNS-VOIP-PROVIDER-ID"; // optional but recommended
-}
-```
-
-## 4. Bring the notification stack into Flutter
-
-### 4.1 Copy [`lib/notifications`](https://github.com/cometchat/cometchat-uikit-flutter/tree/v5/sample_app_push_notifications/lib/notifications)
-
-- Clone or download the sample once.
-- Copy the entire [`lib/notifications`](https://github.com/cometchat/cometchat-uikit-flutter/tree/v5/sample_app_push_notifications/lib/notifications) directory (models, Android/iOS services, helpers) into your app.
-- Update the import prefixes (for example replace `package:flutter_application_demo/...` with your own package name). Keeping the same folder names avoids manual refactors later.
-
-### 4.2 Wire the entry points
-
-**[`lib/main.dart`](https://github.com/cometchat/cometchat-uikit-flutter/blob/v5/sample_app_push_notifications/lib/main.dart)**
-
-- Initialize `SharedPreferencesClass`, Firebase, and `FlutterLocalNotificationsPlugin` before calling `runApp`.
-- Store `NotificationLaunchHandler.pendingNotificationResponse` when the app is opened by tapping a notification while terminated.
-- On iOS, call `APNSService.setupNativeCallListener(context)` from `initState` so Flutter reacts when the native CallKit UI changes state.
-
-**[`lib/guard_screen.dart`](https://github.com/cometchat/cometchat-uikit-flutter/blob/v5/sample_app_push_notifications/lib/guard_screen.dart) / [`lib/dashboard.dart`](https://github.com/cometchat/cometchat-uikit-flutter/blob/v5/sample_app_push_notifications/lib/dashboard.dart) (or your first screen after login)**
-
-- Ensure `CometChatUIKit.init()` and `CometChatUIKit.login()` finish before rendering the dashboard.
-- Instantiate `APNSService` (iOS only) and call `apnsService.init(context)` inside `initState`.
-- Register CometChat UI + Calls listeners (`CometChatUIEventListener`, `CometChatCallEventListener`, and `CallListener`) exactly once per session; the sample stores the listener IDs inside `APNSService`.
-- Replay `NotificationLaunchHandler.pendingNotificationResponse` after the widget tree builds so taps from a killed app still navigate to `MessagesScreen`.
-- Forward lifecycle changes to `IncomingCallOverlay` / `BoolSingleton` to hide stale overlays when the app resumes.
-
-### 4.3 Align dependencies and configuration
-
-Mirror the sample `pubspec.yaml` versions (update as needed when newer releases ship):
-
-```yaml lines
-dependencies:
- firebase_core: ^3.0.0
- firebase_messaging: ^15.0.0
- flutter_apns_x: ^2.1.1
- flutter_callkit_incoming: ^2.0.3+3
- flutter_local_notifications: ^16.0.0
- cometchat_chat_uikit: ^5.0.0
- cometchat_calls_uikit: ^5.0.0
- permission_handler: ^11.3.0
-```
-
-Run `flutter pub get`, then install the native pods:
-
-```bash
-cd ios && pod install && cd ..
-```
-
-Then run `flutterfire configure` if you still need to generate `firebase_options.dart`.
-
-## 5. Configure the native iOS layer
-
-### 5.1 Capabilities and Info.plist
-
-1. Open `ios/Runner.xcworkspace` in Xcode.
-2. Under *Signing & Capabilities*, enable **Push Notifications** and **Background Modes** (Remote notifications + Voice over IP).
-3. Add microphone, camera, bluetooth, and notification permission strings to `Info.plist`.
-4. Set the development team that has access to the APNs/VoIP keys you generated earlier.
-
-
-
-
-
-### 5.2 `AppDelegate.swift` bridge
-
-Start from the sample [`ios/Runner/AppDelegate.swift`](https://github.com/cometchat/cometchat-uikit-flutter/blob/v5/sample_app_push_notifications/ios/Runner/AppDelegate.swift) and keep these pieces intact:
-
-- **MethodChannel handshake** – create a channel that both Flutter and Swift know:
-
-```swift lines
-let appInfoChannel = FlutterMethodChannel(
- name: "com.flutter_application_demo/ios",
- binaryMessenger: controller.binaryMessenger
-)
-```
-
-Handle at least `getAppInfo`, `endCall`, `onCallAcceptedFromNative`, and `onCallEndedFromNative`, mirroring the Dart side (`APNSService.setupNativeCallListener`).
-
-- **Firebase + plugin registration** – call `FirebaseApp.configure()` before `GeneratedPluginRegistrant.register(with: self)`.
-- **PushKit** – instantiate `PKPushRegistry`, set `desiredPushTypes = [.voIP]`, and forward the token inside `pushRegistry(_:didUpdate:for:)` via `SwiftFlutterCallkitIncomingPlugin.sharedInstance?.setDevicePushTokenVoIP(tokenHex)`.
-- **CallKit** – configure `CXProviderConfiguration`, keep a `CXCallController`, and implement `provider(_:perform: CXAnswerCallAction)` / `provider(_:perform: CXEndCallAction)` so native actions propagate to Flutter.
-- **Incoming push handler** – inside `pushRegistry(_:didReceiveIncomingPushWith:)`, convert the CometChat payload into `flutter_callkit_incoming.Data`, set `extra` with the raw payload, and call `showCallkitIncoming(..., fromPushKit: true)`.
-- **UUID helper** – reuse `createUUID(sessionid:)` to produce valid `UUID`s from long CometChat session IDs; this lets CallKit correlate calls even if the payload string exceeds 32 characters.
-
-If you change the MethodChannel name in Swift, remember to update `APNSService.platform` in Dart to match.
-
-## 6. Token registration and runtime events
-
-### 6.1 Standard APNs tokens
-
-`APNSService` hooks into `FirebaseMessaging.instance.getAPNSToken()` (and `onTokenRefresh`) before calling:
-
-```dart lines
-await CometChatPushRegistry.register(
- token: token,
- isFcm: false,
- isVoip: false,
-);
-```
-
-This registers the device token against the APNs provider selected in `CometChatConfig.apnProviderId`.
-
-### 6.2 VoIP tokens
-
-- Capture the PushKit token in `AppDelegate.pushRegistry(_:didUpdate:for:)`.
-- Forward it to Flutter via the MethodChannel or register it directly from Swift by invoking `CometChatPushRegistry` through `SwiftFlutterCallkitIncomingPlugin`.
-- If you keep the Dart implementation, emit a MethodChannel call named `onVoipToken` and handle it in `APNSService` by calling `CometChatPushRegistry.register(token: token, isFcm: false, isVoip: true);`.
-
-### 6.3 Local notifications and navigation
-
-- `APNSService._showNotification` displays a local notification when the incoming CometChat message does not belong to the currently open conversation.
-- `LocalNotificationService.handleNotificationTap` parses the payload, fetches the relevant user/group, and pushes `MessagesScreen`.
-- `NotificationLaunchHandler.pendingNotificationResponse` caches taps triggered while the app is terminated; replay it on the dashboard once the UI is ready.
-
-### 6.4 Call events
-
-- `FlutterCallkitIncoming.onEvent` is already wired inside `APNSService` to accept or end calls initiated by CallKit.
-- When native CallKit UI accepts/ends a call, Swift invokes `onCallAcceptedFromNative` / `onCallEndedFromNative` on the MethodChannel; `APNSService` then calls `FlutterCallkitIncoming.setCallConnected` or `CometChat.endCall()` to keep both stacks synchronized.
-
-
-**Cold-start VoIP call handling:** When the app is killed (not running) and a VoIP push arrives, the standard CometChat call listeners (`onOutgoingCallAccepted`, `onIncomingCallReceived`, etc.) will **not** fire because the Flutter engine is not yet initialized. In this scenario, you need a **native-to-Flutter bridge** that:
-
-1. Handles the VoIP push entirely in native Swift code (`AppDelegate` / `PushKit` handler).
-2. Presents the CallKit UI natively.
-3. When the user accepts, initializes the CometChat SDK from the native side using `CometChat.generateToken()` and `CometChat.startSession()`.
-4. Bridges the call state back to Flutter via a MethodChannel once the Flutter engine is ready.
-
-The standard `CometChatCallingExtension` call listeners only work when the app is already running (foreground or background with an active Flutter engine). See the [sample app's `AppDelegate.swift`](https://github.com/cometchat/cometchat-uikit-flutter/blob/v5/sample_app_push_notifications/ios/Runner/AppDelegate.swift) for a reference implementation of this native bridge pattern.
-
-
-## 7. Badge count using `unreadMessageCount`
-
-CometChat's Enhanced Push Notification payload includes an `unreadMessageCount` field representing the total unread messages across all conversations for the logged-in user. On iOS the badge can be updated from both the client side and the server side.
-
-If you use only APNs (no FCM on iOS), the badge is handled entirely by the server. CometChat sets the `aps.badge` value in the push payload, and iOS updates the app icon badge automatically when the notification is delivered. No additional dependency or client-side code is required — you can skip straight to section 7.5 to handle clearing the badge when the app opens.
-
-### 7.1 Enable unread badge count on the CometChat Dashboard
-
-1. Go to **CometChat Dashboard → Notifications → Settings → Preferences → Push Notification Preferences**.
-2. Scroll to the bottom and enable the **Unread Badge Count** toggle.
-
-This ensures CometChat includes the `unreadMessageCount` field in every push payload (and sets `aps.badge` for APNs) sent to your app.
-
-### 7.2 Expected payload format
-
-CometChat sends APNs payloads with this structure (relevant fields):
-
-```jsonc
-{
- "aps": {
- "alert": {
- "title": "New Message",
- "body": "John: Hello!"
- },
- "badge": 5,
- "sound": "default"
- },
- "unreadMessageCount": 5,
- "conversationId": "user_abc123",
- "parentId": "176001" // Optional - parent message ID; sent only for threaded notifications
-}
-```
-
-The `aps.badge` field is set by CometChat server-side. iOS updates the app icon badge automatically when the push is delivered — no client code is needed for this path.
-
-### 7.3 Client-side badge via FCM on iOS
-
-The steps below apply only if you register FCM tokens on iOS and show local notifications from Flutter. If you use pure APNs, skip to section 7.3.
-
-Add the `app_badge_plus` dependency:
-
-```yaml lines
-dependencies:
- app_badge_plus: ^1.2.6
-```
-
-Run `flutter pub get`, then install the native pods:
-
-```bash
-cd ios && pod install && cd ..
-```
-
-### 7.4 Update the badge from local notifications
-
-Inside your notification handler (for example `APNSService._showNotification`), parse `unreadMessageCount` and set the badge:
-
-```dart lines
-import 'package:app_badge_plus/app_badge_plus.dart';
-
-final unreadCountRaw = notificationData['unreadMessageCount'];
-int unreadMessageCount = int.tryParse(unreadCountRaw?.toString() ?? '') ?? 0;
-
-if (unreadMessageCount >= 0) {
- try {
- await AppBadgePlus.updateBadge(unreadMessageCount);
- } catch (e) {
- debugPrint('Error updating badge count: $e');
- }
-}
-```
-
-On iOS, `app_badge_plus` sets the native `UIApplication.shared.applicationIconBadgeNumber`. Passing `0` clears the badge.
-
-### 7.5 Clear badge and notifications when the app opens
-
-This is required regardless of whether you use APNs or FCM. Clear the badge count and dismiss all local notifications when the app launches and every time it resumes from the background. Add `WidgetsBindingObserver` to your root widget and register/remove the observer in `initState()` and `dispose()`:
-
-```dart lines
-Future _clearBadge() async {
- try {
- LocalNotificationService.flutterLocalNotificationsPlugin.cancelAll();
- await AppBadgePlus.updateBadge(0);
- debugPrint("The badge was cleared");
- } catch (e) {
- debugPrint("Error in clearing the badge value $e");
- }
-}
-
-@override
-void initState() {
- super.initState();
- WidgetsBinding.instance.addObserver(this);
- _clearBadge();
-}
-
-@override
-void didChangeAppLifecycleState(AppLifecycleState state) {
- if (state == AppLifecycleState.resumed) {
- _clearBadge();
- }
-}
-
-@override
-void dispose() {
- WidgetsBinding.instance.removeObserver(this);
- super.dispose();
-}
-```
-
-`cancelAll()` removes stale notification banners from the tray so they stay in sync with the badge count.
-
-## 8. Testing checklist
-
-1. Run the app on a physical device in debug first. Grant notification, microphone, camera, and Bluetooth permissions when prompted.
-2. Send a message from another user:
- - Foreground: a local notification banner shows (unless you are in that chat).
- - Background: APNs notification appears, tapping opens the right conversation.
-3. Force-quit the app, send another message push, tap it, and confirm `NotificationLaunchHandler` launches `MessagesScreen`.
-4. Trigger an incoming CometChat call. Ensure:
- - CallKit UI shows contact name, call type, and Accept/Decline.
- - Accepting on the lock screen notifies Flutter (`setupNativeCallListener`), starts the call session, and dismisses the native UI when the call ends.
-5. Decline the call and confirm both CallKit and Flutter clean up (`BoolSingleton` resets, overlays dismissed).
-6. Rotate through Wi-Fi/cellular and reinstall the app to confirm token registration works after refresh events.
-
-## 9. Troubleshooting tips
-
-| Symptom | Quick checks |
-| --- | --- |
-| No VoIP pushes | Entitlements missing? Ensure Push Notifications + Background Modes (VoIP) are enabled and the bundle ID matches the CometChat provider. |
-| CallKit UI never dismisses | Make sure `endCall(callUUID:)` reports to `CXProvider`, runs a `CXEndCallAction`, **and** calls `SwiftFlutterCallkitIncomingPlugin.sharedInstance?.endCall`. |
-| Flutter never receives native call events | Confirm the MethodChannel names match between Swift and Dart, and `APNSService.setupNativeCallListener` runs inside `initState`. |
-| Token registration errors | Double-check `CometChatConfig` provider IDs, and verify you call `registerPushToken` after `CometChatUIKit.login` succeeds. |
-| Notification taps ignored | Ensure you replay `NotificationLaunchHandler.pendingNotificationResponse` **after** the navigator key is ready (typically `WidgetsBinding.instance.addPostFrameCallback`). |
-
-{/* ## Additional resources
-
-
-
-
-
-Push notifications sample app for Flutter
-
-View on Github
-
-
-
- */}
diff --git a/notifications/flutter-push-notifications.mdx b/notifications/flutter-push-notifications.mdx
new file mode 100644
index 000000000..0e2023590
--- /dev/null
+++ b/notifications/flutter-push-notifications.mdx
@@ -0,0 +1,406 @@
+---
+title: "Flutter"
+description: "Add CometChat chat push notifications to a Flutter app (Android + iOS) with the drop-in cometchat_push_notifications SDK."
+---
+
+## What this guide covers
+
+- Adding the `cometchat_push_notifications` plugin and initializing it.
+- Platform wiring: Gradle/Firebase on Android, Podfile/capabilities on iOS.
+- Requesting permission and registering tokens (FCM on Android, APNs on iOS) after login.
+- Receiving pushes and letting the SDK show chat notifications.
+- Handling notification taps, the conversation on screen, and badge counts.
+- Testing and troubleshooting.
+
+
+The `cometchat_push_notifications` plugin replaces the previous approach of copying the UI Kit sample's `lib/notifications` stack and hand-wiring `PNRegistry` / `CometChatPushRegistry` and native `MainActivity` / `AppDelegate` bridges. Token registration, notification display, delivery receipts, notification taps and badge management are handled inside the plugin.
+
+
+
+This guide covers **chat notifications** only. To ring incoming calls in every app state, see [Push Ringing](/calls/flutter/voip-calling), which uses the `cometchat_voip` package.
+
+
+## How it works
+
+- **Android (FCM):** Firebase issues the registration token and delivers the CometChat payload as a data message. Your `firebase_messaging` handler forwards `message.data` to `handlePushNotification`, and the plugin shows the notification.
+- **iOS (APNs):** Apple issues the APNs device token, and the system shows the chat alert from the APNs payload.
+- **CometChat's role:** The providers you add in the dashboard bind your registered tokens to the logged-in user so CometChat can route pushes on your behalf.
+- **The plugin's role:** `cometchat_push_notifications` registers the token with CometChat, parses payloads, shows notifications on Android and sends delivery receipts. It builds on [`cometchat_sdk`](https://pub.dev/packages/cometchat_sdk), which pub resolves for you.
+
+## Prerequisites
+
+- The providers, Firebase project, and Apple/APNs credentials from **[Getting Started](/notifications/push-getting-started)** (this guide assumes those are done).
+- Dart **3.10.8+**, and the Flutter release that bundles it or newer. (The plugin's pubspec floor of `flutter: >=3.3.0` is looser than its Dart floor; the Dart one is what applies.)
+- **Android:** Kotlin **2.0+**, `minSdkVersion 24`, `compileSdkVersion 36`.
+- **iOS:** iOS 13.0+.
+- A physical device — background delivery is unreliable on emulators/simulators.
+
+
+**Complete the [Getting Started](/notifications/push-getting-started) guide first** — enable Push Notifications, add your providers (FCM for Android, APNs for iOS), and finish the Firebase/Apple setup. This guide covers only the Flutter app wiring.
+
+
+## 1. Store your credentials
+
+Keep the values from Getting Started somewhere your app can read them. Only the fields for the platforms you ship are needed:
+
+```dart lines
+class AppCredentials {
+ static const appId = "YOUR_APP_ID";
+ static const region = "YOUR_REGION";
+ static const authKey = "YOUR_AUTH_KEY";
+
+ // Android
+ static const fcmProviderId = "FCM-PROVIDER-ID";
+
+ // iOS
+ static const apnProviderId = "APNS-PROVIDER-ID";
+}
+```
+
+## 2. Add the plugin and configure the platform
+
+Add the plugin to `pubspec.yaml`. On Android also add Firebase Messaging (used to receive the FCM data messages):
+
+```yaml lines
+dependencies:
+ cometchat_push_notifications: ^2.0.0
+ # Android only — to receive FCM data messages:
+ firebase_core: ^3.9.0
+ firebase_messaging: ^15.1.6
+```
+
+`cometchat_sdk` is pulled in transitively. Run `flutter pub get`.
+
+
+
+ With `google-services.json` already in `android/app/` (from [Getting Started](/notifications/push-getting-started)):
+
+ 1. Apply the Google Services plugin and ensure Kotlin is **2.0+** in `android/settings.gradle.kts`:
+
+ ```kotlin lines
+ plugins {
+ id("dev.flutter.flutter-plugin-loader") version "1.0.0"
+ id("com.android.application") version "8.11.1" apply false
+ id("org.jetbrains.kotlin.android") version "2.1.0" apply false
+ id("com.google.gms.google-services") version "4.4.2" apply false
+ }
+ ```
+
+ 2. Apply the plugins in `android/app/build.gradle.kts`:
+
+ ```kotlin lines
+ plugins {
+ id("com.android.application")
+ id("kotlin-android")
+ id("com.google.gms.google-services")
+ id("dev.flutter.flutter-gradle-plugin")
+ }
+ ```
+
+ 3. Set `applicationId` to your package name and keep `minSdk = 24` or higher. Run `flutterfire configure` if you still need `firebase_options.dart`.
+
+ 4. Add a notification icon to `android/app/src/main/res/drawable/` (for example `ic_notification.png`) and pass its name as `androidSmallIcon` in step 3. Android draws the small icon as a flat silhouette, so it must be white on transparent; a launcher icon shows as a grey square.
+
+
+ You do **not** need to add notification permissions to your `AndroidManifest.xml`. The plugin declares what it needs, and Gradle merges it into your app automatically.
+
+
+
+ 1. Set the deployment target in `ios/Podfile`, then install pods:
+
+ ```ruby
+ platform :ios, '13.0'
+ ```
+
+ ```bash
+ cd ios && pod install && cd ..
+ ```
+
+ 2. Open `ios/Runner.xcworkspace` in Xcode and set the development team that owns the APNs key. (The **Push Notifications** / **Background Modes** capabilities come from [Getting Started](/notifications/push-getting-started).)
+
+ 3. No push code is needed in your `AppDelegate`. The plugin registers with APNs and gets the device token itself.
+
+ 4. **Optional: Notification Service Extension.** For delivery receipts, sender avatars and conversation grouping while the app is in the background or killed, add a Notification Service Extension:
+
+ 1. In Xcode: **File → New → Target → Notification Service Extension**.
+ 2. In your `Podfile`, add the extension's pod to the extension target only:
+
+ ```ruby
+ target 'NotificationService' do
+ pod 'CometChatPushNotificationsNSE',
+ :path => '.symlinks/plugins/cometchat_push_notifications/ios'
+ end
+ ```
+
+ 3. Replace the generated `NotificationService.swift` with:
+
+ ```swift lines
+ import CometChatPushNotificationsNSE
+
+ class NotificationService: CometChatNotificationServiceExtension {}
+ ```
+
+ 4. Add the app and the extension to a shared App Group, and add the key `CometChatExtensionGroupID` with that group's identifier to the extension's `Info.plist`.
+
+
+
+## 3. Initialize the SDK
+
+Initialize the plugin before `runApp`. On Android you also initialize Firebase and forward FCM messages to the plugin (including a background isolate handler).
+
+
+
+ ```dart lines
+ import 'package:firebase_core/firebase_core.dart';
+ import 'package:firebase_messaging/firebase_messaging.dart';
+ import 'package:flutter/material.dart';
+ import 'package:cometchat_push_notifications/cometchat_push_notifications_export.dart';
+
+ /// Runs in a background isolate for data messages delivered while the app is
+ /// terminated or backgrounded. Must initialize the plugin before handling.
+ @pragma('vm:entry-point')
+ Future _firebaseBackgroundHandler(RemoteMessage message) async {
+ await Firebase.initializeApp();
+ await CometChatPushNotifications.init(
+ const CometChatNotificationConfig(
+ appId: AppCredentials.appId,
+ region: AppCredentials.region,
+ androidSmallIcon: 'ic_notification',
+ ),
+ );
+ await CometChatPushNotifications.handlePushNotification(message.data);
+ }
+
+ Future main() async {
+ WidgetsFlutterBinding.ensureInitialized();
+ await Firebase.initializeApp();
+
+ await CometChatPushNotifications.init(
+ const CometChatNotificationConfig(
+ appId: AppCredentials.appId,
+ region: AppCredentials.region,
+ androidSmallIcon: 'ic_notification',
+ ),
+ );
+
+ // Forward FCM data messages to the plugin.
+ FirebaseMessaging.onBackgroundMessage(_firebaseBackgroundHandler);
+ FirebaseMessaging.onMessage.listen((message) {
+ CometChatPushNotifications.handlePushNotification(message.data);
+ });
+
+ runApp(const MyApp());
+ }
+ ```
+
+
+ ```dart lines
+ import 'package:flutter/material.dart';
+ import 'package:cometchat_push_notifications/cometchat_push_notifications_export.dart';
+
+ Future main() async {
+ WidgetsFlutterBinding.ensureInitialized();
+
+ await CometChatPushNotifications.init(
+ const CometChatNotificationConfig(
+ appId: AppCredentials.appId,
+ region: AppCredentials.region,
+ ),
+ );
+
+ runApp(const MyApp());
+ }
+ ```
+
+
+
+`handlePushNotification` returns `true` when it handled a chat push, and `false` for any other push, which is still yours to handle.
+
+The `CometChatNotificationConfig` also accepts:
+
+| Field | Default | Purpose |
+| --- | --- | --- |
+| `showInAppNotifications` | `false` | Show chat pushes as notifications while the app is in the foreground. |
+| `enableBadgeCount` | `true` | Mirror `unreadMessageCount` onto the iOS app icon badge. |
+| `sendDeliveryReceipts` | `true` | Mark each received message as delivered with CometChat. |
+| `androidSmallIcon` | launcher icon | Drawable name of the Android notification icon. |
+| `androidChannelId` | `cometchat_messages` | Android notification channel to post to. |
+| `androidChannelName` | `Chat Messages` | Channel name shown in Android settings. |
+| `androidChannelDescription` | `New message notifications` | Channel description shown in Android settings. |
+
+
+Android fixes a channel's importance, sound and vibration when it's first created. To change any of them later, use a new `androidChannelId`.
+
+
+### If your app also rings calls
+
+If your app uses `cometchat_voip` for incoming calls, hand each push to it first, and to this plugin only when it isn't a call:
+
+```dart lines
+Future handlePush(RemoteMessage message) async {
+ final isCall = await CometChatVoip.handlePushNotification(
+ message.data,
+ sentTime: message.sentTime,
+ );
+ if (!isCall) {
+ await CometChatPushNotifications.handlePushNotification(message.data);
+ }
+}
+```
+
+See [Push Ringing](/calls/flutter/voip-calling) for the calls setup.
+
+## 4. Request permission and register tokens
+
+Request permission early, and register tokens **after** your CometChat user logs in (registration binds the token to the session):
+
+```dart lines
+await CometChatPushNotifications.requestPermission();
+```
+
+
+
+ ```dart lines
+ // After CometChatUIKit.login(...) / CometChat.login(...) succeeds:
+ final token = await FirebaseMessaging.instance.getToken();
+ await CometChatPushNotifications.registerPushToken(
+ token: token,
+ platform: PushPlatform.FCM_FLUTTER_ANDROID,
+ providerId: AppCredentials.fcmProviderId,
+ );
+ ```
+
+
+ Omit the token: the plugin registers with APNs itself and uses the device token it gets back.
+
+ ```dart lines
+ // After CometChatUIKit.login(...) / CometChat.login(...) succeeds:
+ await CometChatPushNotifications.registerPushToken(
+ platform: PushPlatform.APNS_FLUTTER_DEVICE,
+ providerId: AppCredentials.apnProviderId,
+ );
+ ```
+
+
+
+`registerPushToken` returns the server's response, or `null` when registration failed.
+
+On Android, register the new token again whenever Firebase rotates it:
+
+```dart lines
+FirebaseMessaging.instance.onTokenRefresh.listen((token) {
+ CometChatPushNotifications.registerPushToken(
+ token: token,
+ platform: PushPlatform.FCM_FLUTTER_ANDROID,
+ providerId: AppCredentials.fcmProviderId,
+ );
+});
+```
+
+Before logging the user out, unregister the token while the session that owns it still exists:
+
+```dart lines
+await CometChatPushNotifications.unregisterPushToken();
+// then CometChatUIKit.logout(...) / CometChat.logout(...)
+```
+
+## 5. Notification taps and the conversation on screen
+
+Subscribe to taps to open the right conversation (same on both platforms). A tap that launched the app from a cold start is held until you subscribe:
+
+```dart lines
+CometChatPushNotifications.onNotificationTap.listen((response) {
+ // response.conversationId, response.receiverType, response.sender,
+ // response.senderName, response.messageId
+ // Navigate to your messages screen for this conversation.
+});
+```
+
+Tell the plugin which conversation is on screen, so pushes for it aren't shown while the user is already reading it:
+
+```dart lines
+@override
+void initState() {
+ super.initState();
+ CometChatPushNotifications.setActiveConversation(
+ id: widget.uid,
+ receiverType: CometChatPushReceiverType.user,
+ );
+}
+
+@override
+void dispose() {
+ CometChatPushNotifications.setActiveConversation();
+ super.dispose();
+}
+```
+
+## 6. Badge count
+
+CometChat's Enhanced Push Notification payload includes an `unreadMessageCount` field (the total unread across all conversations). Enable it once on the dashboard:
+
+1. Go to **CometChat Dashboard → Notification Engine → Settings → Preferences → Push Notification Preferences**.
+2. Enable the **Unread Badge Count** toggle.
+
+
+
+ Android has no app icon badge. `setBadgeCount` and `getBadgeCount` do nothing there.
+
+
+ With `enableBadgeCount: true` (the default), the plugin mirrors `unreadMessageCount` onto the app icon. To set it yourself:
+
+ ```dart lines
+ await CometChatPushNotifications.setBadgeCount(0);
+ ```
+
+
+
+Dismiss stale notifications when the app opens and on every resume:
+
+```dart lines
+await CometChatPushNotifications.clearNotifications();
+```
+
+Pass `conversationId` to clear only that conversation's notifications. Add a `WidgetsBindingObserver` to your root widget and clear in `initState` and on `AppLifecycleState.resumed`.
+
+## 7. Testing checklist
+
+1. Run on a physical device. Grant notification permission when prompted (Android 13+ requires `POST_NOTIFICATIONS`).
+2. Send a message from another user:
+ - Foreground: no banner unless `showInAppNotifications: true` (and you're not already in that chat).
+ - Background: a notification appears; tapping opens the right conversation via `onNotificationTap`.
+3. Force-quit the app, send another message, tap the notification, and confirm it navigates to the conversation.
+4. Open a conversation and send a message to it from another user: no notification is shown for it.
+5. Toggle Wi-Fi/cellular and reinstall to confirm token registration survives refreshes.
+
+## 8. Troubleshooting
+
+| Symptom | Platform | Quick checks |
+| --- | --- | --- |
+| No notifications received | Android | Confirm `google-services.json` is in `android/app/`, the package name matches Firebase, permission is granted (Android 13+), and your `firebase_messaging` handlers call `handlePushNotification(message.data)`. |
+| Build fails after adding the plugin | Android | Ensure Kotlin is **2.0+** in `android/settings.gradle.kts` and `minSdk = 24`. |
+| Notification icon is a grey square | Android | Set `androidSmallIcon` to a white-on-transparent drawable. |
+| Channel sound or importance doesn't change | Android | Android keeps a channel's settings once created. Use a new `androidChannelId`. |
+| No notifications received | iOS | Ensure Push Notifications and Background Modes → Remote notifications are enabled, the bundle ID matches the CometChat APNs provider, and you registered with `PushPlatform.APNS_FLUTTER_DEVICE`. |
+| No receipts or avatars while the app is killed | iOS | Add the Notification Service Extension and the shared App Group (step 2). |
+| Token registration errors | Both | Verify the provider IDs match the dashboard exactly and that `registerPushToken` runs **after** login. |
+| Notification taps ignored | Both | Subscribe to `onNotificationTap` once your navigator is ready. |
+
+## Resources
+
+
+
+ The drop-in chat push plugin on pub.dev.
+
+
+ Source, changelog, and example app.
+
+
diff --git a/notifications/push-getting-started.mdx b/notifications/push-getting-started.mdx
new file mode 100644
index 000000000..3d52f0db7
--- /dev/null
+++ b/notifications/push-getting-started.mdx
@@ -0,0 +1,99 @@
+---
+title: "Getting Started"
+description: "Getting started with CometChat Push Notifications — enable push, add providers, configure Firebase and APNs, then pick your platform."
+---
+
+The steps below are common to every platform. Complete them once, then follow your platform guide for app-specific wiring (Gradle, Podfile, manifest, service worker, token registration). Each platform guide links back here.
+
+
+Which providers and credentials you need depends on your platform: **Android / Web / React Native (Android) / Flutter (Android)** use **FCM**; **iOS / React Native (iOS) / Flutter (iOS)** use **APNs** (plus **APNs VoIP** for calls).
+
+
+## CometChat dashboard setup
+
+1. Go to **Notifications → Settings** and enable **Push Notifications**.
+
+
+
+
+
+2. Click **Add Credentials** and add the provider(s) your platform needs:
+
+ - **FCM** (Android, Web, React Native Android, Flutter Android): upload the Firebase **service account JSON** (Firebase → Project settings → Service accounts → Generate new private key).
+ - **APNs Device** (iOS alerts): upload your `.p8` Auth Key with its **Key ID**, **Team ID**, and **Bundle ID**.
+ - **APNs VoIP** (iOS calls): add a second provider with the same `.p8` (recommended for CallKit reliability).
+
+
+
+
+
+3. Copy each **Provider ID** and keep your **App ID**, **Region**, and **Auth Key** handy — you'll add these to your app's constants when registering tokens.
+
+## Firebase configuration
+
+Required for FCM (Android, Web, React Native Android, Flutter Android).
+
+1. In the [Firebase Console](https://console.firebase.google.com/), create or select a project and enable **Cloud Messaging**.
+2. Register the app for your platform and download its config file:
+
+
+
+ Register your Android package name (the same as `applicationId` in `android/app/build.gradle`) and download **`google-services.json`** into `android/app/`.
+
+
+ Add a **Web** app (`>`), copy the **Firebase config** object, and under **Project settings → Cloud Messaging → Web Push certificates** generate/copy the **VAPID key**.
+
+
+
+3. For the CometChat **FCM** provider, generate a **service account JSON** under **Project settings → Service accounts → Generate new private key** and upload it in the dashboard step above.
+
+
+
+
+
+## iOS (Apple) setup
+
+Required for any iOS target (native iOS, React Native iOS, Flutter iOS).
+
+1. **APNs Auth Key (`.p8`)** — in the [Apple Developer Member Center](https://developer.apple.com/membercenter) → **Certificates, Identifiers & Profiles → Keys**, click **+**, enable **Apple Push Notification service (APNs)**, register, and download the `.p8`. Note the **Key ID**, **Team ID**, and your **Bundle ID** — you'll upload these to the CometChat APNs provider(s) in the dashboard step above.
+2. **Capabilities** — in Xcode, enable **Push Notifications** and **Background Modes → Remote notifications** (add **Voice over IP** as well if you support calls). Add microphone/camera usage strings to `Info.plist` for CallKit.
+
+
+**`.p12` certificates are deprecated.** Apple recommends `.p8` Auth Keys — one key works for all your apps, they never expire, and they are the only format actively supported going forward. Migrate to `.p8` if you haven't already.
+
+
+
+
+
+
+## Choose your platform
+
+After completing the setup above, follow the guide for your platform to wire the client, register tokens after login, and handle taps/calls/badges.
+
+
+
+} href="/notifications/android-push-notifications">
+UI Kit implementation
+
+
+} href="/notifications/ios-apns-push-notifications">
+UI Kit implementation
+
+
+} href="/notifications/flutter-push-notifications">
+Android + iOS
+
+
+} href="/notifications/react-native-push-notifications-android">
+UI Kit implementation
+
+
+} href="/notifications/react-native-push-notifications-ios">
+UI Kit implementation
+
+
+} href="/notifications/web-push-notifications">
+UI Kit implementation
+
+
+
diff --git a/notifications/push-overview.mdx b/notifications/push-overview.mdx
index 5d6e9fc84..eff64e8c7 100644
--- a/notifications/push-overview.mdx
+++ b/notifications/push-overview.mdx
@@ -59,12 +59,8 @@ UI Kit implementation, hand-wired PushKit + CallKit
UI Kit implementation
-} href="/notifications/flutter-push-notifications-android">
-UI Kit implementation
-
-
-} href="/notifications/flutter-push-notifications-ios">
-UI Kit implementation
+} href="/notifications/flutter-push-notifications">
+Android + iOS
} href="/notifications/react-native-push-notifications-android">
diff --git a/sdk/flutter/real-time-listeners.mdx b/sdk/flutter/real-time-listeners.mdx
index f1004d3fe..1f083f367 100644
--- a/sdk/flutter/real-time-listeners.mdx
+++ b/sdk/flutter/real-time-listeners.mdx
@@ -30,6 +30,10 @@ CometChat provides 5 listeners viz.
4. [ConversationListener](#conversation-listener)
5. [AIAssistantListener](#ai-assistant-listener)
+
+**Calls: `CallListener` is superseded.** `CallListener` and `CometChat.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 (`initiateCall`, `acceptCall`, `rejectCall`, `endCall`). Calls now ring through `cometchat_voip` and announce themselves on the Calls SDK's `CometChatCalls.addIncomingCallListener` (`onIncomingCallReceived`, `onIncomingCallEnded(call, reason)`), whether the call arrived by push on a killed app or while the app was running. The caller's "answered" event is `onParticipantJoined` on the prepared session. See [Ringing](/calls/flutter/ringing) and [Migrating from Legacy Ringing](/calls/flutter/migrating-from-legacy-ringing).
+
+
## User Listener
The `UserListener` class provides you with live events related to users. Below are the callback methods provided by the `UserListener` class.
diff --git a/ui-kit/flutter/call-features.mdx b/ui-kit/flutter/call-features.mdx
index 8ab9a6468..ddfe9b4d2 100644
--- a/ui-kit/flutter/call-features.mdx
+++ b/ui-kit/flutter/call-features.mdx
@@ -1,178 +1,97 @@
---
title: "Call"
-description: "Integrate CometChat Flutter UI Kit calling with one-on-one and group audio/video calls, setup, initialization, and call events."
+description: "Add one-on-one and group audio and video calling to your Flutter UI Kit app: how a call works, what rings incoming calls, and which component handles each part."
---
| Field | Value |
| --- | --- |
-| Package | `cometchat_chat_uikit` |
-| Import | `import 'package:cometchat_chat_uikit/cometchat_chat_uikit.dart';` |
-| Purpose | Integrate CometChat Flutter UI Kit calling with one-on-one and group audio/video calls, setup, initialization, and call events. |
-| Key classes | `CometChatSubscriptionType` · `CometChatUIKit` |
-| Key methods | `addCallListener()` · `init()` · `removeCallListener()` |
-| Related | [Call Buttons](/ui-kit/flutter/call-buttons) · [Incoming Call](/ui-kit/flutter/incoming-call) · [Outgoing Call](/ui-kit/flutter/outgoing-call) · [Call Logs](/ui-kit/flutter/call-logs) |
+| Package | `cometchat_chat_uikit` (brings `cometchat_calls_sdk`); `cometchat_voip` in your app for ringing |
+| Import | `import 'package:cometchat_chat_uikit/cometchat_calls_uikit.dart';` |
+| Required setup | `enableCalls = true` and `callPushBridge` in `UIKitSettings`, `CallNavigationContext.navigatorKey` on `MaterialApp`, `cometchat_voip` initialized, every push handed to `CometChatVoip.handlePushNotification` |
+| Incoming calls | Rung by `cometchat_voip` (notification and ring screen on Android, CallKit on iOS), in every app state. No call listener, no incoming call component |
+| Key classes | `CometChatCallButtons` · `CometChatOutgoingCall` · `CometChatOngoingCall` · `CometChatCallLogs` · `CometChatCallPushBridge` |
+| Deprecated / superseded | `CometChatIncomingCall` (web only); Chat SDK `CometChat.addCallListener` / `initiateCall` / `acceptCall` / `rejectCall` / `endCall` (superseded) |
+| Related | [Calling Integration](/ui-kit/flutter/calling-integration) · [Push Ringing](/calls/flutter/voip-calling) · [Migrating from Legacy Ringing](/calls/flutter/migrating-from-legacy-ringing) |
-## Overview
+CometChat's Calls feature lets you add one-on-one and group audio and video calling to your application. This page is an overview of how calling works in the Flutter UI Kit and which component handles each part.
+
+## How a call works
+
+```mermaid
+sequenceDiagram
+ participant A as Caller · UI Kit
+ participant BE as CometChat backend
+ participant V as Callee · cometchat_voip
+ participant U as Callee · UI Kit
+
+ A->>BE: Call button → initiateCall
+ Note over A: Outgoing call screen: "Calling…"
+ BE-->>V: Push (initiated)
+ V->>V: Rings: notification, ring screen or CallKit, ringtone
+ V-->>A: Callee's phone reached → "Ringing…"
+ V->>U: Answer → call push bridge → answered
+ U->>U: Call screen → joinSession
+ U-->>A: Callee joined → caller joins
+ Note over A,U: In call. Hang-up leaves the session on both sides.
+```
-CometChat's Calls feature allows you to integrate one-on-one and group audio/video calling capabilities into your application. In V6, calling is built into the `cometchat_chat_uikit` package — no separate calls dependency needed.
+- **Placing a call** is done by the UI Kit through the Calls SDK. The Chat SDK is not involved.
+- **Ringing the callee** is done by `cometchat_voip` from the call push, with the app in the foreground, in the background or killed. It draws the ring screen (CallKit on iOS) and asks for the microphone and camera on Answer.
+- **Joining** is done by the UI Kit: `cometchat_voip` hands the answered call to it through your app's call push bridge, and the UI Kit opens its call screen.
+- **Decline, timeout, busy and missed calls** are handled by `cometchat_voip`. The caller's outgoing call screen closes.
+- **The call itself** runs in the Calls SDK session on the UI Kit's call screen; hanging up leaves the session.
## Integration
-### Step 1: Add Dependency
+All steps are on the [Calling Integration](/ui-kit/flutter/calling-integration) page:
-Add the dependency to your `pubspec.yaml`:
+1. Add `cometchat_voip` and Firebase, and initialize the Calls SDK and `cometchat_voip` at app start.
+2. Add the call push bridge (a short class over `cometchat_voip`) and set `enableCalls` and `callPushBridge` in `UIKitSettings`.
+3. Set `CallNavigationContext.navigatorKey` on your `MaterialApp`.
+4. Hand every push to `cometchat_voip` on Android, register the push token, check ring readiness after login, and log out through `CometChatUIKit.logout`.
-```yaml pubspec.yaml
-dependencies:
- cometchat_chat_uikit: ^6.2.0
-```
+
+There is no call listener to add. Apps that registered `CometChat.addCallListener` to show the incoming call screen should remove it; the Chat SDK's calling APIs are superseded. See [Migrating from Legacy Ringing](/calls/flutter/migrating-from-legacy-ringing). On web, which has no push ringing, the UI Kit keeps ringing calls with the Chat SDK.
+
-***
+## Call Components
-### Step 2: Update build.gradle
+### Call Buttons
-Set `minSdkVersion` to at least 26 in `android/app/build.gradle`:
+`CometChatCallButtons` places audio and video calls. It renders in the [Message Header](/ui-kit/flutter/message-header) when calls are enabled.
-
-
-```gradle
-defaultConfig {
- minSdkVersion 26
- targetSdkVersion flutter.targetSdkVersion
- versionCode flutterVersionCode.toInteger()
- versionName flutterVersionName
-}
-```
-
-
+[Learn more about Call Buttons →](/ui-kit/flutter/call-buttons)
-***
+### Outgoing Call
-### Step 3: Update iOS Podfile
+`CometChatOutgoingCall` shows "Calling…", then "Ringing…" once the callee's phone is reached, closes if the call is declined, busy or unanswered, and opens the call screen once the callee joins.
-In `ios/Podfile`, set the minimum iOS version to 15.1:
+
+
+
-```
-platform :ios, '15.1'
-```
+[Learn more about Outgoing Call →](/ui-kit/flutter/outgoing-call)
-
-These are the Calls SDK's own floors (`minSdkVersion 26`, `platform :ios, '15.1'` as of
-`cometchat_calls_sdk` 5.0.7), and they apply to **every** UI Kit app — the kit depends on the Calls
-SDK unconditionally, so you inherit them even without enabling calling. Gradle and CocoaPods take
-the maximum across the dependency graph, so a lower value is a build error rather than a warning.
-
-
-***
-
-### Step 4: Modify UIKitSettings
-
-Activate calling features by setting `enableCalls` to `true` in UIKitSettings:
-
-
-
-```dart
-UIKitSettings uiKitSettings = (UIKitSettingsBuilder()
- ..subscriptionType = CometChatSubscriptionType.allUsers
- ..autoEstablishSocketConnection = true
- ..region = "REGION"
- ..appId = "APP_ID"
- ..authKey = "AUTH_KEY"
- ..enableCalls = true
- ..callingConfiguration = CallingConfiguration()
-).build();
-
-CometChatUIKit.init(uiKitSettings: uiKitSettings,
- onSuccess: (successMessage) {},
- onError: (e) {},
-);
-```
-
-
-
-To allow launching the Incoming Call screen from any widget, provide the navigator key:
-
-
-
-```dart
-MaterialApp(
- navigatorKey: CallNavigationContext.navigatorKey,
- home: YourHomeScreen(),
-)
-```
-
-
-
-### Listeners
-
-Register call listeners for top-level widgets:
-
-
-
-```dart
-class _YourClassNameState extends State with CallListener {
- @override
- void initState() {
- super.initState();
- CometChat.addCallListener(listenerId, this);
- }
-
- @override
- void dispose() {
- super.dispose();
- CometChat.removeCallListener(listenerId);
- }
-
- @override
- void onIncomingCallReceived(Call call) {
- debugPrint("onIncomingCallReceived");
- }
-
- @override
- void onOutgoingCallAccepted(Call call) {
- debugPrint("onOutgoingCallAccepted");
- }
-
- @override
- void onOutgoingCallRejected(Call call) {
- debugPrint("onOutgoingCallRejected");
- }
-
- @override
- void onIncomingCallCancelled(Call call) {
- debugPrint("onIncomingCallCancelled");
- }
-
- @override
- void onCallEndedMessageReceived(Call call) {
- debugPrint("onCallEndedMessageReceived");
- }
-
- @override
- Widget build(BuildContext context) {
- return const Placeholder();
- }
-}
-```
-
-
+### Incoming Call
-***
+On Android and iOS, incoming calls are rung and drawn by `cometchat_voip`, so there is no UI Kit component to place. The deprecated `CometChatIncomingCall` is shown on web only.
-## Features
+[Incoming Call (deprecated) →](/ui-kit/flutter/incoming-call)
-### Incoming Call
+### Call Logs
-The Incoming Call widget lets users receive real-time audio and video calls. When a call is received, it displays caller information with accept/reject options.
+`CometChatCallLogs` displays the history of calls, including missed, received and dialed calls. Users can open call details and call back from the log.
-### Outgoing Call
+
+
+
-The Outgoing Call widget manages the outgoing call process. It displays recipient information and call status, and automatically transitions to the ongoing call screen when accepted.
+[Learn more about Call Logs →](/ui-kit/flutter/call-logs)
-### Call Logs
+### Call Log Details
-The [Call Logs](/ui-kit/flutter/call-logs) widget displays records of call events including caller, time, and duration.
+For participants, join and leave history and recordings of a call, see the [Call Log Details](/ui-kit/flutter/guide-call-log-details) guide.
diff --git a/ui-kit/flutter/calling-integration.mdx b/ui-kit/flutter/calling-integration.mdx
new file mode 100644
index 000000000..9906dca70
--- /dev/null
+++ b/ui-kit/flutter/calling-integration.mdx
@@ -0,0 +1,303 @@
+---
+title: "Calling Integration"
+description: "Add voice and video calling to your Flutter UI Kit app: enable calls, ring incoming calls with cometchat_voip through the UI Kit's call push bridge, permissions and logout."
+---
+
+## Overview
+
+With calling enabled, the UI Kit places calls from the message header and call logs, shows the outgoing call screen, and hosts the in-call screen. Incoming calls ring through `cometchat_voip`, CometChat's push ringing package: it shows the call notification and the ring screen (CallKit on iOS), plays the ringtone, and hands the answered call to the UI Kit, which opens its call screen and joins. This works with the app in the foreground, in the background or killed.
+
+
+Make sure you've completed the [Getting Started](/ui-kit/flutter/getting-started) guide before proceeding.
+
+
+{/* TODO(RC): every API on this page is from the ringing-v2-uikit branch (cometchat_calls_sdk 5.1.0-team + cometchat_voip release/2.0). The branch's pubspec still says version 6.2.0, which is the release already on pub.dev; it needs its own bump before this page can pin a real version. Re-check against the released packages before publishing. */}
+
+```mermaid
+sequenceDiagram
+ participant A as Caller · UI Kit
+ participant BE as CometChat backend
+ participant V as Callee · cometchat_voip
+ participant U as Callee · UI Kit
+
+ A->>BE: Call button → initiateCall
+ Note over A: Outgoing call screen: "Calling…"
+ BE-->>V: Push (initiated)
+ V->>V: Rings: notification, ring screen or CallKit, ringtone
+ V-->>A: Callee's phone reached → "Ringing…"
+ V->>U: Answer → call push bridge → answered
+ U->>U: Call screen → joinSession
+ U-->>A: Callee joined → caller joins
+ Note over A,U: In call. Hang-up leaves the session on both sides.
+```
+
+What you write: the UI Kit settings with calls enabled, the `cometchat_voip` setup, a small call push bridge, the push hand-off, and the platform permissions. There is no call listener to register and no incoming call screen to show.
+
+## Step 1: Dependencies
+
+The UI Kit brings the Calls SDK. Calling through `cometchat_voip` needs a UI Kit newer than 6.2.0 — 6.2.0 doesn't carry the call push bridge. Add `cometchat_voip` for ringing, and Firebase for call pushes on Android:
+
+```yaml pubspec.yaml
+dependencies:
+ cometchat_chat_uikit: ^6.3.0
+ cometchat_voip: ^2.0.0
+ firebase_core: ^3.0.0
+ firebase_messaging: ^15.0.0
+```
+
+{/* TODO(RC): confirm the UI Kit release that ships ringing through cometchat_voip, and the published cometchat_voip version. 6.2.0 is already on pub.dev WITHOUT the bridge API, so this pin must name the first release that has it (6.3.0 here is a placeholder). Bump ui-kit/flutter/getting-started.mdx to match. */}
+
+
+
+`minSdk 26` (already required by the UI Kit), and your Firebase configuration (`google-services.json`). `cometchat_voip` adds the permissions, services and ring screen it needs to the manifest.
+
+
+1. `platform :ios, '15.1'` in the `Podfile` (already required by the UI Kit).
+2. In Xcode, add the **Push Notifications** capability, and under **Background Modes** check **Voice over IP** and **Remote notifications**.
+3. Add `NSMicrophoneUsageDescription` and `NSCameraUsageDescription` to `Info.plist`.
+
+No `AppDelegate` code is needed: `cometchat_voip` owns PushKit and CallKit. Don't create a `PKPushRegistry` of your own; a second registry takes the VoIP pushes and those calls never ring.
+
+
+
+Push notifications must be enabled in the CometChat dashboard, with the push providers set up. See [Getting Started](/notifications/push-getting-started).
+
+## Step 2: Initialize the Calls SDK and cometchat_voip
+
+`cometchat_voip` needs the Calls SDK initialized first. Do both at app start, before `CometChatUIKit.init`, and again in the Android background message handler (Step 5), where your app's start-up code hasn't run:
+
+```dart
+final callAppSettings = (CallAppSettingBuilder()
+ ..appId = "APP_ID"
+ ..region = "REGION")
+ .build();
+
+Future initPushRinger() async {
+ final done = Completer();
+ CometChatCalls.init(
+ callAppSettings,
+ onSuccess: (_) => done.complete(),
+ onError: (CometChatCallsException e) => done.complete(),
+ );
+ await done.future;
+
+ await CometChatVoip.init(
+ CometChatVoipConfig.fromCallAppSettings(
+ callAppSettings,
+ providerId: "PROVIDER_ID", // the FCM provider on Android, the APNs provider on iOS
+ ),
+ );
+}
+```
+
+Every `cometchat_voip` option (ring timeout, missed call notifications, your own ring screen) is on the Calls SDK's [Push Ringing](/calls/flutter/voip-calling#step-2-initialize) page.
+
+## Step 3: Connect cometchat_voip to the UI Kit
+
+The UI Kit doesn't depend on `cometchat_voip`. It talks to the push ringer through `CometChatCallPushBridge`, which your app implements over `cometchat_voip`. Through the bridge the UI Kit:
+
+- joins every call answered on the ring screen or CallKit, on its own call screen (`answered`), including a call that started the app;
+- forgets a call that stopped ringing, and closes the call screen of a call ended from CallKit (`ended`);
+- stops the ring of a call it declines itself, for example as busy (`dismissRinging`);
+- on iOS, reports the calls it places, joins and ends to CallKit.
+
+Copy this bridge into your app:
+
+```dart
+import 'package:cometchat_chat_uikit/cometchat_calls_uikit.dart'
+ show CometChatCallPushBridge, CometChatPushCall, CometChatPushCallEnded;
+import 'package:cometchat_voip/cometchat_voip.dart';
+
+class VoipCallPushBridge extends CometChatCallPushBridge {
+ const VoipCallPushBridge();
+
+ static CometChatPushCall _call(CometChatVoipCall call) => CometChatPushCall(
+ sessionId: call.sessionId,
+ callType: call.callType,
+ receiverType: call.receiverType,
+ callerUid: call.callerUid,
+ callerName: call.callerName,
+ callerAvatarUrl: call.callerAvatarUrl,
+ );
+
+ @override
+ Stream get answered =>
+ CometChatVoip.onCallAnswered.map(_call);
+
+ @override
+ Stream get ended => CometChatVoip.onCallEnded.map(
+ (ended) => CometChatPushCallEnded(
+ call: _call(ended.call),
+ reason: ended.reason.wireName,
+ ),
+ );
+
+ @override
+ Future answer(String sessionId) => CometChatVoip.answer(sessionId);
+
+ @override
+ Future decline(String sessionId) => CometChatVoip.decline(sessionId);
+
+ @override
+ Future dismissRinging(String sessionId, {required bool answered}) =>
+ CometChatVoip.dismissRinging(sessionId, answered: answered);
+
+ @override
+ Future reportOutgoingCall({
+ required String sessionId,
+ required String calleeName,
+ bool hasVideo = false,
+ }) =>
+ CometChatVoip.reportOutgoingCall(
+ sessionId: sessionId,
+ calleeName: calleeName,
+ hasVideo: hasVideo,
+ );
+
+ @override
+ Future reportCallConnected(String sessionId) =>
+ CometChatVoip.reportCallConnected(sessionId);
+
+ @override
+ Future reportCallEnded(String sessionId) =>
+ CometChatVoip.reportCallEnded(sessionId);
+}
+```
+
+
+Extend `CometChatCallPushBridge` rather than implementing it: members added in a later release come with a default, so your bridge keeps compiling.
+
+
+## Step 4: Enable calls in the UI Kit
+
+Set `enableCalls`, give the UI Kit the bridge, and give `MaterialApp` the UI Kit's navigator key so the UI Kit can open its call screens from anywhere, including for a call answered while the app was killed.
+
+```dart
+UIKitSettings uiKitSettings = (UIKitSettingsBuilder()
+ ..subscriptionType = CometChatSubscriptionType.allUsers
+ ..autoEstablishSocketConnection = true
+ ..appId = "APP_ID"
+ ..region = "REGION"
+ ..authKey = "AUTH_KEY"
+ ..enableCalls = true
+ ..callingConfiguration = CallingConfiguration()
+ ..callPushBridge = const VoipCallPushBridge())
+ .build();
+
+CometChatUIKit.init(
+ uiKitSettings: uiKitSettings,
+ onSuccess: (successMessage) {},
+ onError: (e) {},
+);
+```
+
+```dart
+MaterialApp(
+ navigatorKey: CallNavigationContext.navigatorKey,
+ home: YourHomeScreen(),
+)
+```
+
+With `enableCalls`, `CometChatUIKit.init` initializes the Calls SDK and `CometChatUIKit.login` logs the user into it. Initialize and log in at app start from stored credentials, not only after a screen opens: an answer can start the app, and the UI Kit joins it as soon as the user is logged in.
+
+## Step 5: Hand call pushes to cometchat_voip
+
+On Android, pass every push to `cometchat_voip`, from both the foreground and the background handler. It returns `true` for a call push; anything else is yours to handle (chat notifications, for example).
+
+```dart
+// Foreground: FirebaseMessaging.onMessage.listen(onForegroundMessage)
+Future onForegroundMessage(RemoteMessage message) async {
+ bool isCallPush = await CometChatVoip.handlePushNotification(
+ message.data,
+ sentTime: message.sentTime,
+ );
+ if (!isCallPush) {
+ // A chat notification: handle it as you do today.
+ }
+}
+
+// Background and killed: FirebaseMessaging.onBackgroundMessage(onBackgroundMessage)
+@pragma('vm:entry-point')
+Future onBackgroundMessage(RemoteMessage message) async {
+ await Firebase.initializeApp();
+ if (!CometChatVoip.isInitialized) {
+ await initPushRinger(); // Step 2
+ }
+ await CometChatVoip.handlePushNotification(
+ message.data,
+ sentTime: message.sentTime,
+ );
+}
+```
+
+Don't skip call pushes when the app is in the foreground: the push is the only incoming call signal, in every app state. Call pushes must be sent at high priority. On iOS there's nothing to hand over: VoIP pushes go straight to `cometchat_voip`.
+
+## Step 6: Register the push token
+
+```dart
+// Android: after login
+String? fcmToken = await FirebaseMessaging.instance.getToken();
+if (fcmToken != null) {
+ await CometChatVoip.registerPushToken(token: fcmToken);
+}
+FirebaseMessaging.instance.onTokenRefresh.listen(CometChatVoip.handleTokenRefresh);
+```
+
+On iOS, `cometchat_voip` registers the PushKit token itself when the user logs in.
+
+## Step 7: Permissions and ring readiness
+
+- **Microphone and camera:** `cometchat_voip` asks when the user answers, and the UI Kit's call screen asks before it joins. If the microphone is refused, the call doesn't join and the UI Kit shows a notice that offers the app settings (or calls your `onError`).
+- **Ring readiness:** some device settings silence a call with no error anywhere (notifications off, a muted call channel, full-screen alerts not allowed, Do Not Disturb). Check once after login and tell the user:
+
+```dart
+CometChatRingReadiness readiness = await CometChatVoip.checkRingReadiness();
+if (readiness != CometChatRingReadiness.ready) {
+ // Explain why, then open the right settings page:
+ await CometChatVoip.openSettings(readiness);
+}
+```
+
+## Step 8: Log out through the UI Kit
+
+```dart
+await CometChatVoip.unregisterPushToken();
+CometChatUIKit.logout(
+ onSuccess: (s) {
+ // Go to your login screen
+ },
+ onError: (e) {},
+);
+```
+
+Unregister the push token first, while the user's session still works. `CometChatUIKit.logout` then declines a call still ringing, cancels an outgoing call still ringing, leaves a call in progress, and logs out of the Chat SDK and the Calls SDK. `CometChat.logout` alone does none of the calling clean-up.
+
+## What the UI Kit does from here
+
+Once calling is enabled, the voice and video call buttons appear in the [Message Header](/ui-kit/flutter/message-header).
+
+
+
+
+
+| Moment | What happens | Component |
+|--------|--------------|-----------|
+| Tap a call button | The call is placed, then the outgoing call screen opens | [Call Buttons](/ui-kit/flutter/call-buttons) |
+| Waiting | "Calling…", then "Ringing…" when the callee's phone is reached; End cancels the call; 45 seconds with no answer ends it as unanswered | [Outgoing Call](/ui-kit/flutter/outgoing-call) |
+| Callee declines, is busy or doesn't answer | The outgoing call screen closes | Outgoing Call |
+| Incoming call | `cometchat_voip` rings and draws the ring screen (CallKit on iOS) | `cometchat_voip` (no UI Kit component) |
+| Answer | The UI Kit's call screen opens and joins | Call screen |
+| A call during a 1:1 call | Rings over it; answering ends the current call and joins the new one | Call screen |
+| Hang-up | The session is left; the other side's screen closes | Call screen |
+| Call history | Call logs list from the Calls SDK; the call icon calls back | [Call Logs](/ui-kit/flutter/call-logs) |
+| Group call | The group call button sends a meeting message and joins the conference; members join from the message | Call Buttons |
+
+To react to calls in your own code, listen to the UI Kit's [call events](/ui-kit/flutter/events#call-events), or to the Calls SDK's `CometChatCalls.addIncomingCallListener`. Don't use the Chat SDK's `CallListener`: it is superseded and fires nothing in this flow on Android and iOS.
+
+
+**Web** has no push ringing: there, the UI Kit keeps ringing calls with the Chat SDK and its own [Incoming Call](/ui-kit/flutter/incoming-call) screen. `callPushBridge` is not used on web.
+
+
+## Upgrading from an app that used `CometChat.addCallListener`
+
+Remove the `CallListener` you registered to show the UI Kit's incoming call screen, and any push, PushKit, CallKit or `flutter_callkit_incoming` code you wrote to ring calls in the background, then follow the steps above. The UI Kit no longer re-exports `CallListener` and `CallListener2`; import them from `cometchat_sdk` if you still use them on web. The method-by-method mapping is on [Migrating from Legacy Ringing](/calls/flutter/migrating-from-legacy-ringing).
diff --git a/ui-kit/flutter/events.mdx b/ui-kit/flutter/events.mdx
index 257b52516..bc326e1be 100644
--- a/ui-kit/flutter/events.mdx
+++ b/ui-kit/flutter/events.mdx
@@ -285,6 +285,19 @@ answered through the kit's calling widgets.
3. `ccCallRejected`: Triggered when the logged-in user rejects an incoming call.
4. `ccCallEnded`: Triggered when a call the logged-in user was on has ended.
+On Android and iOS, where incoming calls ring through `cometchat_voip` (see [Calling Integration](/ui-kit/flutter/calling-integration)):
+
+| Moment | Event |
+|---|---|
+| A call is placed from the UI Kit | `ccOutgoingCall` |
+| This device cancels a call it placed, or it rings out | `ccCallRejected` |
+| A call still ringing here is declined by a logout | `ccCallRejected` |
+| A joined call ends on this device | `ccCallEnded` |
+| The callee answers or declines (caller side) | None: the outgoing call screen opens the call screen or closes |
+| The user answers or declines on `cometchat_voip`'s ring screen or CallKit | None: on answer, the UI Kit opens its call screen |
+
+`ccCallAccepted` fires only from the deprecated [Incoming Call](/ui-kit/flutter/incoming-call) screen, which the UI Kit shows on web. To follow calls ringing on this device, listen to the Calls SDK's `CometChatCalls.addIncomingCallListener` or to `cometchat_voip`'s `onCallAnswered` and `onCallEnded`.
+
```dart
diff --git a/ui-kit/flutter/getting-started.mdx b/ui-kit/flutter/getting-started.mdx
index ffe22e643..aee07f3b7 100644
--- a/ui-kit/flutter/getting-started.mdx
+++ b/ui-kit/flutter/getting-started.mdx
@@ -182,6 +182,10 @@ CometChatUIKit.init(
> **V6 difference from V5:** No `extensions` or `aiFeature` parameters needed. Extensions are built-in and handled automatically by `MessageTemplateUtils`.
+> **Calling:** voice and video calls need `enableCalls = true` here, plus push ringing with `cometchat_voip` for incoming calls, and a UI Kit newer than 6.2.0. See [Calling Integration](/ui-kit/flutter/calling-integration).
+
+{/* TODO(RC): bump the ^6.2.0 pins above to the release that ships ringing, matching ui-kit/flutter/calling-integration.mdx. */}
+
***
### **Step 4: Login to UI Kit**
@@ -388,5 +392,6 @@ Scaffold(
* **[Integrate One-to-One Chat](/ui-kit/flutter/flutter-one-to-one-chat)**
* **[Integrate Tab-Based Chat](/ui-kit/flutter/flutter-tab-based-chat)**
* **[Advanced Customizations](/ui-kit/flutter/theme-introduction)**
+* **[Add Voice and Video Calling](/ui-kit/flutter/calling-integration)**
***
diff --git a/ui-kit/flutter/incoming-call.mdx b/ui-kit/flutter/incoming-call.mdx
index beffbae16..64cc3aba5 100644
--- a/ui-kit/flutter/incoming-call.mdx
+++ b/ui-kit/flutter/incoming-call.mdx
@@ -1,6 +1,6 @@
---
title: "Incoming Call"
-description: "Handle incoming CometChat Flutter UI Kit audio and video calls with full-screen caller info, accept, reject, and navigation setup."
+description: "Deprecated incoming call screen, shown on web only; on Android and iOS incoming calls ring through cometchat_voip."
---
@@ -21,13 +21,17 @@ description: "Handle incoming CometChat Flutter UI Kit audio and video calls wit
-`CometChatIncomingCall` displays a full-screen overlay when an incoming call is received, showing caller info with accept and reject buttons.
+
+**Deprecated.** On Android and iOS, incoming calls ring through `cometchat_voip`, which draws its own ring screen (CallKit on iOS) in every app state, including when the app is killed. The UI Kit no longer shows `CometChatIncomingCall` there; it shows it on **web only**, where calls still ring through the Chat SDK. `CometChatIncomingCall`, `IncomingCallOverlay` and `CometChatIncomingCallStyle` are deprecated. New integrations need no incoming call component: see [Calling Integration](/ui-kit/flutter/calling-integration).
+
+
+`CometChatIncomingCall` displays a full-screen overlay when an incoming call is received on web, showing caller info with accept and reject buttons.
---
## Where It Fits
-Typically triggered automatically when `CallNavigationContext.navigatorKey` is set in your `MaterialApp`.
+On web, shown automatically when `CallNavigationContext.navigatorKey` is set in your `MaterialApp`. On Android and iOS, the ring screen is `cometchat_voip`'s; see [Your own ring screen](/calls/flutter/voip-calling#your-own-ring-screen) to draw it yourself.
diff --git a/ui-kit/flutter/methods.mdx b/ui-kit/flutter/methods.mdx
index c020775c8..efd736b07 100644
--- a/ui-kit/flutter/methods.mdx
+++ b/ui-kit/flutter/methods.mdx
@@ -38,6 +38,9 @@ The `UIKitSettings` is an important parameter of the `init()` function. It funct
| subscriptionType | String | Sets subscription type for tracking the presence of all users |
| roles | String | Sets subscription type for tracking the presence of users with specified roles |
| autoEstablishSocketConnection | Boolean | Configures if web socket connections will be established automatically on app initialization or be done manually, set to true by default |
+| enableCalls | Boolean | Turns on calling: `init()` initializes the Calls SDK and `login()` logs the user into it. Default `false`. See [Calling Integration](/ui-kit/flutter/calling-integration) |
+| callingConfiguration | CallingConfiguration | Settings for the call buttons, the outgoing call screen and group calls |
+| callPushBridge | CometChatCallPushBridge | Connects the UI Kit to the app's push ringer (`cometchat_voip`), so calls answered on the ring screen join on the UI Kit's call screen. Required for incoming calls on Android and iOS; not used on web |
> **V6 Note:** The `extensions` and `aiFeature` parameters from V5 have been removed. Extensions are built-in and handled automatically by `MessageTemplateUtils`. No registration is needed.
@@ -93,6 +96,8 @@ CometChatUIKit.loginWithAuthToken("authToken", onSuccess: (user) {
Before a new user logs in, it is crucial to clean session data to avoid potential conflicts. This can be achieved by invoking the `.logout()` function.
+With calls enabled, `logout()` first declines a call still ringing, cancels an outgoing call still ringing and leaves a call in progress, then logs out of the Chat SDK and the Calls SDK. Unregister the push token with `CometChatVoip.unregisterPushToken()` before calling it. Use `CometChatUIKit.logout`, not `CometChat.logout`, which does none of the calling clean-up.
+
```dart
CometChatUIKit.logout(onSuccess: (s) {
// Logged out successfully
diff --git a/ui-kit/flutter/outgoing-call.mdx b/ui-kit/flutter/outgoing-call.mdx
index 8fb5cf70b..86fccfa7d 100644
--- a/ui-kit/flutter/outgoing-call.mdx
+++ b/ui-kit/flutter/outgoing-call.mdx
@@ -21,7 +21,9 @@ description: "Full-screen view displaying recipient information and call status
-`CometChatOutgoingCall` manages the outgoing call process. It displays recipient information (avatar, name) and call status, and automatically transitions to the ongoing call screen when the receiver accepts.
+`CometChatOutgoingCall` shows the outgoing call screen from the moment a call is placed until the callee joins, declines, is busy or doesn't answer. It displays the recipient's avatar and name, the call status ("Calling…", then "Ringing…" once the callee's phone is reached) and an end call button, and opens the call screen when the callee joins.
+
+On Android and iOS, the screen follows the Calls SDK session that the call prepared, without joining it: the caller joins only once the callee has joined. The Chat SDK is not involved.
@@ -31,7 +33,20 @@ description: "Full-screen view displaying recipient information and call status
## Where It Fits
-`CometChatOutgoingCall` is typically launched automatically by `CometChatCallButtons` when a call is initiated. It can also be launched manually for custom call flows.
+`CometChatOutgoingCall` is typically launched automatically by `CometChatCallButtons` and the call back button of `CometChatCallLogs` when a call is placed. It can also be launched manually for custom call flows.
+
+---
+
+## Call states
+
+| State | Subtitle | What happened |
+|---|---|---|
+| Calling | "Calling…" | The call was placed; CometChat is pushing it to the callee |
+| Ringing | "Ringing…" | The callee's phone received the push and is ringing |
+| Accepted | — | The callee joined. The screen opens the call screen and the caller joins |
+| Ended | — | The call ended without an answer: declined, busy, unanswered or cancelled. The screen closes |
+
+A call nobody answers ends after 45 seconds: the callee's `cometchat_voip` stops ringing, and this screen ends the call as `unanswered`. Tapping End cancels the call (`cancelled`): the callee's ring stops and a missed call is shown there. After End, an answer that arrives late is not joined.
---
@@ -98,7 +113,7 @@ Prerequisites: CometChat SDK initialized, `enableCalls = true` set in UIKitSetti
#### `onCancelled`
-Fires when the user cancels the outgoing call. Override to customize cancel behavior.
+Fires when the user taps End. Default: cancels the call with the `cancelled` status, closes the screen and emits `ccCallRejected`. Provide the callback to handle the cancel yourself.
@@ -138,8 +153,10 @@ The component emits events via `CometChatCallEvents`:
| Event | Description |
|---|---|
-| `ccCallAccepted` | Triggers when the outgoing call is accepted |
-| `ccCallRejected` | Triggers when the outgoing call is rejected |
+| `ccOutgoingCall` | The call was placed |
+| `ccCallRejected` | This device ended the call before it was answered: End tapped, or no answer in 45 seconds |
+
+On Android and iOS, no event fires when the callee answers or declines: the screen opens the call screen or closes. A call that was answered and then ends fires `ccCallEnded`.
@@ -158,8 +175,8 @@ class _YourScreenState extends State with CometChatCallEventListener
}
@override
- void ccCallAccepted(Call call) {
- debugPrint("Call accepted: ${call.sessionId}");
+ void ccOutgoingCall(Call call) {
+ debugPrint("Call placed: ${call.sessionId}");
}
@override
@@ -365,8 +382,8 @@ CometChatOutgoingCall(
## Next Steps
-
- Handle incoming calls
+
+ Ring incoming calls with cometchat_voip
Add voice and video call buttons
diff --git a/ui-kit/flutter/troubleshooting.mdx b/ui-kit/flutter/troubleshooting.mdx
index 105c84bfb..19be5ff7c 100644
--- a/ui-kit/flutter/troubleshooting.mdx
+++ b/ui-kit/flutter/troubleshooting.mdx
@@ -61,8 +61,14 @@ description: "Troubleshoot CometChat Flutter UI Kit V6 setup, login, theming, st
| Symptom | Cause | Fix |
|---|---|---|
| Call buttons not appearing | Calls not enabled | Ensure `enableCalls = true` is set in your `UIKitSettingsBuilder` |
-| Incoming call screen not showing | Call listener not registered | Ensure `CometChat.addCallListener()` is called in your app initialization |
-| Call connects but no audio/video | Missing permissions | Request camera and microphone permissions before initiating calls |
+| Incoming calls never ring (Android) | Pushes not handed to `cometchat_voip`, or it isn't initialized in the background handler | Pass every push to `CometChatVoip.handlePushNotification` from `onMessage` and `onBackgroundMessage`, and initialize the Calls SDK and `cometchat_voip` in the background handler. See [Calling Integration](/ui-kit/flutter/calling-integration) |
+| Incoming calls never ring (iOS) | Push Notifications or the Voice over IP background mode missing, or another `PKPushRegistry` in the app | Add both in Xcode and remove your own PushKit code. See [Push Ringing](/calls/flutter/voip-calling) |
+| Rings on one phone but not on another | Device settings silence the ring | Run `CometChatVoip.checkRingReadiness()` after login and show any result other than `ready` |
+| Answer opens the app but no call screen appears | No `callPushBridge` in `UIKitSettings`, no `CallNavigationContext.navigatorKey` on `MaterialApp`, or the user isn't logged in at app start | Set the bridge and the navigator key, and initialize and log in at app start from stored credentials |
+| Caller stays on "Calling…" and never sees "Ringing…" | The callee's phone wasn't reached | Check the callee's push setup and ring readiness; the call ends as unanswered after 45 seconds |
+| Two ring screens or two ringtones | An old `CometChat.addCallListener` showing the incoming call screen, or your own CallKit or `flutter_callkit_incoming` code | Remove them; `cometchat_voip` rings. See [Migrating from Legacy Ringing](/calls/flutter/migrating-from-legacy-ringing) |
+| Calls still ring after logout | The push token wasn't unregistered | Call `CometChatVoip.unregisterPushToken()` before `CometChatUIKit.logout` |
+| Answered call closes at once | The microphone (or camera) was refused | The UI Kit shows a notice that opens the app settings; grant the permission and call again |
---
diff --git a/ui-kit/flutter/upgrading-from-v5.mdx b/ui-kit/flutter/upgrading-from-v5.mdx
index c5f036529..8d5630819 100644
--- a/ui-kit/flutter/upgrading-from-v5.mdx
+++ b/ui-kit/flutter/upgrading-from-v5.mdx
@@ -114,6 +114,23 @@ BlocProvider(
| `CometChatMessageHeaderController` | `MessageHeaderBloc` |
| `CometChatMessageListController` | `MessageListBloc` |
+### 6. Calling and ringing
+
+Incoming calls on Android and iOS now ring through `cometchat_voip` instead of the Chat SDK's `CallListener` and the UI Kit's incoming call screen.
+
+| Before | Now |
+|---|---|
+| `CometChat.addCallListener(...)` to show `CometChatIncomingCall` | Remove it. `cometchat_voip` rings and draws the ring screen in every app state |
+| Your own FCM, PushKit or CallKit code (or `flutter_callkit_incoming`) to ring calls in the background | Remove it. Hand every push to `CometChatVoip.handlePushNotification` |
+| `CometChatIncomingCall` as the incoming call UI | Deprecated; shown on web only |
+| Nothing connecting the push ringer to the UI Kit | `callPushBridge` in `UIKitSettings` (a short class over `cometchat_voip`) |
+| `CallListener` / `CallListener2` from the UI Kit's barrels | No longer re-exported; import them from `cometchat_sdk` if you still need them |
+| `CometChat.logout` | `CometChatVoip.unregisterPushToken()`, then `CometChatUIKit.logout` |
+
+Full walkthrough: [Calling Integration](/ui-kit/flutter/calling-integration) and [Migrating from Legacy Ringing](/calls/flutter/migrating-from-legacy-ringing).
+
+---
+
## Migration Checklist
1. ☐ Replace all `CometChatXxxController` usage with `XxxBloc`