Skip to main content
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. This page covers the push side of ringing. Starting, answering and ending calls with the Calls SDK is on the Ringing page.

Prerequisites

  • The Calls SDK set up and the user logged in.
  • Push notifications enabled in the CometChat dashboard, with the push providers, Firebase project and Apple credentials set up. See 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 that uses a token-based .p8 key; a certificate provider serves only one of chat and VoIP pushes. Its Bundle ID must be your app’s, and its production setting must match the build: a build run from Xcode gets a sandbox token. Test on a real device; CallKit and VoIP pushes don’t run in the simulator.

Step 1: Add the packages

cometchat_voip brings cometchat_calls_sdk with it; list both so the Calls SDK version is yours to pin. The Calls SDK’s own requirements on both platforms are on Setup.
Set minSdk 26 in android/app/build.gradle. cometchat_voip adds what ringing needs to your manifest: its ring screen, its ringing foreground service (type phoneCall) and the permissions for them, including USE_FULL_SCREEN_INTENT and FOREGROUND_SERVICE_PHONE_CALL, which Google Play asks you to justify.It doesn’t add the Calls SDK’s ongoing call service. Your app declares that one, as described in Background Handling, or the microphone and camera stop when the answered call goes to the background.
Don’t create a PKPushRegistry or a CXProvider of your own on iOS. A second registry takes the VoIP pushes, and those calls never ring, with no error. An app that needs to own PushKit uses manual mode.

Step 2: Initialize

Initialize the Calls SDK and then cometchat_voip early in main, on every launch: a push that launched a killed app waits for Dart before the Calls SDK can connect the ring, and iOS doesn’t wait for long. Build the configuration from the same CallAppSettings you give the Calls SDK, so the app ID and region match.

Step 3: Hand call pushes to cometchat_voip

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.
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.
You don’t initialize anything in the background handler in the auto modes. cometchat_voip saved its configuration when init last ran, and the native side rings from that; if it needs the Calls SDK, for example to send a decline after the app was killed, it starts its own isolate. CometChatVoip.init must have run once on the device, and a call can’t ring before that. In manual mode, where the ring is yours, initialize the Calls SDK and cometchat_voip in the handler first.
Send call pushes at high priority. Android only lets a background app stay alive through the answer when a high-priority push woke it.
On iOS there’s nothing to hand over: VoIP pushes go straight to cometchat_voip. If your app also has chat notifications, see If your app also rings calls.

Step 4: Register the push token

iOS: in both auto modes cometchat_voip owns the PKPushRegistry: 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. There’s nothing for you to pass. Android: cometchat_voip can’t see the FCM token, so hand it over. It keeps the token in memory only, so hand it over on every launch, not just the first: unregisterPushToken and verifyPushSetup need it in this run of the app. unregisterPushToken throws a noPushToken error if it hasn’t been given one.
With autoRegisterPushToken on (the default), a later Calls SDK login in the same run registers the token again. A chat token registered with cometchat_push_notifications can be the same FCM token; CometChat keeps one entry per token.

Step 5: Permissions and ring readiness

Microphone and camera. cometchat_voip asks on Answer, with requestCallPermissions on (the default), and the call joins whatever the user chose. The timing on each platform, and what happens on a locked phone, is under Answer a call. A screen of your own can check first with permissionsToAnswer(call) and ask with requestCallPermissions(call). Nothing asks for the caller: request those permissions before initiateCall. Ring readiness. Some device settings silence a call with no error anywhere. Check once after login and tell the user:
CometChatVoip.verifyPushSetup() checks the rest of the setup and reports ok, notInitialized, notLoggedIn, tokenNotRegistered, tokenRegistrationFailed, notificationsBlocked or ringNotReady. CometChatVoip.getDiagnostics() returns a snapshot that is safe to attach to a bug report: its pushesSeen, pushesRung and pushesDropped counts tell “no push arrived” from “a push arrived and was dropped”.

Receive a call

How an incoming call rings depends on callMode: In every mode, when the user answers, your app joins the call (see Answer a call).

Auto

cometchat_voip does all of the ringing, including its own ring screen. Listen for an answer, and for a call that stops ringing:

Your own ring screen

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.
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.

Manual

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. ringSurfaces is Android only and throws on iOS, where reporting a call to CallKit is the ring.
Report what the user chose with CometChatVoip.accept(sessionId) (fires onCallAnswered) or CometChatVoip.decline(sessionId). accept reports the answer without touching a ring; in the auto modes use answer, which also stops it. 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.
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.

Theming the ring screen

On Android, the ring screen cometchat_voip draws in Auto mode, and its ring, connecting and missed call notifications, take each colour from, in order:
  1. a CometChatVoipIncomingCallTheme set in code;
  2. the cometchat_voip_call_* colour resources, which your app can override by name;
  3. cometchat_voip’s defaults.
Each colour is looked up on its own, so a theme can set only the colours it needs. Without a theme or overrides, the screen is dark in both light and dark mode.

In code

  • Mode. system follows the phone’s light or dark setting when the call rings. If your app has its own light and dark switch, pass light or dark, and set the theme again when the user switches.
  • Saved. The theme is saved when you pass it, so it also applies when a push starts your app from killed.
  • Kept across init. An init without a theme, such as the one in your background message handler, keeps the saved theme. Remove it with CometChatVoip.setIncomingCallTheme(null).
  • Last one wins. Between incomingCallTheme and setIncomingCallTheme, the theme set last is used.
  • Only what you set. A colour you leave out comes from its resource. In a light set, it comes from built-in light colours instead, so a light page never gets the dark default’s light text.
  • Where it applies. In Auto mode, the ring screen and the notifications. With your own ring screen, the screen is yours, so only the notifications. On iOS, CallKit draws the call and the theme does nothing.

As resources

Override any of these by name in your app’s android/app/src/main/res/values/colors.xml, and in res/values-night/colors.xml for dark mode. Your app’s resource wins over cometchat_voip’s. Each one matches a field of CometChatVoipIncomingCallColors.
android/app/src/main/res/values/colors.xml
On Android 12 and later, the Answer and Decline buttons in the notification are drawn by the system, which treats your colours as a hint. To change more than the colours, show your own Flutter screen with Your own ring screen.

Incoming call events

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. CometChatVoip.onCallRinging fires for a call that starts ringing, in every mode. Don’t use the listener id cometchat_voip: the package registers under it, and ids are keys, so a second listener with that id replaces the first.

How a ring ends

Every way out is handled by cometchat_voip and needs no code in your app. The caller’s column depends on a push reaching the caller; see Listen for calls ending. CometChatCallEndReason.fromWire(reason) turns a status into the same values onCallEnded reports, if you want one vocabulary. It maps both busy and rejected to declined, so compare the status itself to tell them apart.

A call while in a call

Call waiting, the way the phone app does it. “In a call” means the Calls SDK is joined to a session. In Manual mode, your app decides how to handle these calls.

Calls this device places (iOS)

A call this device places is on CallKit too, so it survives a cellular call, can be ended from the lock screen and appears in Recents. cometchat_voip reports a session the Calls SDK owns by itself, under your app’s name until a participant connects. Report it yourself to give CallKit the callee’s name and the call type from the start:
CallKit’s own End button ends the CallKit call and nothing else, so listen for it. While the callee still rings, withdraw the call, or they go on ringing:

Reporting the session

cometchat_voip follows the Calls SDK’s session and media events itself, so CallKit’s timer, mute and hold follow the call without you doing anything. These calls are optional, for a session it never saw ring:
reason is what CallKit records for the call: a CometChatCallEndReason, not a call status, so there is no ended. Android ignores it.

A screen that already answers calls

If your app answers and declines through CometChat from its own screen, such as an overlay driven by the Chat SDK’s calling, stop cometchat_voip’s ring without sending anything a second time:

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 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, Android asks for any missing microphone and camera permission and opens your app over the lock screen; iOS answers in CallKit and asks once the app is active. onCallAnswered fires as soon as your code listens. Join the call there, without waiting for your UI.
  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 subscribe to onCallAnswered, at app start, not only after a screen opens, so an answered call finds a ready SDK. The Calls SDK restores its login from disk.

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. Logging out stops a call still ringing for that user.

Troubleshooting