diff --git a/calls/flutter/voip-calling.mdx b/calls/flutter/voip-calling.mdx index b5be9ca57..9804b611a 100644 --- a/calls/flutter/voip-calling.mdx +++ b/calls/flutter/voip-calling.mdx @@ -38,7 +38,7 @@ Before implementing VoIP calling, ensure you have: - [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 + - [APNs](/notifications/ios-push-notifications) for iOS - [Push notifications enabled](/notifications/push-overview) in CometChat Dashboard diff --git a/docs.json b/docs.json index 316b5d28f..b58f37e78 100644 --- a/docs.json +++ b/docs.json @@ -6479,22 +6479,26 @@ { "tab": "Push", "pages": [ - "notifications/push-overview", { - "group": "Getting Started", + "group": "Overview", + "pages": [ + "notifications/push-overview", + "notifications/push-getting-started" + ] + }, + { + "group": "Platform Integrations", "pages": [ "notifications/android-push-notifications", - "notifications/ios-apns-push-notifications", - "notifications/ios-fcm-push-notifications", - "notifications/flutter-push-notifications-android", - "notifications/flutter-push-notifications-ios", + "notifications/ios-push-notifications", + "notifications/flutter-push-notifications", "notifications/react-native-push-notifications-android", "notifications/react-native-push-notifications-ios", "notifications/web-push-notifications" ] }, { - "group": " ", + "group": "Settings", "pages": [ "notifications/preferences", "notifications/templates-and-sounds", @@ -7096,15 +7100,31 @@ }, { "source": "/extensions/ios-fcm-push-notifications", - "destination": "/notifications/ios-fcm-push-notifications" + "destination": "/notifications/ios-push-notifications" }, { "source": "/extensions/ios-apns-push-notifications", - "destination": "/notifications/ios-apns-push-notifications" + "destination": "/notifications/ios-push-notifications" + }, + { + "source": "/notifications/ios-apns-push-notifications", + "destination": "/notifications/ios-push-notifications" + }, + { + "source": "/notifications/ios-fcm-push-notifications", + "destination": "/notifications/ios-push-notifications" }, { "source": "/extensions/flutter-push-notifications", - "destination": "/notifications/flutter-push-notifications-android" + "destination": "/notifications/flutter-push-notifications" + }, + { + "source": "/notifications/flutter-push-notifications-android", + "destination": "/notifications/flutter-push-notifications" + }, + { + "source": "/notifications/flutter-push-notifications-ios", + "destination": "/notifications/flutter-push-notifications" }, { "source": "/extensions/react-native-push-notifications", @@ -8454,10 +8474,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" diff --git a/notifications.mdx b/notifications.mdx index 0eb31ff02..c478083cb 100644 --- a/notifications.mdx +++ b/notifications.mdx @@ -61,11 +61,9 @@ canonical: "https://cometchat.com/docs" } href="/notifications/android-push-notifications" horizontal /> - } href="/notifications/ios-apns-push-notifications" horizontal /> - } href="/notifications/ios-fcm-push-notifications" horizontal /> + } href="/notifications/ios-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/android-push-notifications.mdx b/notifications/android-push-notifications.mdx index 017536b07..6f5580973 100644 --- a/notifications/android-push-notifications.mdx +++ b/notifications/android-push-notifications.mdx @@ -1,731 +1,276 @@ --- -title: "Android Push Notifications" -description: "Setup FCM and CometChat for message and call push notifications on Android." +title: "Android" +description: "Add CometChat push notifications and VoIP calls to an Android app with the drop-in cometchat push-notifications-android SDK." --- - - - Reference implementation of Kotlin UI Kit, FCM and Push Notification Setup. - ## What this guide covers -- FCM setup and CometChat provider wiring (credentials + Gradle + manifest). -- Token registration/unregistration so CometChat routes pushes correctly. -- Handling message pushes with grouped notifications and inline reply. -- Handling call pushes with `ConnectionService` for native telecom UI. -- Deep links/navigation from notifications and payload customization. -- App icon badge count and grouped notifications using `unreadMessageCount` from the CometChat push payload. - -{/* ## What you need first - -- Firebase project with an Android app added, `google-services.json` downloaded, and Cloud Messaging enabled. -- CometChat App ID, Region, Auth Key; **Push Notifications** enabled with an **FCM Android provider** and its Provider ID. -- Android device with Play Services (sample uses `minSdk 26` because of `ConnectionService`). -- Latest CometChat UI Kit + Calls SDK dependencies (see Gradle tabs below). */} - -## How FCM and CometChat fit together - -- **Why FCM?** Google issues device tokens and delivers raw push payloads to Android. You must add `google-services.json`, the Messaging SDK, and a service receiver (`FCMService`) so the device can receive pushes. -- **Why a CometChat provider?** The Provider ID tells CometChat which FCM credentials to use when sending to your app. Without registering tokens against this ID, CometChat cannot target your device. -- **Token registration bridge:** The app retrieves the FCM token and calls `CometChatNotifications.registerPushToken(pushToken, PushPlatforms.FCM_ANDROID, providerId, …)`. That binds the token to your logged-in user so CometChat can route message/call pushes to FCM on your behalf. -- **Payload handling:** When FCM delivers a push, your `FCMService`/`FCMMessageBroadcastReceiver` parses CometChat’s payload, shows notifications (grouped, inline reply), and forwards intents to your activities. For calls, `CometChatVoIPConnectionService` surfaces a telecom-grade UI and uses the same payload to accept/reject server-side. -- **Dashboard ↔ app contract:** The Provider ID in `AppConstants.FCMConstants.PROVIDER_ID` must match the dashboard provider you created. The package name in Firebase and the `applicationId` in Gradle must match, or FCM will reject the token. - -## 1. Prepare Firebase and CometChat - -1. **Firebase Console**: Add your Android package, download `google-services.json` into the app module, and enable Cloud Messaging. - - - Firebase - Push Notifications - - -2. **CometChat dashboard**: Go to **Notifications → Settings**, enable **Push Notifications**, click **Add Credentials** (FCM), upload the Firebase service account JSON (Project settings → Service accounts → Generate new private key), and copy the Provider ID. - - - Enable Push Notifications - - -From the same screen, click **Add Provider** to upload the Firebase service account JSON. This is how you can Generate a new private key from Firebase: - - - Upload FCM service account JSON - - -3. **App constants**: -Note down your CometChat App ID, Auth Key, and Region from the CometChat dashboard and keep them available to be added to your project's AppCrendentials.kt. -Similarly, note the FCM Provider ID generated in the CometChat dashboard and add the same in your AppConstants.kt; this will be when registering the FCM token with CometChat. - -## 2. Add dependencies (Gradle) - -Use a version catalog and aliases (Update `applicationId`, package names, icons, and app name.). Also, if you are new to CometChat, please review the Maven repositories and related setup requirements before proceeding. - - - - -```toml lines -[versions] -minSdk = "26" -compileSdk = "35" -targetSdk = "35" -agp = "8.7.0" -kotlin = "2.0.0" -googleServices = "4.4.2" -cometChatUikit = "5.2.6" -cometChatSdk = "4.1.8" -cometChatCalls = "4.3.2" -firebaseBom = "33.7.0" -coreKtx = "1.13.1" -appcompat = "1.7.0" -material = "1.12.0" -gson = "2.11.0" -glide = "4.16.0" - -[libraries] -cometchat-uikit = { group = "com.cometchat", name = "chat-uikit", version.ref = "cometChatUikit" } -cometchat-sdk = { group = "com.cometchat", name = "chat-sdk-android", version.ref = "cometChatSdk" } -cometchat-calls = { group = "com.cometchat", name = "calls-sdk-android", version.ref = "cometChatCalls" } -firebase-bom = { group = "com.google.firebase", name = "firebase-bom", version.ref = "firebaseBom" } -firebase-messaging = { group = "com.google.firebase", name = "firebase-messaging" } -firebase-auth = { group = "com.google.firebase", name = "firebase-auth" } -androidx-core-ktx = { group = "androidx.core", name = "core-ktx", version.ref = "coreKtx" } -androidx-appcompat = { group = "androidx.appcompat", name = "appcompat", version.ref = "appcompat" } -material = { group = "com.google.android.material", name = "material", version.ref = "material" } -gson = { group = "com.google.code.gson", name = "gson", version.ref = "gson" } -glide = { group = "com.github.bumptech.glide", name = "glide", version.ref = "glide" } - -[plugins] -android-application = { id = "com.android.application", version.ref = "agp" } -kotlin-android = { id = "org.jetbrains.kotlin.android", version.ref = "kotlin" } -google-services = { id = "com.google.gms.google-services", version.ref = "googleServices" } -``` - -This TOML file defines versions and aliases for the required dependencies. - - +- Adding the `push-notifications-android` SDK and initializing it. +- Wiring your own Firebase Messaging service to forward payloads to the SDK. +- Requesting permission and registering the FCM token after login. +- Letting the SDK render chat notifications (grouped, inline reply) and full-screen VoIP calls. +- Handling notification taps and call events, and suppressing notifications for the open chat. +- Testing and troubleshooting. -```gradle lines -plugins { - alias(libs.plugins.android.application) - alias(libs.plugins.kotlin.android) - alias(libs.plugins.google.services) -} - -android { - compileSdk 35 - defaultConfig { - applicationId "your.package.name" - minSdk 26 - targetSdk 35 - } - kotlinOptions { jvmTarget = "11" } -} - -dependencies { - // CometChat - implementation(libs.cometchat.uikit) - implementation(libs.cometchat.sdk) - implementation(libs.cometchat.calls) - - // Firebase - implementation(platform(libs.firebase.bom)) - implementation(libs.firebase.messaging) - implementation(libs.firebase.auth) - - // UI + utilities - implementation(libs.androidx.core.ktx) - implementation(libs.androidx.appcompat) - implementation(libs.material) - implementation(libs.gson) - implementation(libs.glide) -} -``` - - - - -- Apply the `google-services` plugin and place `google-services.json` in the same module; keep `viewBinding` enabled if you copy UI Kit screens directly from the sample. -- Update `applicationId`, package names, icons, and app name as needed. - -## 3. Manifest permissions and services - -Start from the sample [`AndroidManifest.xml`](https://github.com/cometchat/cometchat-uikit-android/blob/v5/sample-app-kotlin%2Bpush-notification/src/main/AndroidManifest.xml): - -```xml lines highlight={15, 19, 26, 29} - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -``` + +The `push-notifications-android` SDK replaces the previous approach of copying the UI Kit sample's `fcm/` and `voip/` packages (`FCMService`, `FCMMessageNotificationUtils`, `FCMMessageBroadcastReceiver`, `CometChatVoIP`, `CometChatVoIPConnectionService`, and related helpers). Token registration, notification channels, stacking, avatars, inline reply, delivery receipts, the VoIP ConnectionService, the incoming-call screen, ring timeout, call-collision handling, and SDK auto-init on a killed-app wake are all handled inside the SDK. Your app keeps only a ~5-line Firebase Messaging service and three facade calls. + -- Permissions cover notifications + telecom; services/receiver wire Firebase delivery (`FCMService`), notification actions (`FCMMessageBroadcastReceiver`), and telecom UI (`CometChatVoIPConnectionService`). Point `android:name` to your `MyApplication`. +## How it works -- Set `android:name` on `` to your `MyApplication` subclass. -- Keep runtime permission prompts for notifications, mic, camera, and media access (see `AppUtils.kt` / `HomeActivity.kt` in the sample). +- **FCM's role:** Firebase issues the registration token and delivers the CometChat payload as a **data message**. The SDK does **not** depend on `firebase-messaging` — your app owns Firebase and passes the payload as a `Map`. +- **CometChat's role:** The FCM provider you add in the dashboard binds your registered token to the logged-in user so CometChat can route pushes on your behalf. +- **The SDK's role:** `CometChatPushNotifications` parses the payload and decides chat vs call. Chat → builds/stacks notifications. Call → drives a full-screen incoming-call experience over an Android Telecom `SELF_MANAGED` `ConnectionService` (the same approach WhatsApp/Signal use — no `READ_PHONE_STATE`/`ANSWER_PHONE_CALLS` and no phone-account toggle in Settings). +- **Killed-app wake:** When FCM starts your process, `init()` in `Application.onCreate()` runs first and stores your credentials; `handlePushNotification(...)` then auto-initializes the Chat SDK before routing, so calls connect even from a terminated state. -## 4. Application wiring, sample code, and callbacks +## Prerequisites -- Clone/open the [reference repo](https://github.com/cometchat/cometchat-uikit-android/tree/v5/sample-app-kotlin%2Bpush-notification). -- Copy into your app module (keep structure): - - [`fcm/fcm`](https://github.com/cometchat/cometchat-uikit-android/tree/v5/sample-app-kotlin%2Bpush-notification/src/main/java/com/cometchat/sampleapp/kotlin/fcm/fcm) for services/DTOs/notification utils/broadcast receiver. - - [`fcm/voip`](https://github.com/cometchat/cometchat-uikit-android/tree/v5/sample-app-kotlin%2Bpush-notification/src/main/java/com/cometchat/sampleapp/kotlin/fcm/voip) for ConnectionService + VoIP helpers. - - `fcm/utils` for `MyApplication`, `AppUtils`, `AppConstants`, `AppCredentials`. - - Copy String values from `res/values/strings.xml`. - - BuildConfig file `build.gradle`. -- Update packages to your namespace; set `AppCredentials` (App ID/Auth Key/Region) and `AppConstants.FCMConstants.PROVIDER_ID` to your dashboard provider. Point `` and services/receivers to your package; update app name/icons as needed. -- Keep notification constants from [`AppConstants.kt`](https://github.com/cometchat/cometchat-uikit-android/blob/v5/sample-app-kotlin%2Bpush-notification/src/main/java/com/cometchat/sampleapp/kotlin/fcm/utils/AppConstants.kt); rename channels/keys consistently if you change them. +- The FCM provider, Firebase project, and `google-services.json` from **[Getting Started](/notifications/push-getting-started)** (this guide assumes those are done). +- The CometChat **Chat SDK / UI Kit** already integrated (you call `CometChatUIKit.init()` / `login()` yourself). +- **minSdk 24+**, **compileSdk 36**, **Java 11**, and a Gradle/AGP version matching your UI Kit project. +- A physical device — background delivery and full-screen calls are unreliable on emulators. -**What the core pieces do** + +**Complete the [Getting Started](/notifications/push-getting-started) guide first** — enable Push Notifications, add your FCM provider, and configure Firebase (`google-services.json` + service account JSON). This guide covers only the Android app wiring. + -- `FCMService` – receives FCM data/notification messages, parses CometChat payload, and hands off to `FCMMessageBroadcastReceiver`. -- `FCMMessageBroadcastReceiver` – builds grouped notifications, inline reply actions, and routes taps/deeplinks to your `HomeActivity`. -- `Repository.registerFCMToken` – fetches the FCM token and registers it with CometChat using `AppConstants.FCMConstants.PROVIDER_ID`; call after login. -- `Repository.acceptCall/rejectCall/rejectCallWithBusyStatus` – performs server-side call actions so the caller sees the correct state even if your UI is backgrounded. -- `MyApplication` – initializes UIKit, manages websocket connect/disconnect, tracks foreground state, and shows/dismisses incoming call overlays. -- `CometChatVoIPConnectionService` – handles Android telecom integration so call pushes display a system-grade incoming call UI and cleanly end/busy on reject. +## 1. Add the dependency -**Splash/entry deep link handler** (adapt activity targets): +Add the CometChat Maven repository (the same one that serves the Chat and Calls SDKs) in `settings.gradle.kts`: -```kotlin lines -// In your Splash/entry activity (e.g., SplashActivity) -override fun onCreate(savedInstanceState: Bundle?) { - super.onCreate(savedInstanceState) - handleDeepLinking() -} - -private fun handleDeepLinking() { - NotificationManagerCompat.from(this) - .cancel(AppConstants.FCMConstants.NOTIFICATION_GROUP_SUMMARY_ID) - - val notificationType = intent.getStringExtra(AppConstants.FCMConstants.NOTIFICATION_TYPE) - val notificationPayload = intent.getStringExtra(AppConstants.FCMConstants.NOTIFICATION_PAYLOAD) - - startActivity( - Intent(this, HomeActivity::class.java).apply { - putExtra(AppConstants.FCMConstants.NOTIFICATION_TYPE, notificationType) - putExtra(AppConstants.FCMConstants.NOTIFICATION_PAYLOAD, notificationPayload) - } - ) - finish() +```kotlin settings.gradle.kts +dependencyResolutionManagement { + repositories { + google() + mavenCentral() + maven("https://dl.cloudsmith.io/public/cometchat/cometchat/maven/") + } } ``` -This reads the push extras, clears the summary notification, and forwards the payload to `HomeActivity` so taps or deep links land in the right screen. - -**SplashViewModel (init UIKit + login check)** - -```kotlin lines -class SplashViewModel : ViewModel() { - private val loginStatus = MutableLiveData() - - fun initUIKit(context: Context) { - val appId = AppUtils.getDataFromSharedPref(context, String::class.java, R.string.app_cred_id, AppCredentials.APP_ID) - val region = AppUtils.getDataFromSharedPref(context, String::class.java, R.string.app_cred_region, AppCredentials.REGION) - val authKey = AppUtils.getDataFromSharedPref(context, String::class.java, R.string.app_cred_auth, AppCredentials.AUTH_KEY) - - val uiKitSettings = UIKitSettings.UIKitSettingsBuilder() - .setAutoEstablishSocketConnection(false) - .setAppId(appId) - .setRegion(region) - .setAuthKey(authKey) - .subscribePresenceForAllUsers() - .build() - - CometChatUIKit.init(context, uiKitSettings, object : CometChat.CallbackListener() { - override fun onSuccess(s: String) { - CometChat.setDemoMetaInfo(getAppMetadata(context)) - checkUserIsNotLoggedIn() - } - override fun onError(e: CometChatException) { - Toast.makeText(context, e.message, Toast.LENGTH_SHORT).show() - } - }) - } +Add the SDK plus your own Firebase Messaging dependency in the app module. Apply the `google-services` plugin so `google-services.json` is picked up: - private fun getAppMetadata(context: Context): JSONObject { - val jsonObject = JSONObject() - jsonObject.put("name", context.getString(R.string.app_name)) - jsonObject.put("bundle", BuildConfig.APPLICATION_ID) - jsonObject.put("version", BuildConfig.VERSION_NAME) - jsonObject.put("platform", "android") - return jsonObject - } +```kotlin app/build.gradle.kts +plugins { + id("com.android.application") + id("org.jetbrains.kotlin.android") + id("com.google.gms.google-services") +} - fun checkUserIsNotLoggedIn() { - loginStatus.value = CometChatUIKit.getLoggedInUser() != null - } +dependencies { + implementation("com.cometchat:push-notifications-android:1.0.0") - fun getLoginStatus(): LiveData = loginStatus + // Your app owns Firebase — the SDK does not depend on it. + implementation(platform("com.google.firebase:firebase-bom:34.15.0")) + implementation("com.google.firebase:firebase-messaging") } ``` -Loads credentials from shared prefs, builds `UIKitSettings`, initializes CometChat UIKit (without auto socket), sets sample metadata, and exposes `loginStatus` so the splash can route to login vs home. - -**Repository (push token + call helpers)** + +You do **not** need to add notification, full-screen-intent, foreground-service, `WAKE_LOCK`, or `MANAGE_OWN_CALLS` permissions, or declare the incoming-call activity / VoIP services in your `AndroidManifest.xml`. The SDK's manifest declares everything it needs and Gradle merges it into your app automatically. + -```kotlin lines -object Repository { - fun registerFCMToken(listener: CometChat.CallbackListener) { /* fetch FCM token and call registerPushToken */ } - fun unregisterFCMToken(listener: CometChat.CallbackListener) { /* call unregisterPushToken */ } +## 2. Store your credentials - fun rejectCallWithBusyStatus( - call: Call, - callbackListener: CometChat.CallbackListener? = null - ) { /* reject with CALL_STATUS_BUSY and notify UIKit */ } +Keep the values from Getting Started where your app can read them: - fun acceptCall( - call: Call, - callbackListener: CometChat.CallbackListener - ) { /* acceptCall and notify UIKit */ } - - fun rejectCall( - call: Call, - callbackListener: CometChat.CallbackListener - ) { /* rejectCall with CALL_STATUS_REJECTED and notify UIKit */ } +```kotlin AppCredentials.kt +object AppCredentials { + const val APP_ID = "YOUR_APP_ID" + const val REGION = "YOUR_REGION" + const val AUTH_KEY = "YOUR_AUTH_KEY" + const val FCM_PROVIDER_ID = "FCM-PROVIDER-ID" } ``` -Thin wrappers that register/unregister FCM tokens with your Provider ID and perform server-side call actions (accept/reject/busy) so the caller sees the correct state even if your UI is backgrounded. +## 3. Initialize the SDK + +Initialize in `Application.onCreate()` so the SDK is ready before any push arrives (including killed-app wakes). Build the config with `PNConfiguration.Builder(appId, region)`: -**MyApplication (push/call lifecycle essentials)** +```kotlin MyApplication.kt +import android.app.Application +import com.cometchat.pushnotification.CometChatPushNotifications +import com.cometchat.pushnotification.PNConfiguration -```kotlin lines class MyApplication : Application() { override fun onCreate() { super.onCreate() - if (!CometChatUIKit.isSDKInitialized()) { - SplashViewModel().initUIKit(this) - } - - FirebaseApp.initializeApp(this) - addCallListener() - registerActivityLifecycleCallbacks(object : ActivityLifecycleCallbacks { - override fun onActivityCreated(activity: Activity, savedInstanceState: Bundle?) { currentActivity = activity } - override fun onActivityStarted(activity: Activity) { - if (activity !is SplashActivity && - CometChatUIKit.isSDKInitialized() && - isConnectedToWebSockets.compareAndSet(false, true) - ) { - CometChat.connect(object : CometChat.CallbackListener() { - override fun onSuccess(s: String?) { isConnectedToWebSockets.set(true) } - override fun onError(e: CometChatException) { isConnectedToWebSockets.set(false) } - }) - } - currentActivity = activity - if (++activityReferences == 1 && !isActivityChangingConfigurations) { - isAppInForeground = true - } - } - override fun onActivityResumed(activity: Activity) { currentActivity = activity } - override fun onActivityPaused(activity: Activity) {} - override fun onActivityStopped(activity: Activity) { - if (activity !is SplashActivity) { - isActivityChangingConfigurations = activity.isChangingConfigurations - if (--activityReferences == 0 && !isActivityChangingConfigurations) { - isAppInForeground = false - if (CometChatUIKit.isSDKInitialized()) { - CometChat.disconnect(object : CometChat.CallbackListener() { - override fun onSuccess(s: String?) { isConnectedToWebSockets.set(false) } - override fun onError(e: CometChatException) {} - }) - } - } - } - } - override fun onActivitySaveInstanceState(activity: Activity, outState: Bundle) {} - override fun onActivityDestroyed(activity: Activity) { if (currentActivity === activity) currentActivity = null } - }) - } + // Initialize the CometChat UI Kit / Chat SDK first (your existing setup) … - private fun addCallListener() { - CometChat.addCallListener(LISTENER_ID, object : CometChat.CallListener() { - override fun onIncomingCallReceived(call: Call) { /* handle call UI or banner */ } - override fun onOutgoingCallAccepted(call: Call) {} - override fun onOutgoingCallRejected(call: Call) {} - override fun onIncomingCallCancelled(call: Call) {} - override fun onCallEndedMessageReceived(call: Call) {} - }) - } + val config = PNConfiguration.Builder(AppCredentials.APP_ID, AppCredentials.REGION) + .setNotificationSmallIcon(R.drawable.ic_notification) + .setVoIPEnabled(true) + .build() - companion object { - var currentOpenChatId: String? = null - var currentActivity: Activity? = null - private var isAppInForeground = false - private val isConnectedToWebSockets = AtomicBoolean(false) - private var activityReferences = 0 - private var isActivityChangingConfigurations = false - private var LISTENER_ID: String = System.currentTimeMillis().toString() - private var tempCall: Call? = null - - fun getTempCall(): Call? = tempCall - fun setTempCall(call: Call?) { - tempCall = call - if (call == null && soundManager != null) { - soundManager?.pauseSilently() - } - } - - fun isAppInForeground(): Boolean = isAppInForeground - var soundManager: CometChatSoundManager? = null + CometChatPushNotifications.init(this, config) } } ``` -Initializes UIKit/Firebase, adds call listeners, manages websocket connect/disconnect tied to app foreground, tracks the current activity, and caches temp call state so banners can reappear after resume. - -State to set at runtime: +Point `` at `MyApplication` in your manifest. -- `isAppInForeground`/`currentActivity` inside lifecycle callbacks. -- `currentOpenChatId` when a chat screen opens; clear on exit to suppress notifications only for the active chat. -- `tempCall` via `setTempCall(...)` when an incoming call arrives; clear on dismiss/end. `getTempCall()` is read on resume to re-show the banner. +## 4. Forward FCM payloads to the SDK -## 5. Application wiring and permissions +Create your own `FirebaseMessagingService`. Forward each data message to `handlePushNotification(...)` and each token to `handleTokenRefresh(...)`: -- [`AppUtils.kt`](https://github.com/cometchat/cometchat-uikit-android/blob/v5/sample-app-kotlin%2Bpush-notification/src/main/java/com/cometchat/sampleapp/kotlin/fcm/utils/AppUtils.kt) + your entry screen (e.g., `HomeActivity`): request notification/mic/camera/storage permissions early. -- In `HomeActivity`, keep the VoIP permission chain and phone-account enablement so call pushes can render the native UI: +```kotlin AppFCMService.kt +import com.cometchat.pushnotification.CometChatPushNotifications +import com.cometchat.pushnotification.models.PushPlatform +import com.google.firebase.messaging.FirebaseMessagingService +import com.google.firebase.messaging.RemoteMessage -```kotlin lines -override fun onCreate(savedInstanceState: Bundle?) { - super.onCreate(savedInstanceState) - AppUtils.requestNotificationPermission(this) - configureVoIP() - handleDeepLinking() // open chats based on NOTIFICATION_TYPE/NOTIFICATION_PAYLOAD -} - -private fun configureVoIP() { - CometChatVoIP.init(this, applicationInfo.loadLabel(packageManager).toString()) - launchVoIP() -} - -private fun launchVoIP() { - if (!CometChatVoIP.hasReadPhoneStatePermission(this)) { - CometChatVoIP.requestReadPhoneStatePermission(this, CometChatVoIPConstant.PermissionCode.READ_PHONE_STATE) - return - } - if (!CometChatVoIP.hasManageOwnCallsPermission(this)) { - CometChatVoIP.requestManageOwnCallsPermission(this, CometChatVoIPConstant.PermissionCode.MANAGE_OWN_CALLS) - return - } - if (!CometChatVoIP.hasAnswerPhoneCallsPermission(this)) { - CometChatVoIP.requestAnswerPhoneCallsPermission(this, CometChatVoIPConstant.PermissionCode.ANSWER_PHONE_CALLS) - return +class AppFCMService : FirebaseMessagingService() { + override fun onMessageReceived(message: RemoteMessage) { + CometChatPushNotifications.handlePushNotification(this, data = message.data) } - CometChatVoIP.hasEnabledPhoneAccountForVoIP(this, object : VoIPPermissionListener { - override fun onPermissionsGranted() { /* ready for call pushes */ } - override fun onPermissionsDenied(error: CometChatVoIPError?) { - CometChatVoIP.alertDialogForVoIP(this@HomeActivity) - } - }) -} - -override fun onRequestPermissionsResult(reqCode: Int, permissions: Array, results: IntArray) { - super.onRequestPermissionsResult(reqCode, permissions, results) - when (reqCode) { - AppUtils.PushNotificationPermissionCode -> if (granted(results)) { - CometChatVoIP.requestPhoneStatePermissions(this, CometChatVoIPConstant.PermissionCode.READ_PHONE_STATE) - } - CometChatVoIPConstant.PermissionCode.READ_PHONE_STATE -> if (granted(results)) { - if (CometChatVoIP.hasManageOwnCallsPermission(this)) { - CometChatVoIP.requestAnswerPhoneCallsPermissions(this, CometChatVoIPConstant.PermissionCode.ANSWER_PHONE_CALLS) - } else { - CometChatVoIP.requestManageOwnCallsPermissions(this, CometChatVoIPConstant.PermissionCode.MANAGE_OWN_CALLS) - } - } - CometChatVoIPConstant.PermissionCode.MANAGE_OWN_CALLS -> if (granted(results)) { - CometChatVoIP.requestAnswerPhoneCallsPermissions(this, CometChatVoIPConstant.PermissionCode.ANSWER_PHONE_CALLS) - } - CometChatVoIPConstant.PermissionCode.ANSWER_PHONE_CALLS -> if (granted(results)) { - launchVoIP() - } - } -} -private fun granted(results: IntArray) = - results.isNotEmpty() && results[0] == PackageManager.PERMISSION_GRANTED - -// Deep link from notification payload to Chats -private fun handleDeepLinking() { - val type = intent.getStringExtra(AppConstants.FCMConstants.NOTIFICATION_TYPE) - val payload = intent.getStringExtra(AppConstants.FCMConstants.NOTIFICATION_PAYLOAD) ?: return - if (type == AppConstants.FCMConstants.NOTIFICATION_TYPE_MESSAGE) { - val fcmMessageDTO = Gson().fromJson(payload, FCMMessageDTO::class.java) - // Set currentOpenChatId to suppress notifications for the open chat - MyApplication.currentOpenChatId = if (fcmMessageDTO.receiverType == "user") { - fcmMessageDTO.sender - } else fcmMessageDTO.receiver - } -} -``` - -Requests notification + telecom permissions in sequence, initializes the VoIP phone account, and maps notification payload extras to set `currentOpenChatId` so you don’t alert for the chat currently open. - -## 6. Register the FCM token after login - -Call registration right after `CometChatUIKit.login()` succeeds: - -```kotlin lines -FirebaseMessaging.getInstance().token.addOnCompleteListener { task -> - if (task.isSuccessful) { - val token = task.result - CometChatNotifications.registerPushToken( + override fun onNewToken(token: String) { + // Re-binds a rotated token when a user is logged in; no-op otherwise. + CometChatPushNotifications.handleTokenRefresh( + PushPlatform.FCM_ANDROID, token, - PushPlatforms.FCM_ANDROID, - AppConstants.FCMConstants.PROVIDER_ID, - object : CometChat.CallbackListener() { - override fun onSuccess(uid: String?) { /* token registered */ } - override fun onError(e: CometChatException) { /* handle failure */ } - } + AppCredentials.FCM_PROVIDER_ID, ) } } ``` -Registers the current device token with CometChat under your Provider ID after login so the backend can target this user via FCM; retry on failure and rerun when the token rotates. - -Re-register on token refresh. Keep the provider ID aligned to the FCM provider you created for this app. - -Handle FCM refresh tokens too: +Register the service in your manifest: -```kotlin lines -// In FCMService -override fun onNewToken(token: String) { - super.onNewToken(token) - // Re-register with CometChat using your provider ID - CometChatNotifications.registerPushToken( - token, - PushPlatforms.FCM_ANDROID, - AppConstants.FCMConstants.PROVIDER_ID, - object : CometChat.CallbackListener() { - override fun onSuccess(s: String?) { /* token registered */ } - override fun onError(e: CometChatException) { /* handle failure */ } - } - ) -} +```xml AndroidManifest.xml + + + + + ``` -Ensures a rotated FCM token is re-bound to the logged-in user; without this, pushes will stop after Firebase refreshes the token. + +`handlePushNotification` also accepts optional `title`, `body`, `icon`, `uid`, and `guid` overrides — pass `uid`/`guid` if you want to control the notification grouping key. For the default experience, just pass `data`. + -## 7. Unregister the token on logout - -```kotlin lines -CometChatNotifications.unregisterPushToken(object : CometChat.CallbackListener() { - override fun onSuccess(s: String?) { /* success */ } - override fun onError(e: CometChatException) { /* handle error */ } -}) -// Then call CometChatUIKit.logout() -``` +## 5. Request permission and register the token -## 8. Badge count +Request `POST_NOTIFICATIONS` (Android 13+) early using the SDK helper, and register the token **after** login so it binds to the session: -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. - -### 8.1 Enable unread badge count on the CometChat Dashboard - -1. Go to **CometChat Dashboard → Notification Engine → 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. - -### 8.2 Add the ShortcutBadger dependency - -Add the ShortcutBadger library to your app-level `build.gradle`: +```kotlin +import com.cometchat.pushnotification.helpers.CometChatPNHelper -```gradle lines -dependencies { - implementation 'me.leolin:ShortcutBadger:1.1.22@aar' +// In your entry activity, before/at login: +if (!CometChatPNHelper.hasNotificationPermission(this)) { + CometChatPNHelper.requestNotificationPermission(this, REQUEST_CODE_NOTIFICATIONS) } ``` -### 8.3 Expected payload format - -CometChat sends FCM data messages with this structure (relevant fields): - -```json -{ - "data": { - "unreadMessageCount": "5", - "title": "New Message", - "alert": "John: Hello!", - "conversationId": "user_abc123", - "conversationType": "user" - } +```kotlin +import com.cometchat.pushnotification.CometChatPushNotifications +import com.cometchat.pushnotification.models.PushPlatform +import com.google.firebase.messaging.FirebaseMessaging + +// After CometChatUIKit.login(...) succeeds: +FirebaseMessaging.getInstance().token.addOnSuccessListener { token -> + CometChatPushNotifications.registerToken( + PushPlatform.FCM_ANDROID, + token, + AppCredentials.FCM_PROVIDER_ID, + onSuccess = { /* token registered */ }, + onError = { e -> /* log e */ }, + ) } ``` -`unreadMessageCount` is a string representing the total unread messages across all conversations for the logged-in user. - -### 8.4 Update the app badge from the push payload - -Inside your notification service (for example `FCMService.onMessageReceived`), parse `unreadMessageCount` and update the badge: +Unregister **before** logout so the device stops receiving pushes for that user: -```kotlin lines -import me.leolin.shortcutbadger.ShortcutBadger - -// Inside onMessageReceived, after receiving the message: -val unreadCountStr: String? = message.data["unreadMessageCount"] -unreadCountStr?.toIntOrNull()?.let { count -> - if (count >= 0) { - ShortcutBadger.applyCount(applicationContext, count) - } else { - Log.w(TAG, "Invalid badge count: $count") - } -} ?: Log.d(TAG, "No unreadMessageCount in payload") +```kotlin +CometChatPushNotifications.unregisterToken( + onSuccess = { /* then CometChatUIKit.logout(...) */ }, + onError = { e -> /* log e */ }, +) ``` -`ShortcutBadger` uses launcher-specific APIs (Samsung, Huawei, Xiaomi, etc.) to display a badge number on the app icon. Passing `0` clears the badge. +## 6. Notification taps and call events -### 8.5 Show unread count in the notification +Suppress notifications for the chat that is currently open, and set the tap listener so you can navigate when a chat notification is tapped: -Update your notification builder to display the unread count in the notification itself: - -```kotlin lines -// Inside your notification building logic -mNotificationBuilder.setNumber(count) -mNotificationBuilder.setSubText("$count unread messages") +```kotlin +import com.cometchat.pushnotification.CometChatPushNotifications +import com.cometchat.pushnotification.listeners.NotificationTapListener + +// When a chat screen opens (clear it on exit): +CometChatPushNotifications.setCurrentOpenChatId(conversationId) + +CometChatPushNotifications.setOnNotificationTapListener(object : NotificationTapListener { + override fun onNotificationTapped( + context: Context, + user: User?, + group: Group?, + message: BaseMessage?, + ) { + // Build your own Intent to open the conversation for `user` / `group`. + } +}) ``` -- `setNumber(count)` displays a count badge on the notification icon. -- `setSubText()` shows the unread count below the notification title. - -### 8.6 Clear badge when the app opens - -Clear the badge count when the app launches and every time it resumes from the background. Override `onResume()` in your main activity: +By default the SDK shows its **built-in full-screen incoming-call screen** and launches the CometChat Calls UI on accept — you don't need to write any call UI. Optionally, observe foreground call events to drive your own in-app UI: -```kotlin lines -override fun onResume() { - super.onResume() - ShortcutBadger.removeCount(this) -} -``` +```kotlin +import com.cometchat.pushnotification.listeners.CallEventListener +import com.cometchat.pushnotification.models.PNCallInfo -This ensures the badge is cleared when the user opens the app, keeping the badge count in sync with the actual unread state. - -{/* ## 9. What arrives in the push payload - -Payload keys delivered to `onMessageReceived` (adapted from the shared push integration): - -```json lines -{ - "title": "Andrew Joseph", - "body": "Hello!", - "sender": "cometchat-uid-1", - "senderName": "Andrew Joseph", - "senderAvatar": "https://assets.cometchat.io/sampleapp/v2/users/cometchat-uid-1.webp", - "receiver": "cometchat-uid-2", - "receiverName": "George Alan", - "receiverAvatar": "https://assets.cometchat.io/sampleapp/v2/users/cometchat-uid-2.webp", - "receiverType": "user", - "tag": "123", - "conversationId": "cometchat-uid-1_user_cometchat-uid-2", - "type": "chat", // or "call" - "callAction": "initiated", // "initiated" | "cancelled" | "unanswered" | "ongoing" | "rejected" | "ended" | "busy" - "sessionId": "v1.123.aik2", - "callType": "audio", - "sentAt": "1741847453000", - "message": { }, // CometChat Message Object if included - "custom": { } // Custom JSON if configured -} +CometChatPushNotifications.setCallEventListener(object : CallEventListener { + override fun onIncomingCall(callInfo: PNCallInfo) { /* callInfo.sessionId, callerName, callType … */ } + override fun onCallCancelled(sessionId: String) { /* tear down UI */ } +}) ``` -Use `message` for deep links (`CometChatHelper.processMessage`) and `type/callAction` to branch chat vs call flows. */} - -## 9. Handle message pushes - -- [`FCMService.onMessageReceived`](https://github.com/cometchat/cometchat-uikit-android/blob/v5/sample-app-kotlin%2Bpush-notification/src/main/java/com/cometchat/sampleapp/kotlin/fcm/fcm/FCMService.kt) checks `message.data["type"]`. -- For `type == "chat"`: mark delivered (`CometChat.markAsDelivered`), skip notifying if the chat is already open (`MyApplication.currentOpenChatId`), and build grouped notifications (avatars + BigText) via [`FCMMessageNotificationUtils`](https://github.com/cometchat/cometchat-uikit-android/blob/v5/sample-app-kotlin%2Bpush-notification/src/main/java/com/cometchat/sampleapp/kotlin/fcm/fcm/FCMMessageNotificationUtils.kt) with inline reply actions. -- [`FCMMessageBroadcastReceiver`](https://github.com/cometchat/cometchat-uikit-android/blob/v5/sample-app-kotlin%2Bpush-notification/src/main/java/com/cometchat/sampleapp/kotlin/fcm/fcm/FCMMessageBroadcastReceiver.kt) handles inline replies, initializes the SDK headlessly, sends the reply, and refreshes the notification. -- In your messaging service (e.g., `FCMService`), set the notification tap intent to your splash/entry activity (e.g., `SplashActivity`), and keep the `currentOpenChatId` check to suppress notifications for the open chat. + +To fully replace the built-in call screen with your own, set `setOnIncomingCallHandler(...)` (override the ringing screen) and/or `setOnCallAnsweredHandler(...)` (override the post-accept ongoing-call screen). The SDK still manages ringtone, ring timeout, and cleanup. + -## 10. Handle call pushes (ConnectionService) +## 7. Badge count -- For `type == "call"`, `FCMService.handleCallFlow` parses [`FCMCallDto`](https://github.com/cometchat/cometchat-uikit-android/blob/v5/sample-app-kotlin%2Bpush-notification/src/main/java/com/cometchat/sampleapp/kotlin/fcm/fcm/FCMCallDto.kt) and routes to the `voip` package. -- [`CometChatVoIP`](https://github.com/cometchat/cometchat-uikit-android/blob/v5/sample-app-kotlin%2Bpush-notification/src/main/java/com/cometchat/sampleapp/kotlin/fcm/voip/CometChatVoIP.kt) registers a `PhoneAccount` and triggers `TelecomManager.addNewIncomingCall` for native full-screen UI with Accept/Decline. -- Busy logic: if already on a call, reject with busy (`Repository.rejectCallWithBusyStatus`). Cancel/timeout pushes end the active telecom call when IDs match. -- Runtime VoIP checks: before handling call pushes, request `READ_PHONE_STATE`, `MANAGE_OWN_CALLS`, and `ANSWER_PHONE_CALLS` at runtime and ensure the phone account is enabled (`CometChatVoIP.hasEnabledPhoneAccountForVoIP`). -- Foreground suppression: the sample ignores VoIP banners if `MyApplication.isAppInForeground()` is true; keep or remove based on your UX. -- Cancel/unanswered handling: on `callAction` of `cancelled`/`unanswered`, end the active telecom call if the session IDs match. +CometChat's Enhanced Push Notification payload includes an `unreadMessageCount` field (a string, total unread across all conversations). Enable it once on the dashboard: **CometChat Dashboard → Notification Engine → Settings → Preferences → Push Notification Preferences → Unread Badge Count**. -## 11. Customize notification text or parse payloads - -Parse the push into a `BaseMessage` for deep links: +Parse it from the payload and set the app-icon badge with a launcher badge library such as `ShortcutBadger`: ```kotlin -override fun onMessageReceived(remoteMessage: RemoteMessage) { - val messageJson = remoteMessage.data["message"] ?: return - val baseMessage = CometChatHelper.processMessage(JSONObject(messageJson)) - // open the right chat/thread using baseMessage -} +val count = message.data["unreadMessageCount"]?.toIntOrNull() ?: 0 +ShortcutBadger.applyCount(applicationContext, count) // 0 clears the badge ``` -Parses the CometChat message JSON shipped in the payload into a `BaseMessage` so you can navigate to the right conversation/thread without extra API calls. - -Override the push body before sending: - -```kotlin -val meta = JSONObject().put("pushNotification", "Custom notification body") -customMessage.metadata = meta -CometChat.sendCustomMessage(customMessage, object : CallbackListener() {}) -``` +Clear the badge when the app resumes (`onResume` of your main activity). -Adds a `pushNotification` field in metadata so CometChat uses your custom text as the push body for that message. -## 12. Navigation from notifications -Notification taps launch [`SplashActivity`](https://github.com/cometchat/cometchat-uikit-android/blob/v5/sample-app-kotlin%2Bpush-notification/src/main/java/com/cometchat/sampleapp/kotlin/fcm/ui/activity/SplashActivity.kt); it reads `NOTIFICATION_PAYLOAD` extras and opens the correct user or group in `MessagesActivity`. Keep `launchMode` settings that allow the intent extras to arrive. -## 13. Testing checklist +## 8. Testing checklist 1. Install on a physical device and grant notification + mic permissions (Android 13+ needs `POST_NOTIFICATIONS`). -2. Log in and ensure token registration succeeds (check Logcat). +2. Log in and confirm token registration succeeds (check the `registerToken` success callback / Logcat). 3. Send a message from another user: - - Foreground: grouped notification shows unless you are already in that chat. - - Background/terminated: tap opens the correct conversation. -4. Inline reply from the shade delivers the message and updates the notification. -5. Trigger an incoming call push: - - Native full-screen call UI appears with caller info. - - Accept/Decline work; cancel/timeout dismisses the telecom call. -6. Reinstall or clear app data to confirm token re-registration works. + - App open in a different chat: notification appears (grouped, with inline reply). + - App backgrounded/killed: notification appears; tapping opens the right conversation via `onNotificationTapped`. + - No notification for the chat currently open (via `setCurrentOpenChatId`). +4. Trigger an incoming CometChat call and confirm the full-screen call UI shows the caller with Accept/Decline, even on the lock screen; Accept joins the call, Decline rejects it. +5. Toggle Wi-Fi/cellular and reinstall to confirm token registration survives refreshes (`onNewToken` → `handleTokenRefresh`). -## Troubleshooting +## 9. Troubleshooting | Symptom | Quick checks | | --- | --- | -| No notifications | Package name matches Firebase app, `google-services.json` is present, notification permission granted, Provider ID correct, Push Notifications enabled. | -| Token registration fails | Run registration after login, confirm `AppConstants.FCMConstants.PROVIDER_ID`, and verify the Firebase project matches the app ID. | -| Notification tap does nothing | Ensure `SplashActivity` reads `NOTIFICATION_PAYLOAD` and activity launch modes do not drop extras. | -| Call UI never shows | All telecom permissions declared + granted; `CometChatVoIPConnectionService` in manifest; device supports `MANAGE_OWN_CALLS`. | -| Inline reply crashes | Keep `FCMMessageBroadcastReceiver` registered; do not strip FCM or `RemoteInput` classes in ProGuard/R8. | -| Badge count not showing | Verify **Unread Badge Count** is enabled in CometChat Dashboard, ShortcutBadger dependency is added, and the launcher supports badges (Samsung, Huawei, Xiaomi). | +| No notifications received | Confirm `google-services.json` is in the app module, the package name matches Firebase, `POST_NOTIFICATIONS` is granted (Android 13+), and your `FirebaseMessagingService` calls `handlePushNotification(this, data = message.data)`. | +| Token registration errors | Verify `FCM_PROVIDER_ID` matches the dashboard exactly and that `registerToken` runs **after** `login` succeeds. | +| Nothing happens on a killed-app push | Ensure `CometChatPushNotifications.init(...)` runs in `Application.onCreate()` and `` points at your `Application` subclass. | +| Full-screen call UI not showing | Test on a physical device; on aggressive OEM skins (MIUI/Redmi/POCO), grant autostart/lock-screen/overlay permissions for the app. | +| Pushes stop after some time | Make sure `onNewToken` forwards to `handleTokenRefresh` (or re-`registerToken`) — FCM rotates tokens. | + +## Resources + + + + The drop-in push & VoIP SDK on the CometChat Maven repo. + + + Source, changelog, and sample. + + diff --git a/notifications/flutter-push-notifications-android.mdx b/notifications/flutter-push-notifications-android.mdx deleted file mode 100644 index 3a761817e..000000000 --- a/notifications/flutter-push-notifications-android.mdx +++ /dev/null @@ -1,359 +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**. - - - 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. - - - Upload FCM service account JSON - - -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. - - - Firebase - Push Notifications - - -### 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 → Notification Engine → 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): - -```json -{ - "data": { - "unreadMessageCount": "5", - "title": "New Message", - "alert": "John: Hello!", - "conversationId": "user_abc123", - "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 2e020f564..000000000 --- a/notifications/flutter-push-notifications-ios.mdx +++ /dev/null @@ -1,367 +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**. - - - 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. - - - Upload APNs credentials - - -3. (Optional) Add an **FCM** provider if you plan to register FCM tokens on iOS. - - - Upload FCM service account JSON - - -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*. - - - Firebase - Push Notifications - - -## 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. - - - Enable Push Notifications and Background Modes for APNs - - -### 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 → Notification Engine → 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): - -```json -{ - "aps": { - "alert": { - "title": "New Message", - "body": "John: Hello!" - }, - "badge": 5, - "sound": "default" - }, - "unreadMessageCount": 5, - "conversationId": "user_abc123" -} -``` - -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..4abc5860a --- /dev/null +++ b/notifications/flutter-push-notifications.mdx @@ -0,0 +1,503 @@ +--- +title: "Flutter" +description: "Add CometChat push notifications and VoIP calls 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/AppDelegate on iOS. +- Requesting permission and registering tokens (FCM on Android, APNs + VoIP on iOS) after login. +- Receiving pushes and letting the SDK render chat notifications and full-screen calls. +- Handling notification taps, incoming-call navigation, badge counts, and Android OEM permissions. +- 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`, `flutter_callkit_incoming`, and native `MainActivity` / `AppDelegate` bridges. Token registration, foreground presentation, notification taps, badge management, and the full incoming-call experience (the Android lock-screen call activity and iOS CallKit) are handled inside the plugin. + + +## 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 or full-screen call. +- **iOS (APNs + PushKit):** Apple issues the APNs device token (chat alerts) and the VoIP token (calls). APNs alerts are shown by the system; VoIP pushes are presented through CallKit by the plugin. +- **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` retrieves the tokens, registers them with CometChat, parses payloads, and drives the call UI. 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). +- Flutter 3.3.0+ / Dart 3.10.8+. +- **Android:** Kotlin **2.0+**, `minSdkVersion 24`, `compileSdkVersion 36`. +- **iOS:** iOS 13.0+ (set the Podfile platform to **14.0** for VoIP/CallKit). +- A physical device — background delivery, full-screen calls, and VoIP pushes are unreliable on emulators/simulators. + + +**Complete the [Getting Started](/notifications/push-getting-started) guide first** — enable Push Notifications, add your providers (FCM for Android, APNs + APNs VoIP 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"; + static const apnVoipProviderId = "APNS-VOIP-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: ^1.0.1 + # 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`. + + + You do **not** need to add notification, call, full-screen-intent, or lock-screen permissions to your `AndroidManifest.xml`. The plugin declares everything it needs (including the lock-screen `IncomingCallActivity` and the Decline broadcast receiver), and Gradle merges them into your app automatically. + + + + 1. Set the deployment target in `ios/Podfile`, then install pods: + + ```ruby + platform :ios, '14.0' + ``` + + ```bash + cd ios && pod install && cd .. + ``` + + 2. Open `ios/Runner.xcworkspace` in Xcode and set the development team that owns the APNs/VoIP key. (The **Push Notifications** / **Background Modes** capabilities and `Info.plist` usage strings come from [Getting Started](/notifications/push-getting-started).) + + 3. The plugin handles PushKit and CallKit natively, so no PushKit/CallKit code is needed in your `AppDelegate` — but you must forward the APNs device token so it can be registered: + + ```swift lines + import Flutter + import UIKit + import notification_voip_plugin + + @main + @objc class AppDelegate: FlutterAppDelegate { + override func application( + _ application: UIApplication, + didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? + ) -> Bool { + GeneratedPluginRegistrant.register(with: self) + return super.application(application, didFinishLaunchingWithOptions: launchOptions) + } + + override func application( + _ application: UIApplication, + didRegisterForRemoteNotificationsWithDeviceToken deviceToken: Data + ) { + NotificationVoipPlugin.shared?.setAPNsToken(deviceToken) + } + } + ``` + + + +## 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.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( + CometChatNotificationConfig( + appId: AppCredentials.appId, + region: AppCredentials.region, + ), + ); + await CometChatPushNotifications.handlePushNotification(message.data); + } + + Future main() async { + WidgetsFlutterBinding.ensureInitialized(); + await Firebase.initializeApp(); + + await CometChatPushNotifications.init( + CometChatNotificationConfig( + appId: AppCredentials.appId, + region: AppCredentials.region, + onCallAccepted: (call) { + // Start your CometChat call session, then: + // CometChatPushNotifications.setCallConnected(call.sessionId); + }, + onCallDeclined: (call) { + // Reject the call server-side via the CometChat SDK. + }, + ), + ); + + // 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.dart'; + + Future main() async { + WidgetsFlutterBinding.ensureInitialized(); + + await CometChatPushNotifications.init( + CometChatNotificationConfig( + appId: AppCredentials.appId, + region: AppCredentials.region, + onCallAccepted: (call) { + // Start your CometChat call session, then: + // CometChatPushNotifications.setCallConnected(call.sessionId); + }, + onCallDeclined: (call) { + // Reject the call server-side via the CometChat SDK. + }, + ), + ); + + runApp(const MyApp()); + } + ``` + + + +The `CometChatNotificationConfig` also accepts: + +| Field | Default | Purpose | +| --- | --- | --- | +| `customCallScreenBuilder` | `null` | Return your own incoming-call widget. When set, you own accept/reject, dismissing the call notification (`cancelCallNotification`), and popping the screen; `onCallAccepted`/`onCallDeclined` are **not** called. | +| `onCallAccepted` / `onCallDeclined` | `null` | Callbacks for the built-in default call screen. | +| `callActionHandler` | default | Subclass `CometChatCallActionHandler` to intercept mute/speaker/camera (e.g. drive your WebRTC engine). | +| `showInAppNotifications` | `false` | Show chat pushes as in-app banners when the app is foregrounded. | +| `showInAppVoIP` | `false` | Show the incoming-call UI for pushes received in the foreground (leave `false` if your UI Kit's `CallEventService` already handles foreground calls over WebSocket). | + +Once the navigator is ready on your first screen after login, replay any cold-start call launch and route subsequent call taps: + +```dart lines +final navigatorKey = GlobalKey(); + +// After the widget tree builds: +CometChatPushNotifications.handleIncomingCallLaunch(navigatorKey); +``` + +Pass the same `navigatorKey` to your `MaterialApp`. + +### Android: lock-screen call entrypoint + +When a call arrives while the device is locked, the plugin launches a dedicated full-screen activity that runs a separate Dart entrypoint named `incomingCallMain`. Define it in `main.dart`. It talks to the plugin's native activity over the `cometchat_locked_call` method channel: + +```dart lines +import 'package:flutter/services.dart'; + +@pragma('vm:entry-point') +void incomingCallMain() { + WidgetsFlutterBinding.ensureInitialized(); + runApp(const _LockedCallApp()); +} + +class _LockedCallApp extends StatefulWidget { + const _LockedCallApp(); + @override + State<_LockedCallApp> createState() => _LockedCallAppState(); +} + +class _LockedCallAppState extends State<_LockedCallApp> { + static const _channel = MethodChannel('cometchat_locked_call'); + CometChatCallDetails? _call; + + @override + void initState() { + super.initState(); + // The native activity calls "reload" for each new/duplicate call. + _channel.setMethodCallHandler((call) async { + if (call.method == 'reload') _loadCall(); + }); + _loadCall(); + } + + Future _loadCall() async { + final d = await _channel.invokeMapMethod('getCallDetails'); + if (d == null) return; + setState(() { + _call = CometChatCallDetails( + sessionId: d['callId'] as String? ?? '', + callerUid: '', + callerName: d['callerName'] as String? ?? 'Unknown', + callerAvatar: d['callerAvatar'] as String?, + callType: (d['isVideo'] as bool? ?? false) + ? CometChatCallType.video + : CometChatCallType.audio, + receiverType: CometChatReceiverType.user, + conversationId: '', + isVideo: d['isVideo'] as bool? ?? false, + ); + }); + } + + @override + Widget build(BuildContext context) { + final call = _call; + return MaterialApp( + debugShowCheckedModeBanner: false, + home: call == null + ? const SizedBox.shrink() + : CometChatDefaultCallScreen( + callDetails: call, + onAccept: () async { + await _channel.invokeMethod('cancelNotification', + {'callId': call.sessionId}); + // Bring your app forward / start the call session here. + await _channel.invokeMethod('finish', {'callId': call.sessionId}); + }, + onDecline: () async { + await _channel.invokeMethod('finish', {'callId': call.sessionId}); + }, + ), + ); + } +} +``` + + +Adapt the accept path to launch your in-call screen. The channel methods (`getCallDetails`, `cancelNotification`, `finish`, and the incoming `reload`) are the contract the plugin's `IncomingCallActivity` exposes. + + +## 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: + CometChatPushNotifications.registerToken( + PushPlatform.FCM_FLUTTER_ANDROID, + providerId: AppCredentials.fcmProviderId, + onSuccess: (token) => debugPrint('Registered FCM token: $token'), + onError: (e) => debugPrint('Registration failed: ${e.message}'), + ); + ``` + + + Register **both** the APNs device token and the VoIP token. Each call includes both tokens in the server request, so register them with their matching provider IDs: + + ```dart lines + // After CometChatUIKit.login(...) / CometChat.login(...) succeeds: + + // APNs device token (chat/alert pushes) + CometChatPushNotifications.registerToken( + PushPlatform.APNS_FLUTTER_DEVICE, + providerId: AppCredentials.apnProviderId, + onSuccess: (token) => debugPrint('APNs registered: $token'), + onError: (e) => debugPrint('APNs failed: ${e.message}'), + ); + + // VoIP (PushKit) token — required for calls + CometChatPushNotifications.registerToken( + PushPlatform.APNS_FLUTTER_VOIP, + providerId: AppCredentials.apnVoipProviderId, + onSuccess: (token) => debugPrint('VoIP registered: $token'), + onError: (e) => debugPrint('VoIP failed: ${e.message}'), + ); + ``` + + + +Token refreshes are handled for you — the plugin listens for `onTokenRefresh` and automatically re-registers the new token with CometChat. Subscribe to `CometChatPushNotifications.onTokenRefresh` if you want to log it. + +## 5. Notification taps and call events + +Subscribe to the streams to navigate and coordinate call state (same on both platforms): + +```dart lines +CometChatPushNotifications.onNotificationTap.listen((details) { + // details.conversationId, details.sender, details.receiverType, ... + // Navigate to your messages screen for this conversation. +}); + +CometChatPushNotifications.onCallEvent.listen((event) { + switch (event.type) { + case CometChatCallEventType.accepted: + // Start / join the call session, then setCallConnected(event.sessionId). + break; + case CometChatCallEventType.declined: + case CometChatCallEventType.ended: + case CometChatCallEventType.timeoutEnded: + // Tear down any call state. + break; + case CometChatCallEventType.incoming: + break; + } +}); +``` + +Useful call methods: `setCallConnected(sessionId)` (after media is established — critical for connecting calls from a terminated state), `endCall(sessionId)`, `endAllCalls()` (on logout), and `getActiveCallIds()`. + + +**Cold-start VoIP handling (iOS):** when the app is killed and a VoIP push arrives, the plugin presents CallKit natively via PushKit before the Flutter engine is ready. When the user answers, `handleIncomingCallLaunch` routes to your call screen and the `onCallEvent` / `onCallAccepted` handlers fire — set up your CometChat call session there and call `setCallConnected`. + + +## 6. Android: OEM permissions for lock-screen calls + +To reliably show a full-screen call over the lock screen — especially on Android 14+ and OEM skins like MIUI/Redmi/POCO — check and request the relevant permissions: + +```dart lines +final perms = await CometChatPushNotifications.checkCallPermissions(); +// perms: { fullScreenIntent, overlay, batteryOptimized } + +if (perms['fullScreenIntent'] == false) { + await CometChatPushNotifications.openFullScreenIntentSettings(); +} +if (perms['overlay'] == false) { + await CometChatPushNotifications.openOverlaySettings(); // MIUI / Xiaomi +} +if (perms['batteryOptimized'] == true) { + await CometChatPushNotifications.openBatteryOptimizationSettings(); +} +// Xiaomi / Oppo / Vivo / Huawei / Samsung autostart: +await CometChatPushNotifications.openAutoStartSettings(); +``` + +On iOS and web these checks return "granted" and the open-settings calls are no-ops. + +## 7. 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. + + + + Set the badge from the payload (for example inside a foreground handler) using the plugin's badge helper — no extra dependency needed: + + ```dart lines + final count = int.tryParse('${message.data['unreadMessageCount'] ?? ''}') ?? 0; + await CometChatPushNotifications.setBadgeCount(count); + ``` + + + With pure APNs the badge is handled **server-side**: CometChat sets `aps.badge` in the payload and iOS updates the app icon automatically — no client code required to display it. + + + +Clear the badge and dismiss stale notifications when the app opens and every resume: + +```dart lines +await CometChatPushNotifications.setBadgeCount(0); +await CometChatPushNotifications.clearAll(); +``` + +Add a `WidgetsBindingObserver` to your root widget and clear in `initState` and on `AppLifecycleState.resumed`. + +## 8. Testing checklist + +1. Run on a physical device. Grant notification, microphone, and camera permissions 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. Trigger an incoming CometChat call and confirm: + - The full-screen call UI (Android) / CallKit (iOS) shows the caller with Accept/Decline, even on the lock screen. + - Accepting starts the call session (via `onCallAccepted`) and dismisses the notification when the call ends. + - Declining rejects the call server-side (via your `onCallDeclined`). +5. Toggle Wi-Fi/cellular and reinstall to confirm token registration survives refreshes. + +## 9. 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`. | +| Full-screen call UI not showing | Android | Check `checkCallPermissions()` and request full-screen-intent / overlay / battery / autostart via the helpers in section 6. | +| Lock-screen call is a black screen | Android | Make sure `incomingCallMain` is defined in `main.dart` and annotated with `@pragma('vm:entry-point')`. | +| No VoIP pushes | iOS | Ensure Push Notifications + Background Modes (Voice over IP) are enabled, the bundle ID matches the CometChat APNs VoIP provider, and you registered `PushPlatform.APNS_FLUTTER_VOIP`. | +| APNs token never registers | iOS | Confirm the `AppDelegate` forwards the token via `NotificationVoipPlugin.shared?.setAPNsToken(deviceToken)` and that permission was granted. | +| CallKit UI never dismisses | iOS | Call `endCall(sessionId)` (or `endAllCalls()`) when your call session ends so the plugin reports it to CallKit. | +| Token registration errors | Both | Verify the provider IDs match the dashboard exactly and that `registerToken` runs **after** login. | +| Notification/call taps ignored | Both | Subscribe to `onNotificationTap`, and call `handleIncomingCallLaunch(navigatorKey)` after the navigator is ready with the same key passed to `MaterialApp`. | + +## Resources + + + + The drop-in push & VoIP plugin on pub.dev. + + + Source, changelog, and example app. + + diff --git a/notifications/ios-apns-push-notifications.mdx b/notifications/ios-apns-push-notifications.mdx deleted file mode 100644 index 509dde513..000000000 --- a/notifications/ios-apns-push-notifications.mdx +++ /dev/null @@ -1,1336 +0,0 @@ ---- -title: "iOS APNs Push Notifications" -description: "Implement APNs push notifications with CometChat UIKit for iOS, including CallKit integration for VoIP calls." ---- - - - Reference implementation of iOS UIKit, APNs and Push Notification Setup. - - -## What this guide covers - -- CometChat dashboard setup (enable push, add APNs Device + APNs VoIP providers) with screenshots. -- APNs + PushKit/CallKit wiring (tokens, delegates, CallKit). -- Incoming message/call handling and deep links. -- Badge count and grouped notifications. -- Payload customization and testing. - -{/* ## What you need first - -- CometChat App ID, Region, Auth Key; Push Notifications with **APNs Device provider ID** and **APNs VoIP provider ID**. -- Apple capabilities: Push Notifications, Background Modes (Remote notifications, Voice over IP), CallKit usage descriptions. Physical device required. -- `.p8` APNs key (Key ID, Team ID, Bundle ID) or certificates. */} - -## How APNs + CometChat work together - -- **APNs is the transport:** Apple issues the APNs device/VoIP tokens and delivers the payloads. No FCM bridge is involved. -- **CometChat providers:** The APNs Device and APNs VoIP providers you add in the CometChat dashboard hold your APNs key/cert. When you call `CometChatNotifications.registerPushToken(..., .APNS_IOS_DEVICE / .APNS_IOS_VOIP, providerId)` after login, CometChat binds those tokens to the logged-in user and sends to APNs for you. -- **Flow:** Permission prompt → APNs returns device + VoIP tokens → after `CometChat.login`, register both tokens with the matching provider IDs → CometChat sends to APNs → APNs delivers → `UNUserNotificationCenterDelegate` (and `PushKit`/`CallKit` for VoIP) surface the notification/tap. - -## 1. Enable push and add providers (CometChat Dashboard) - -1. Go to **Notifications → Settings** and enable **Push Notifications**. - - - Enable Push Notifications - - -2. Click **Add Credentials**: - - Add an **APNs Device** provider (alerts) using your `.p8` key, Team ID, Key ID, and Bundle ID; copy the Provider ID. - - Add an **APNs VoIP** provider (calls) with the same `.p8` (recommended for CallKit reliability); copy the Provider ID. - - -**`.p12` certificates are deprecated.** Apple recommends using `.p8` Auth Keys for push notifications. `.p8` keys are simpler to manage (one key works for all your apps), never expire, and are the only format actively supported going forward. If you are still using `.p12`, migrate to `.p8` at your earliest convenience. - - - - Add APNs credentials - - -Keep the provider IDs—you’ll paste them into your app constants. - -## 2. Apple setup - -1. Capabilities: Push Notifications, Background Modes → Remote notifications & Voice over IP, CallKit usage descriptions in `Info.plist` (mic/camera). -2. APNs Auth Key: generate `.p8` (or use cert), note **Key ID**, **Team ID**, and **Bundle ID**; upload to CometChat providers. - - - Enable Push Notifications and Background Modes for APNs - - -## 3. Wiring APNs + PushKit/CallKit - -- From below code, copy `CometChatAPNsHelper.swift`, `CometChatPNHelper.swift`, and the two `AppDelegate` extensions (`AppDelegate+PN.swift` and `AppDelegate+VoIP.swift`) into your project. -- These files implement APNs + PushKit/CallKit handling, notification presentation, tap and quick-reply actions, and call management. -- Update bundle ID, team ID, and provider IDs (`AppConstants.PROVIDER_ID` etc.). Keep the `voip` push type. - - - -```swift lines -import Foundation -import UIKit -import CometChatSDK -import CometChatUIKitSwift - -extension AppDelegate: UNUserNotificationCenterDelegate { - - // MARK: - Foreground Notifications - func userNotificationCenter(_ center: UNUserNotificationCenter, willPresent notification: UNNotification, withCompletionHandler completionHandler: @escaping (UNNotificationPresentationOptions) -> Void) { - print("willPresent notification: \(notification.request.content.userInfo)") - let userInfo = notification.request.content.userInfo - - if CometChatPNHelper.shouldPresentNotification(userInfo: userInfo) == false { - print("Suppressing notification (user is in active chat)") - completionHandler([]) - return - } - - completionHandler([.banner, .badge, .sound]) - } - - // MARK: - Notification Tap/Interaction - func userNotificationCenter(_ center: UNUserNotificationCenter, didReceive response: UNNotificationResponse, withCompletionHandler completionHandler: @escaping () -> Void) { - - let userInfo = response.notification.request.content.userInfo - print("User tapped notification: \(userInfo)") - - if response.actionIdentifier == "REPLY_ACTION" { - if let textResponse = response as? UNTextInputNotificationResponse { - let userReply = textResponse.userText - print("Quick reply: \(userReply)") - CometChatPNHelper.handleQuickReplyActionOnNotification(userInfo: userInfo, text: userReply, completionHandler: completionHandler) - } - completionHandler() - return - } - - CometChatPNHelper.handleTapActionOnNotification(userInfo: userInfo, completionHandler: completionHandler) - } - -} - -``` - - - ```swift lines -#if canImport(CometChatCallsSDK) - -import Foundation -import PushKit -import CallKit -import AVFoundation -import CometChatSDK -import CometChatCallsSDK - -extension AppDelegate: PKPushRegistryDelegate, CXProviderDelegate { - - // MARK: - VoIP Push Token Updates - - func pushRegistry( - _ registry: PKPushRegistry, - didUpdate pushCredentials: PKPushCredentials, - for type: PKPushType - ) { - print("VoIP token updated for type: \(type.rawValue)") - cometchatAPNsHelper.registerForVoIPCalls(pushCredentials: pushCredentials) - } - - func pushRegistry( - _ registry: PKPushRegistry, - didInvalidatePushTokenFor type: PKPushType - ) { - print("VoIP push token invalidated for type: \(type.rawValue)") - initializePushKit() - refreshPushCredentials() - } - - // MARK: - PushKit Setup - - func initializePushKit() { - if pushRegistry == nil { - let registry = PKPushRegistry(queue: DispatchQueue.main) - registry.delegate = self - registry.desiredPushTypes = [.voIP] - pushRegistry = registry - print("Push registry initialized") - } else { - print("Push registry already initialized") - } - } - - func refreshPushCredentials() { - guard let registry = pushRegistry else { - print("Push registry is nil") - return - } - - registry.desiredPushTypes = [] - registry.desiredPushTypes = [.voIP] - print("VoIP token refreshed") - } - - // MARK: - Incoming VoIP Push - - func pushRegistry( - _ registry: PKPushRegistry, - didReceiveIncomingPushWith payload: PKPushPayload, - for type: PKPushType, - completion: @escaping () -> Void - ) { - print("Incoming VoIP push received") - let provider = cometchatAPNsHelper.didReceiveIncomingPushWith(payload: payload) - provider?.setDelegate(self, queue: nil) - completion() - } - - // MARK: - CallKit Delegates - - func providerDidReset(_ provider: CXProvider) { - print("CallKit provider did reset") - cometchatAPNsHelper.onProviderDidReset(provider: provider) - } - - func provider(_ provider: CXProvider, perform action: CXAnswerCallAction) { - print("User answered call") - - // CRITICAL: Configure audio session BEFORE answering - configureAudioSession() - - cometchatAPNsHelper.onAnswerCallAction(action: action) - } - - func provider(_ provider: CXProvider, perform action: CXEndCallAction) { - print("User ended call") - cometchatAPNsHelper.onEndCallAction(action: action) - action.fulfill() - } - - func provider(_ provider: CXProvider, perform action: CXSetMutedCallAction) { - print("User toggled mute: \(action.isMuted)") - CometChatCalls.audioMuted(action.isMuted) - action.fulfill() - } - - // MARK: - CRITICAL: Audio Session Delegates (MISSING IN YOUR CODE) - - /// Called when CallKit activates the audio session - func provider(_ provider: CXProvider, didActivate audioSession: AVAudioSession) { - print("Audio session activated") - - // Configure audio session for VoIP - configureAudioSession() - - // Removed CometChatCalls.startAudioSession() as per instructions - } - - /// Called when CallKit deactivates the audio session - func provider(_ provider: CXProvider, didDeactivate audioSession: AVAudioSession) { - print("Audio session deactivated") - - // Removed CometChatCalls.stopAudioSession() as per instructions - } - - // MARK: - Audio Session Configuration - - /// Configure AVAudioSession for VoIP calls - private func configureAudioSession() { - let audioSession = AVAudioSession.sharedInstance() - - do { - // Set category for VoIP with speaker and bluetooth support - try audioSession.setCategory( - .playAndRecord, - mode: .voiceChat, - options: [.allowBluetooth, .allowBluetoothA2DP] - ) - - // Activate the session - try audioSession.setActive(true) - - print("Audio session configured successfully") - - } catch { - print("Failed to configure audio session: \(error.localizedDescription)") - } - } -} - -#endif -``` - - - -```swift lines -import Foundation -import UIKit -import CometChatSDK -import CometChatUIKitSwift -import PushKit -import CallKit -import AVFAudio - -#if canImport(CometChatCallsSDK) -import CometChatCallsSDK -#endif - -class CometChatAPNsHelper { - - var uuid: UUID? - var activeCall: Call? - var cancelCall: Bool = true - var onCall = true - var callController = CXCallController() - let voipRegistry = PKPushRegistry(queue: DispatchQueue.main) - var provider: CXProvider? = nil - - // MARK: - Configure Push Notifications - public func configurePushNotification(application: UIApplication, delegate: AppDelegate) { - - print("Configuring Push Notifications...") - - let authOptions: UNAuthorizationOptions = [.alert, .badge, .sound] - UNUserNotificationCenter.current().requestAuthorization( - options: authOptions, - completionHandler: { granted, error in - print("Push notification authorization granted: \(granted)") - if let error = error { - print("Authorization error: \(error.localizedDescription)") - } else if granted { - print("User granted notification permissions") - // Register for remote notifications on main thread - DispatchQueue.main.async { - UIApplication.shared.registerForRemoteNotifications() - } - } else { - print("User denied notification permissions") - } - }) - - // Define the reply action - let replyAction = UNTextInputNotificationAction( - identifier: "REPLY_ACTION", - title: "Reply", - options: [], - textInputButtonTitle: "Send", - textInputPlaceholder: "Type your reply here" - ) - - // Define the notification category - let messageCategory = UNNotificationCategory( - identifier: "MESSAGE_CATEGORY", - actions: [replyAction], - intentIdentifiers: [], - options: [] - ) - - // Register the category - UNUserNotificationCenter.current().setNotificationCategories([messageCategory]) - - // Add login listener - CometChat.addLoginListener("loginlistener-pnToken-register-login", self) - - #if canImport(CometChatCallsSDK) - let voipRegistry: PKPushRegistry = PKPushRegistry(queue: DispatchQueue.main) - voipRegistry.delegate = (delegate as? PKPushRegistryDelegate) - voipRegistry.desiredPushTypes = [PKPushType.voIP] - CometChatCallEvents.addListener("loginlistener-pnToken-register-login", self) - #endif - } - - // MARK: - Register APNs Token - public func registerTokenForPushNotification(deviceToken: Data) { - guard CometChat.getLoggedInUser() != nil else { - print("Cannot register token: User not logged in") - return - } - - let hexString = deviceToken.map { String(format: "%02.2hhx", $0) }.joined() - UserDefaults.standard.set(hexString, forKey: "apnspuToken") - print("APNs token: \(hexString)") - - CometChatNotifications.registerPushToken( - pushToken: hexString, - platform: CometChatNotifications.PushPlatforms.APNS_IOS_DEVICE, - providerId: AppConstants.PROVIDER_ID, - onSuccess: { success in - print("APNs token registered successfully: \(success)") - }, - onError: { error in - print("APNs token registration failed: \(error.errorCode) - \(error.errorDescription)") - } - ) - } - - // MARK: - Register Pending Token (After Login) - private func registerPendingTokenIfNeeded() { - if let pendingToken = UserDefaults.standard.string(forKey: "pendingAPNsToken") { - print("Registering pending APNs token after login...") - if let tokenData = hexStringToData(pendingToken) { - registerTokenForPushNotification(deviceToken: tokenData) - UserDefaults.standard.removeObject(forKey: "pendingAPNsToken") - } - } - } - - private func hexStringToData(_ string: String) -> Data? { - let len = string.count / 2 - var data = Data(capacity: len) - for i in 0.. CXProvider? { - guard let sender = payload.dictionaryPayload["sender"] as? String, - let senderName = payload.dictionaryPayload["senderName"] as? String, - let body = payload.dictionaryPayload["body"] as? String, - let callAction = payload.dictionaryPayload["callAction"] as? String, - let receiver = payload.dictionaryPayload["receiver"] as? String, - let type = payload.dictionaryPayload["type"] as? String, - let callType = payload.dictionaryPayload["callType"] as? String, - let sessionId = payload.dictionaryPayload["sessionId"] as? String, - let conversationId = payload.dictionaryPayload["conversationId"] as? String else { - print("Incomplete VoIP payload") - return nil - } - - let applicationState = UIApplication.shared.applicationState - print("VoIP push received - Action: \(callAction), State: \(applicationState.rawValue)") - - if type == "call" { - switch callAction { - case "initiated": - switch applicationState { - case .active: - if CometChat.getActiveCall() != nil { - print("User already on a call, rejecting with busy...") - CometChat.rejectCall(sessionID: sessionId, status: .busy, onSuccess: { rejectedCall in - print("Rejected incoming call with busy status") - }, onError: { error in - print("Failed to reject with busy: \(error?.errorDescription ?? "")") - }) - return nil - } else { - return updatedInitiateCall(sender: sender, senderName: senderName, body: body, callAction: callAction, receiver: receiver, callType: callType, sessionId: sessionId, conversationId: conversationId) - } - case .inactive, .background: - return updatedInitiateCall(sender: sender, senderName: senderName, body: body, callAction: callAction, receiver: receiver, callType: callType, sessionId: sessionId, conversationId: conversationId) - @unknown default: - break - } - - case "ongoing": - print("Call ongoing") - break - - case "unanswered": - provider?.reportCall(with: uuid!, endedAt: Date(), reason: .unanswered) - handleMissedCallNotification(payload: payload.dictionaryPayload) - - case "rejected": - provider?.reportCall(with: uuid!, endedAt: Date(), reason: .unanswered) - - case "busy": - if let uuid = uuid { - provider?.reportCall(with: uuid, endedAt: Date(), reason: .unanswered) - self.uuid = nil - } - - case "cancelled": - provider?.reportCall(with: uuid!, endedAt: Date(), reason: .failed) - handleMissedCallNotification(payload: payload.dictionaryPayload) - - case "ended": - provider?.reportCall(with: uuid!, endedAt: Date(), reason: .remoteEnded) - - default: - provider?.reportCall(with: uuid!, endedAt: Date(), reason: .remoteEnded) - } - } - - return nil - } - - public func onAnswerCallAction(action: CXAnswerCallAction) { - if activeCall != nil { - startCall() - } - action.fulfill() - } - - private func updatedInitiateCall(sender: String, senderName: String, body: String, callAction: String, receiver: String, callType: String, sessionId: String, conversationId: String) -> CXProvider? { - - let callTypeValue: CometChat.CallType = callType == "audio" ? .audio : .video - let receiverType: CometChat.ReceiverType = conversationId.contains("group") ? .group : .user - let call = Call(receiverId: receiver, callType: callTypeValue, receiverType: receiverType) - call.sessionID = sessionId - call.callStatus = .initiated - call.initiatedAt = Date().timeIntervalSince1970 - call.callInitiator = User(uid: sender, name: senderName) - call.callType = callTypeValue - call.callReceiver = User(uid: receiver, name: receiver) - - activeCall = call - uuid = UUID() - - let callerName = senderName - let config = CXProviderConfiguration(localizedName: "APNS + Callkit") - config.iconTemplateImageData = UIImage(named: "AppIcon")?.pngData() - config.includesCallsInRecents = true - config.ringtoneSound = "ringtone.caf" - config.supportsVideo = true - - provider = CXProvider(configuration: config) - - let update = CXCallUpdate() - update.remoteHandle = CXHandle(type: .generic, value: callerName.capitalized) - update.hasVideo = callType == "video" - - provider?.reportNewIncomingCall(with: uuid!, update: update, completion: { error in - if error == nil { - self.configureAudioSession() - } - }) - - return provider! - } - - private func configureAudioSession() { - do { - try AVAudioSession.sharedInstance().setCategory(AVAudioSession.Category.playAndRecord, options: [.mixWithOthers, .allowBluetooth, .defaultToSpeaker]) - try AVAudioSession.sharedInstance().setActive(true) - } catch let error as NSError { - print("Audio session error: \(error)") - } - } - - private func startCall() { - let cometChatOngoingCall = CometChatOngoingCall() - - CometChat.acceptCall(sessionID: activeCall?.sessionID ?? "") { call in - DispatchQueue.main.async { - let isAudioCall = (self.activeCall?.callType == .audio) - var callSettingsBuilder = CometChatCallsSDK.CallSettingsBuilder() - callSettingsBuilder = callSettingsBuilder.setIsAudioOnly(isAudioCall) - cometChatOngoingCall.set(callSettingsBuilder: callSettingsBuilder) - cometChatOngoingCall.set(callWorkFlow: .defaultCalling) - cometChatOngoingCall.set(sessionId: call?.sessionID ?? "") - cometChatOngoingCall.modalPresentationStyle = .fullScreen - - if let sceneDelegate = UIApplication.shared.connectedScenes.first?.delegate as? SceneDelegate, - let window = sceneDelegate.window, - let rootViewController = window.rootViewController { - var currentController = rootViewController - while let presentedController = currentController.presentedViewController { - currentController = presentedController - } - currentController.present(cometChatOngoingCall, animated: true) - } - } - - cometChatOngoingCall.setOnCallEnded { [weak self] call in - DispatchQueue.main.async { - if let scene = UIApplication.shared.connectedScenes.first(where: { $0.activationState == .foregroundActive }) as? UIWindowScene { - if let rootViewController = scene.windows.first?.rootViewController { - self?.dismissCometChatIncomingCall(from: rootViewController) - self?.reloadViewController(rootViewController) - } - } - } - self?.provider?.reportCall(with: self?.uuid ?? UUID(), endedAt: Date(), reason: .remoteEnded) - } - } onError: { error in - print("Error accepting call: \(error?.errorDescription ?? "")") - } - } - - func onCallEnded(call: CometChatSDK.Call) { - guard let uuid = uuid else { return } - - if activeCall != nil { - let transaction = CXTransaction(action: CXEndCallAction(call: uuid)) - callController.request(transaction, completion: { error in }) - activeCall = nil - } - - DispatchQueue.main.sync { [self] in - if let scene = UIApplication.shared.connectedScenes.first(where: { $0.activationState == .foregroundActive }) as? UIWindowScene { - if let rootViewController = scene.windows.first?.rootViewController { - dismissCometChatIncomingCall(from: rootViewController) - self.reloadViewController(rootViewController) - } - } - } - } - - func onCallInitiated(call: CometChatSDK.Call) { - let callerName = (call.callReceiver as? User)?.name - callController = CXCallController() - uuid = UUID() - - let transactionCallStart = CXTransaction(action: CXStartCallAction(call: uuid!, handle: CXHandle(type: .generic, value: callerName ?? ""))) - callController.request(transactionCallStart, completion: { error in }) - } - - private func dismissCometChatIncomingCall(from viewController: UIViewController) { - if let presentedViewController = viewController.presentedViewController { - if presentedViewController is CometChatIncomingCall { - presentedViewController.dismiss(animated: false, completion: nil) - } else { - dismissCometChatIncomingCall(from: presentedViewController) - } - } - } - - public func onProviderDidReset(provider: CXProvider) { - if let uuid = self.uuid { - onCall = true - provider.reportCall(with: uuid, endedAt: Date(), reason: .unanswered) - } - } - - public func onEndCallAction(action: CXEndCallAction) { - let endCallAction = CXEndCallAction(call: uuid!) - let transaction = CXTransaction() - transaction.addAction(endCallAction) - - callController.request(transaction) { error in - if let error = error { - print("Error requesting transaction: \(error)") - } else { - print("Requested transaction successfully") - } - } - - if let activeCall = activeCall { - if CometChat.getActiveCall() == nil || (CometChat.getActiveCall()?.callStatus == .initiated && CometChat.getActiveCall()?.callInitiator != CometChat.getLoggedInUser()) { - CometChat.rejectCall(sessionID: activeCall.sessionID ?? "", status: .rejected, onSuccess: { [self] (rejectedCall) in - action.fulfill() - print("CallKit: Reject call success") - DispatchQueue.main.async { [self] in - if let scene = UIApplication.shared.connectedScenes.first(where: { $0.activationState == .foregroundActive }) as? UIWindowScene { - if let rootViewController = scene.windows.first?.rootViewController { - self.dismissCometChatIncomingCall(from: rootViewController) - self.reloadViewController(rootViewController) - } - } - if let uuid = uuid { - provider?.reportCall(with: uuid, endedAt: Date(), reason: .remoteEnded) - self.uuid = nil - } - } - }) { (error) in - print("CallKit: Reject call failed: \(error?.errorDescription ?? "")") - } - } else { - CometChat.endCall(sessionID: CometChat.getActiveCall()?.sessionID ?? "") { call in - CometChatCalls.endSession() - action.fulfill() - print("CallKit: End call success") - DispatchQueue.main.async { [self] in - if let scene = UIApplication.shared.connectedScenes.first(where: { $0.activationState == .foregroundActive }) as? UIWindowScene { - if let rootViewController = scene.windows.first?.rootViewController { - self.dismissCometChatIncomingCall(from: rootViewController) - self.reloadViewController(rootViewController) - } - } - } - } onError: { error in - print("CallKit: End call failed: \(error?.errorDescription ?? "")") - } - } - } - } - -} - -extension CometChatAPNsHelper: CometChatCallEventListener { -func ccCallEnded(call: Call) { -guard let uuid = uuid else { return } - - if activeCall != nil { - let transactionCallAccepted = CXTransaction(action: CXEndCallAction(call: uuid)) - callController.request(transactionCallAccepted, completion: { error in }) - activeCall = nil - } - } - -} - -#endif - -``` - - -```swift lines -import Foundation -import UIKit -import CometChatSDK -import CometChatUIKitSwift - -class CometChatPNHelper { - -let cometchatAPNsHelper = CometChatAPNsHelper() -static var currentActiveUser: CometChatSDK.User? -static var currentActiveGroup: CometChatSDK.Group? - -static func handleTapActionOnNotification(userInfo: [AnyHashable: Any], completionHandler: @escaping () -> Void) { - guard let notificationType = userInfo["type"] as? String, - let receiverType = userInfo["receiverType"] as? String else { - print("Notification type or receiver type not found in payload") - completionHandler() - return - } - - switch notificationType { - case "chat": - if receiverType == "user" { - handleChatNotification(userInfo: userInfo) - } else if receiverType == "group" { - handleGroupChatNotification(userInfo: userInfo) - } else { - print("Invalid receiver type for chat notification") - } - - case "call": - if receiverType == "user" { - handleChatNotification(userInfo: userInfo) - } else if receiverType == "group" { - handleGroupChatNotification(userInfo: userInfo) - } else { - print("Invalid receiver type for call notification") - } - - default: - navigateToDefaultScreen() - } - - completionHandler() -} - -static func handleQuickReplyActionOnNotification(userInfo: [AnyHashable: Any], text: String, completionHandler: @escaping () -> Void) { - guard let notificationType = userInfo["type"] as? String, - let receiverType = userInfo["receiverType"] as? String else { - print("Notification type or receiver type not found in payload") - completionHandler() - return - } - - switch notificationType { - case "chat": - if receiverType == "user" { - replyToUserWith(message: text, userInfo: userInfo) - } else if receiverType == "group" { - replyToGroupWith(message: text, userInfo: userInfo) - } else { - print("Invalid receiver type for chat notification") - } - default: - break - } - - completionHandler() -} - -static func navigateToViewController(_ viewController: UIViewController) { - - guard let window = UIApplication.shared.windows.first else { - print("Window not found") - return - } - - if let navigationController = window.rootViewController as? UINavigationController { - if let currentViewController = navigationController.viewControllers.last, - currentViewController.description == viewController.description { - print("Already in same view") - return - - } - navigationController.popViewController(animated: false) - navigationController.pushViewController(viewController, animated: false) - } else { - print("Root view controller is not a UINavigationController") - } - -} - -static func replyToUserWith(message text: String, userInfo: [AnyHashable: Any], withParentId: Int? = nil) { - guard let sender = userInfo["sender"] as? String, - let senderName = userInfo["senderName"] as? String else { - print("Sender information missing in payload") - return - } - - let textMessage = TextMessage(receiverUid: sender, text: text, receiverType: .user) - if let parentID = withParentId { - textMessage.parentMessageId = parentID - } - CometChatUIKit.sendTextMessage(message: textMessage) -} - -static func replyToGroupWith(message text: String, userInfo: [AnyHashable: Any], withParentId: Int? = nil) { - guard let groupID = userInfo["receiver"] as? String, - let groupName = userInfo["receiverName"] as? String else { - print("Group information missing in payload") - return - } - - let textMessage = TextMessage(receiverUid: groupID, text: text, receiverType: .group) - if let parentID = withParentId { - textMessage.parentMessageId = parentID - } - CometChatUIKit.sendTextMessage(message: textMessage) -} - -static func handleChatNotification(userInfo: [AnyHashable: Any]) { - guard let sender = userInfo["sender"] as? String, - let senderName = userInfo["senderName"] as? String else { - print("Sender information missing in payload") - return - } - - let senderUser = User(uid: sender, name: senderName) - senderUser.avatar = userInfo["senderAvatar"] as? String - - getUser(forUID: sender) { retrievedUser in - DispatchQueue.main.async { - if let user = retrievedUser { - senderUser.status = user.status - } else { - print("Failed to retrieve user status") - } - - let chatViewController = MessagesVC() - chatViewController.user = retrievedUser - self.navigateToViewController(chatViewController) - } - - } -} - - -static func handleGroupChatNotification(userInfo: [AnyHashable: Any]) { - guard let groupID = userInfo["receiver"] as? String, - let groupName = userInfo["receiverName"] as? String else { - print("Group information missing in payload") - return - } - - let groupUser = Group(guid: groupID, name: groupName, groupType: .private, password: nil) - - self.getGroup(for: groupUser, guid: groupID) { fetchedGroup in - DispatchQueue.main.async { - if let group = fetchedGroup { - groupUser.membersCount = group.membersCount - groupUser.icon = group.icon - } else { - print("Failed to fetch group members count") - } - let chatViewController = MessagesVC() - chatViewController.group = fetchedGroup - self.navigateToViewController(chatViewController) - } - } -} - -static func handleCallNotification(userInfo: [AnyHashable: Any]) { - guard let sender = userInfo["sender"] as? String, - let senderName = userInfo["senderName"] as? String else { - print("Sender information missing in payload") - return - } - - let user = User(uid: sender, name: senderName) - user.avatar = userInfo["senderAvatar"] as? String - DispatchQueue.main.async { - let callViewController = MessagesVC() - callViewController.user = user - CometChatPNHelper.navigateToViewController(callViewController) - } -} - -static func handleGroupCallNotification(userInfo: [AnyHashable: Any]) { - guard let groupID = userInfo["receiver"] as? String, - let groupName = userInfo["receiverName"] as? String else { - print("Group information missing in payload") - return - } - - let groupUser = Group(guid: groupID, name: groupName, groupType: .private, password: nil) - groupUser.icon = userInfo["receiverAvatar"] as? String - DispatchQueue.main.async { - - let callViewController = MessagesVC() - callViewController.group = groupUser - CometChatPNHelper.navigateToViewController(callViewController) - } -} - -static func navigateToDefaultScreen() { - DispatchQueue.main.async { - let defaultViewController = MessagesVC() - - guard let window = UIApplication.shared.windows.first else { - print("Window not found") - return - } - - if let navigationController = window.rootViewController as? UINavigationController { - navigationController.pushViewController(defaultViewController, animated: true) - } else { - print("Root view controller is not a UINavigationController") - } - } -} -static func getUser(forUID uid: String, completionHandler: @escaping (CometChatSDK.User?) -> Void) { - CometChat.getUser(UID: uid, onSuccess: { user in - let user = user - completionHandler(user) - }) { error in - print("User fetching failed with error: \(error?.errorDescription ?? "Unknown error")") - completionHandler(nil) - } -} - -static func getGroup(for group: Group, guid: String, completionHandler: @escaping (Group?) -> Void) { - CometChat.getGroup(GUID: guid, onSuccess: { fetchedGroup in - completionHandler(fetchedGroup) - }) { error in - print("Group details fetching failed with error: \(error?.errorDescription ?? "Unknown error")") - completionHandler(nil) - } -} - -static func shouldPresentNotification(userInfo: [AnyHashable: Any]) -> Bool { - guard let notificationType = userInfo["type"] as? String, - let receiverType = userInfo["receiverType"] as? String else { - return true - } - - if notificationType == "chat" { - if receiverType == "user" { - let sender = userInfo["sender"] as? String - if sender == CometChatPNHelper.currentActiveUser?.uid { - return false - } - } else if receiverType == "group" { - let receiver = userInfo["receiver"] as? String - if receiver == CometChatPNHelper.currentActiveGroup?.guid { - return false - } - } - } - - return true -} -} -``` - - - -```swift lines highlight={5-8} -import Foundation -import UIKit - -class AppConstants { -static var APP_ID: String = "" -static var AUTH_KEY: String = "" -static var REGION: String = "" -static var PROVIDER_ID: String = "" -} - -extension AppConstants{ -static func saveAppConstants(){ -UserDefaults.standard.set(APP_ID, forKey: "appID") -UserDefaults.standard.set(AUTH_KEY, forKey: "authKey") -UserDefaults.standard.set(REGION, forKey: "region") -} - - static func retrieveAppConstants(){ - APP_ID = UserDefaults.standard.string(forKey: "appID") ?? AppConstants.APP_ID - AUTH_KEY = UserDefaults.standard.string(forKey: "authKey") ?? AppConstants.AUTH_KEY - REGION = UserDefaults.standard.string(forKey: "region") ?? AppConstants.REGION - } - -} - -``` - - - -## 4. Register APNs device + VoIP tokens with CometChat - -- In your `AppDelegate.swift`, implement the following methods to handle APNs registration success and failure, and to register the device token with CometChat. -- Make sure to import the necessary modules at the top of the file. -- Complete your `AppDelegate.swift` as shown below: - -```Swift lines highlight={17, 19, 22} -import UIKit -import PushKit -import CometChatSDK -import CometChatUIKitSwift - -@main -class AppDelegate: UIResponder, UIApplicationDelegate { - - var window: UIWindow? - var pushRegistry: PKPushRegistry? - let cometchatAPNsHelper = CometChatAPNsHelper() - func application( - _ application: UIApplication, - didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? - ) -> Bool { - - UNUserNotificationCenter.current().delegate = self - - cometchatAPNsHelper.configurePushNotification(application: application, delegate: self) - - // Initialize PushKit - initializePushKit() - - return true - } - - // MARK: - APNs Registration Success - func application(_ application: UIApplication, didRegisterForRemoteNotificationsWithDeviceToken deviceToken: Data) { - print("APNs Device token received!") - - if CometChat.getLoggedInUser() != nil { - print("User is logged in, registering APNs token...") - cometchatAPNsHelper.registerTokenForPushNotification(deviceToken: deviceToken) - } else { - print("User NOT logged in yet, will register token after login") - // Store token for later registration - let hexString = deviceToken.map { String(format: "%02.2hhx", $0) }.joined() - UserDefaults.standard.set(hexString, forKey: "pendingAPNsToken") - } - } - - // MARK: - APNs Registration Failure - func application(_ application: UIApplication, didFailToRegisterForRemoteNotificationsWithError error: Error) { - print("Failed to register for APNs: \(error.localizedDescription)") - } -} -``` - -## 5. Unregister the token on logout - -Before logging the user out, unregister the push token so the device stops receiving notifications for that user. - -```swift lines -CometChatNotifications.unregisterPushToken( - onSuccess: { success in - print("Push token unregistered: \(success)") - CometChatUIKit.logout(onSuccess: { _ in - print("Logout successful") - }, onError: { error in - print("Logout failed: \(error.errorDescription)") - }) - }, - onError: { error in - print("Token unregister failed: \(error.errorCode) - \(error.errorDescription)") - } -) -``` - -Always call `CometChatNotifications.unregisterPushToken()` **before** `CometChatUIKit.logout()`. If you skip this step, the device may continue to receive pushes for the logged-out user. - -## 6. Badge count - -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. - -### 6.1 Enable unread badge count on the CometChat Dashboard - -1. Go to **CometChat Dashboard → Notification Engine → 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 Expected payload format - -CometChat sends APNs payloads with this structure (relevant fields): - -```json -{ - "unreadMessageCount": "5", - "title": "New Message", - "alert": "John: Hello!", - "conversationId": "user_abc123", - "receiverType": "user" -} -``` - -`unreadMessageCount` is a string representing the total unread messages across all conversations for the logged-in user. - -### 6.3 Update the app badge from the push payload - -Inside your `UNUserNotificationCenterDelegate` method (for example `willPresent` or a Notification Service Extension), parse `unreadMessageCount` and update the badge: - -```swift lines -// Inside userNotificationCenter(_:willPresent:) or a Notification Service Extension -let userInfo = notification.request.content.userInfo - -if let unreadCountStr = userInfo["unreadMessageCount"] as? String, - let count = Int(unreadCountStr), count >= 0 { - DispatchQueue.main.async { - UIApplication.shared.applicationIconBadgeNumber = count - } -} else { - print("No valid unreadMessageCount in payload") -} -``` - -Setting `applicationIconBadgeNumber` to `0` clears the badge. - -### 6.4 Clear badge when the app opens - -Clear the badge count when the app launches and every time it returns to the foreground. In your `SceneDelegate` or `AppDelegate`: - -```swift lines -func sceneDidBecomeActive(_ scene: UIScene) { - UIApplication.shared.applicationIconBadgeNumber = 0 -} -``` - -This keeps the badge in sync with the actual unread state. - -## 7. Navigation from notifications - -When the user taps a notification, use `userNotificationCenter(_:didReceive:withCompletionHandler:)` to extract conversation details and navigate to the correct screen. - -```swift lines -func userNotificationCenter( - _ center: UNUserNotificationCenter, - didReceive response: UNNotificationResponse, - withCompletionHandler completionHandler: @escaping () -> Void -) { - let userInfo = response.notification.request.content.userInfo - - guard let receiverType = userInfo["receiverType"] as? String else { - completionHandler() - return - } - - if receiverType == "user" { - guard let senderUid = userInfo["sender"] as? String, - let senderName = userInfo["senderName"] as? String else { - completionHandler() - return - } - let user = User(uid: senderUid, name: senderName) - let messagesVC = MessagesVC() - messagesVC.user = user - navigateToViewController(messagesVC) - - } else if receiverType == "group" { - guard let groupId = userInfo["receiver"] as? String, - let groupName = userInfo["receiverName"] as? String else { - completionHandler() - return - } - let group = Group(guid: groupId, name: groupName, groupType: .public, password: nil) - let messagesVC = MessagesVC() - messagesVC.group = group - navigateToViewController(messagesVC) - } - - completionHandler() -} -``` - -The `navigateToViewController` helper (shown in `CometChatPNHelper.swift` above) pushes the `MessagesVC` onto the navigation stack. Ensure the root view controller is ready before navigation -- if the app was terminated, wait until login completes before routing. - -## 8. Testing checklist - -1. Install on a device; grant notification permission. Verify APNs device token logs. -2. Log in, then confirm both device + VoIP tokens register with CometChat (success callbacks). -3. Send a message from another user: - - Foreground: ensure `willPresent` shows your chosen presentation. - - Background/terminated: tapping opens the correct conversation. -4. Trigger an incoming call; CallKit UI should show caller info. Accept should join the call; Decline should reject via CometChat and end CallKit. -5. Rotate tokens (reinstall or toggle VoIP) to ensure re-registration works. - -## 6. Troubleshooting - -| Symptom | Quick checks | -| ---------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------- | -| No pushes | Entitlements set, APNs provider creds correct, bundle ID matches dashboard, permission granted. | -| Token registration fails | Run after login; provider IDs correct for device vs VoIP. | -| Taps do nothing | Verify notification center delegate and navigation readiness before routing. | -| Call UI missing | Ensure PushKit delegate fires, CallKit capabilities enabled, VoIP provider ID set. | -| Audio errors | Configure `AVAudioSession` for playAndRecord when reporting/accepting calls. | -| Badge count not showing | Verify **Unread Badge Count** is enabled in CometChat Dashboard and that your app reads `unreadMessageCount` from the payload. | -| Notification tap does not navigate | Ensure `UNUserNotificationCenterDelegate` is set, payload contains `receiverType`/`sender`/`receiver`, and root view controller is ready. | diff --git a/notifications/ios-fcm-push-notifications.mdx b/notifications/ios-fcm-push-notifications.mdx deleted file mode 100644 index 9bf7987e1..000000000 --- a/notifications/ios-fcm-push-notifications.mdx +++ /dev/null @@ -1,428 +0,0 @@ ---- -title: "iOS FCM Push Notifications" -description: "Guide to integrating Firebase Cloud Messaging (FCM) push notifications in iOS apps using CometChat." ---- - -FCM doesn't support VoIP pushes; use APNs + PushKit for VoIP calls. - - - Reference implementation of iOS UIKit, FCM and Push Notification Setup. - - -## What this guide covers - -- CometChat dashboard setup (enable push, add FCM iOS + APNs providers) with screenshots. -- Firebase/FCM + CometChat provider wiring (credentials, Podfile, capabilities). -- Token registration/rotation with CometChat Push Notifications (FCM iOS provider). -- Incoming message/call handling and deep links. -- Badge count and grouped notifications. -- Payload customization and testing. - -{/* ## What you need first - -- CometChat App ID, Region, Auth Key; Push Notifications with **FCM iOS provider ID** (and APNs device/VoIP provider IDs if you deliver via APNs/PushKit). -- Firebase project with an iOS app; `GoogleService-Info.plist` downloaded; APNs key uploaded to Firebase Cloud Messaging. -- Xcode capabilities: Push Notifications, Background Modes (Remote notifications, Voice over IP), CallKit usage descriptions. Physical device for push testing. */} - -## How FCM + CometChat work together - -- **FCM’s role:** Firebase issues the iOS FCM registration token and delivers the push payload. On iOS, FCM hands off to APNs using the APNs key/cert you upload in Firebase. -- **CometChat Notifications’ role:** The FCM iOS provider you create in the CometChat dashboard holds your Firebase service account. When you call `CometChatNotifications.registerPushToken(..., .FCM_IOS, providerId)`, CometChat binds that FCM token to the logged-in user and sends pushes to FCM on your behalf. -- **Flow (same bridge used in `android-push-notifications.mdx`):** Request permission → register for remote notifications → FCM returns the registration token → after `CometChat.login` succeeds, register that token with `CometChatNotifications` using the FCM provider ID → CometChat sends payloads to FCM → FCM hands to APNs → `UNUserNotificationCenterDelegate` surfaces the notification/tap. - -## 1. Enable push and add providers (CometChat Dashboard) - -1. Go to **Notifications → Settings** and enable **Push Notifications**. - - - Enable Push Notifications - - -2. On Firebase: - - Go to **Project Settings → Service accounts** and generate a new private key; download the JSON file. - - - Add FCM credentials - - -3. On CometChat Dashboard: - - Add an **FCM iOS provider** and upload your Firebase service account JSON; copy the Provider ID. - - - Add FCM credentials - - -## 2. Upload your APNs Certificates - -1. In the **Firebase Console**, go to **Project Settings → Cloud Messaging**. -2. Under **iOS app configuration**, upload your APNs authentication key or certificates. - - - Upload APNs Certificates in Firebase Console - - -## 3. Prepare Firebase and CometChat - -1. Firebase Console: download and add `GoogleService-Info.plist` to your target. - - - Upload APNs Certificates in Firebase Console - - -2. Xcode capabilities: Push Notifications, Background Modes → Remote notifications. - - - Enable Push Notifications and Background Modes for FCM - - -## 4. Add dependencies (Podfile) - -```ruby lines -target 'YourApp' do - use_frameworks! - - pod 'CometChatCallsSDK' - pod 'CometChatUIKitSwift', '5.1.7' - pod 'Firebase/Messaging' # add for FCM -end -``` - -Run `pod install`. - -## 5. Wire AppDelegate (Firebase + delegates) - -```swift lines highlight={15, 17, 19-37} -import UIKit -import FirebaseCore -import FirebaseMessaging -import CometChatUIKitSwift -import CometChatSDK - -@main -class AppDelegate: UIResponder, UIApplicationDelegate, UNUserNotificationCenterDelegate, MessagingDelegate { - - func application( - _ application: UIApplication, - didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? - ) -> Bool { - - FirebaseApp.configure() - - Messaging.messaging().delegate = self - - UNUserNotificationCenter.current().delegate = self - UNUserNotificationCenter.current().requestAuthorization( - options: [.alert, .badge, .sound] - ) { granted, error in - print("Notification permission granted:", granted) - if let error = error { - print("Permission error:", error.localizedDescription) - } - - // Only register for remote notifications if permission granted - if granted { - DispatchQueue.main.async { - print("Registering for remote notifications...") - application.registerForRemoteNotifications() - } - } else { - print("Push notification permission denied by user") - } - } - - return true - } - - func application( - _ application: UIApplication, - didRegisterForRemoteNotificationsWithDeviceToken deviceToken: Data - ) { - Messaging.messaging().apnsToken = deviceToken - - // Store the token for later registration after login - let hexString = deviceToken.map { String(format: "%02.2hhx", $0) }.joined() - UserDefaults.standard.set(hexString, forKey: "apnsPushToken") - print("APNs token received and stored: \(hexString)") - } - - func application( - _ application: UIApplication, - didFailToRegisterForRemoteNotificationsWithError error: Error - ) { - print("Failed to register for remote notifications: \(error.localizedDescription)") - } - - func application( - _ application: UIApplication, - didReceiveRemoteNotification userInfo: [AnyHashable : Any], - fetchCompletionHandler completionHandler: @escaping (UIBackgroundFetchResult) -> Void - ) { - print("Background notification received:", userInfo) - completionHandler(.newData) - } - - func messaging( - _ messaging: Messaging, - didReceiveRegistrationToken fcmToken: String? - ) { - guard let fcmToken = fcmToken else { - print("FCM token is nil") - return - } - - print("FCM Token received:", fcmToken) - - // Store FCM token as well - UserDefaults.standard.set(fcmToken, forKey: "fcmPushToken") - - if let apnsToken = UserDefaults.standard.string(forKey: "apnsPushToken") { - print("Registering APNs token with CometChat: \(apnsToken)") - CometChatNotifications.registerPushToken( - pushToken: fcmToken, - platform: CometChatNotifications.PushPlatforms.FCM_IOS, - providerId: AppConstants.PROVIDER_ID, - onSuccess: { success in - print("APNs token registered with CometChat: \(success)") - }, - onError: { error in - print("APNs token registration failed: \(error.errorCode) - \(error.errorDescription)") - } - ) - } else { - print("No stored APNs token found - Check if Push Notifications capability is enabled in Xcode") - } - } - - - func userNotificationCenter( - _ center: UNUserNotificationCenter, - willPresent notification: UNNotification, - withCompletionHandler completionHandler: @escaping (UNNotificationPresentationOptions) -> Void - ) { - completionHandler([.banner, .sound, .badge]) - } - - // Call this method AFTER CometChat login succeeds - static func registerStoredPushToken() { - guard CometChat.getLoggedInUser() != nil else { - print("Cannot register push token: User not logged in") - return - } - - // Register APNs token - if let apnsToken = UserDefaults.standard.string(forKey: "apnsPushToken") { - print("Registering APNs token with CometChat: \(apnsToken)") - CometChatNotifications.registerPushToken( - pushToken: apnsToken, - platform: CometChatNotifications.PushPlatforms.APNS_IOS_DEVICE, - providerId: AppConstants.PROVIDER_ID, - onSuccess: { success in - print("APNs token registered with CometChat: \(success)") - }, - onError: { error in - print("APNs token registration failed: \(error.errorCode) - \(error.errorDescription)") - } - ) - } else { - print("No stored APNs token found - Check if Push Notifications capability is enabled in Xcode") - } - - // Register FCM token - if let fcmToken = UserDefaults.standard.string(forKey: "fcmPushToken") { - print("Registering FCM token with CometChat...") - CometChat.registerTokenForPushNotification( - token: fcmToken, - onSuccess: { message in - print("CometChat FCM token registered successfully") - }, - onError: { error in - print("CometChat FCM token registration failed:", error?.errorDescription ?? "Unknown error") - } - ) - } else { - print("No stored FCM token found") - } - } -} -``` - -**What this code is doing** - -- Initializes Firebase, sets `MessagingDelegate` and `UNUserNotificationCenterDelegate`, asks for alert/badge/sound permission, and registers for remote notifications. -- Sets `Messaging.messaging().apnsToken` so FCM can map the APNs token and later deliver via APNs. -- Stores the FCM registration token and calls `registerStoredPushToken()` so CometChat can bind the token to your logged-in user (using the Provider ID you configured). -- Leaves `willPresent` to show banners/sounds in foreground instead of silently ignoring the notification. - -## 6. Create FCM helper - -Create `CometChatFCMHelper.swift` and add: - -```swift -import UIKit -import UserNotifications - -final class CometChatFCMHelper { - - static let shared = CometChatFCMHelper() - private init() {} - - /// Configure push notifications (iOS-safe) - func configure(application: UIApplication, delegate: UNUserNotificationCenterDelegate) { - - // 1. Request permission - UNUserNotificationCenter.current().requestAuthorization( - options: [.alert, .badge, .sound] - ) { granted, error in - print("Notification permission:", granted) - if let error = error { - print("Permission error:", error.localizedDescription) - } - } - - // 2. Set delegate - UNUserNotificationCenter.current().delegate = delegate - - // 3. Register for APNs - DispatchQueue.main.async { - application.registerForRemoteNotifications() - } - } -} -``` - -{/* Alternative: pass the FCM token into `UIKitSettings` via `.set(fcmKey:)` before `CometChatUIKit.init` (e.g., `SceneDelegate`), alongside APNs device/VoIP tokens. */} - -**What this helper is doing** - -- Wraps permission prompts + delegate wiring so you can call one method from `AppDelegate`. -- Sets the notification delegate early and registers for APNs on the main queue (Apple requirement). -- Keeps all push setup in a reusable singleton to call from tests, scenes, or multi-target setups. - -## 7. 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. Use this to set the app icon badge and enrich local notifications. - -### 7.1 Enable unread badge count on the CometChat Dashboard - - - - Go to **CometChat Dashboard → Notifications → Settings → Preferences → Push - Notification Preferences**. - - - 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. - -### 7.2 Clear badge count when app becomes active - -Add the following to your `SceneDelegate.swift` to reset the badge when the user opens the app: - -```swift -func sceneDidBecomeActive(_ scene: UIScene) { - // Clear badge count when user opens the app - UIApplication.shared.applicationIconBadgeNumber = 0 - print("Badge count cleared") -} -``` - -### 7.3 Update badge count from push notifications - -Update your `AppDelegate.swift` to handle badge count updates in both foreground and background states: - - - -```swift -func userNotificationCenter( - _ center: UNUserNotificationCenter, - willPresent notification: UNNotification, - withCompletionHandler completionHandler: @escaping (UNNotificationPresentationOptions) -> Void -) { - let userInfo = notification.request.content.userInfo - print("Will present notification: \(userInfo)") - - // Update badge count from payload - if let unreadCountString = userInfo["unreadMessageCount"] as? String, - let unreadCount = Int(unreadCountString) { - if unreadCount >= 0 { - UIApplication.shared.applicationIconBadgeNumber = unreadCount - print("Badge count updated (foreground): \(unreadCount)") - } - } else { - print("No unreadMessageCount in payload") - } - - completionHandler([.banner, .badge, .sound]) -} -``` - - -```swift -func application( - _ application: UIApplication, - didReceiveRemoteNotification userInfo: [AnyHashable: Any], - fetchCompletionHandler completionHandler: @escaping (UIBackgroundFetchResult) -> Void -) { - print("Received remote notification: \(userInfo)") - - // Update badge count from payload - if let unreadCountString = userInfo["unreadMessageCount"] as? String, - let unreadCount = Int(unreadCountString) { - if unreadCount >= 0 { - UIApplication.shared.applicationIconBadgeNumber = unreadCount - print("Badge count updated (background): \(unreadCount)") - } - } else { - print("No unreadMessageCount in payload") - } - - completionHandler(.newData) -} -``` - - - - - The `unreadMessageCount` field is sent as a string in the payload. Always - parse it to an integer and validate it's non-negative before updating the - badge. - - -## 8. Testing checklist - -1. `FirebaseApp.configure()` runs; FCM token logs after login; registration with CometChat succeeds. -2. Message from another user: - - Foreground: `willPresent` behavior as expected. - - Background/terminated: tap opens the correct chat. -3. Rotate the FCM token (`didReceiveRegistrationToken`) and confirm re-registration. -4. VoIP: VoIP token registers; incoming call shows CallKit; Accept/Decline controls the CometChat call. - -## 9. Troubleshooting - -| Symptom | Quick checks | -| ------------------------ | ----------------------------------------------------------------------------------------------------------------------- | -| No FCM pushes | `GoogleService-Info.plist` present; APNs key uploaded to Firebase; bundle ID matches; permission granted. | -| Token registration fails | Run after login; provider ID matches FCM iOS provider; `Messaging.messaging().delegate` set. | -| Taps ignored | Ensure `UNUserNotificationCenterDelegate` methods fire and navigation is ready before routing. | -| Call UI missing | Add PushKit + CallKit wiring; register VoIP token with an APNs VoIP provider; ensure VoIP/background modes are enabled. | diff --git a/notifications/ios-push-notifications.mdx b/notifications/ios-push-notifications.mdx new file mode 100644 index 000000000..71c963708 --- /dev/null +++ b/notifications/ios-push-notifications.mdx @@ -0,0 +1,373 @@ +--- +title: "iOS" +description: "Add CometChat push notifications and VoIP calls to an iOS app with the drop-in CometChatPushNotifications SDK." +--- + +## What this guide covers + +- Adding the `CometChatPushNotifications` SDK and initializing it. +- Wiring `AppDelegate` to forward APNs callbacks to the SDK and register for VoIP pushes. +- Letting the SDK handle APNs alerts (foreground presentation, taps, quick reply, badge) and the full incoming-call experience via PushKit + CallKit. +- Handling notification taps and presenting the ongoing call screen via the delegate. +- Suppressing notifications for the chat that is currently open, clearing the badge on resume, and telling the SDK when the Calls stack is ready. +- Testing and troubleshooting. + + +The `CometChatPushNotifications` SDK replaces the previous approach of copying `CometChatAPNsHelper.swift`, `CometChatPNHelper.swift`, and the `AppDelegate+PN` / `AppDelegate+VoIP` extensions from the UI Kit sample. APNs token registration, foreground presentation, tap-to-open, quick reply, badge management, delivery / campaign engagement receipts, and the full PushKit + CallKit incoming-call flow are all handled inside the SDK. + + +## How it works + +- **APNs is the transport:** Apple issues the APNs device token (chat alerts) and the PushKit VoIP token (calls) — no FCM bridge is involved. +- **CometChat's role:** The **APNs Device** and **APNs VoIP** providers you add in the dashboard hold your `.p8` key. The SDK registers both tokens with CometChat automatically after login, so CometChat can route pushes on your behalf. +- **The SDK's role:** `CometChatPushNotifications` retrieves the tokens, registers them with CometChat, parses payloads, decides chat vs. call, presents CallKit for VoIP pushes, and calls back into your `CometChatPushNotificationsDelegate` for navigation and to present the ongoing call screen. +- **Cold-start VoIP:** when the app is killed and a VoIP push arrives, PushKit wakes the app and the SDK presents the CallKit screen natively before any UI is built. On accept, `presentCallScreen(for:sessionId:)` fires once your `notifyCallsSDKReady()` has run. + +## Prerequisites + +- The APNs Device provider, APNs VoIP provider, and `.p8` setup from **[Getting Started](/notifications/push-getting-started)** (this guide assumes those are done). +- The CometChat **Chat SDK / UI Kit** already integrated (you call `CometChatUIKit.init()` / `login()` yourself). +- **Xcode 15+**, **iOS 15.1+** deployment target, **Swift 5.9+**. +- **CometChatSDK ≥ 4.1.5** and **CometChatCallsSDK ≥ 5.0.0** (SPM users must add these packages explicitly — see below). +- A physical device — APNs, PushKit, and CallKit do not work on the simulator. + + +**Complete the [Getting Started](/notifications/push-getting-started) guide first** — enable Push Notifications, add your **APNs Device** and **APNs VoIP** providers, and finish the Apple / Xcode setup (`.p8` key + capabilities). This guide covers only the iOS app wiring. + + +## 1. Add the dependency + + + + In Xcode, go to **File → Add Package Dependencies…** and add: + + ``` + https://github.com/cometchat/push-notifications-sdk-ios.git + ``` + + Add the product **`CometChatPushNotificationsSwift`** to your app target. + + + SPM does not resolve transitive dependencies for prebuilt xcframeworks. You **must** also add these two packages to your project or the build fails with `Unable to resolve module dependency`: + + | Package | Minimum version | Product | + | --- | --- | --- | + | `https://github.com/cometchat/chat-sdk-ios.git` | **4.1.5** | `CometChatSDK` | + | `https://github.com/cometchat/calls-sdk-ios.git` | **5.0.0** | `CometChatCallsSDK` | + + + + ```ruby Podfile + platform :ios, '15.1' + + target 'YourApp' do + use_frameworks! + pod 'CometChatPushNotifications', '1.0.0' + end + ``` + + CocoaPods resolves `CometChatSDK` and `CometChatCallsSDK` for you. + + ```bash + pod install + ``` + + Open the generated `.xcworkspace` from here on. + + + +## 2. Configure the app target + +1. **Signing & Capabilities → + Capability** — add **Push Notifications** and **Background Modes**. Under Background Modes tick **Remote notifications**, **Voice over IP**, and **Audio, AirPlay, and Picture in Picture** (needed while a call is active). + + + Enable Push Notifications and Background Modes + + +2. **Entitlements** — Push Notifications adds `aps-environment` for you (`development` for debug builds, `production` for release). + +3. **Info.plist** — add microphone and camera usage strings so CallKit can request them, and confirm the background modes are present: + + ```xml + NSMicrophoneUsageDescription + Required for voice and video calls. + NSCameraUsageDescription + Required for video calls. + + UIBackgroundModes + + remote-notification + voip + audio + + ``` + +4. In the CometChat dashboard, keep the **Bundle ID** on your APNs providers in sync with your Xcode target's bundle ID. + +## 3. Store your credentials + +Keep the values from Getting Started where your app can read them. The provider ID here is the **APNs Device** provider — the SDK also registers the VoIP token against the same provider automatically: + +```swift AppConstants.swift +enum AppConstants { + static let APP_ID = "YOUR_APP_ID" + static let REGION = "YOUR_REGION" + static let AUTH_KEY = "YOUR_AUTH_KEY" + static let PROVIDER_ID = "APNS-PROVIDER-ID" +} +``` + +## 4. Initialize the SDK in `AppDelegate` + +Initialize the SDK in `application(_:didFinishLaunchingWithOptions:)`, set yourself as the delegate, register for VoIP pushes, and forward the three APNs `UIApplicationDelegate` callbacks. The SDK cannot intercept these callbacks itself — you must forward them: + +```swift AppDelegate.swift +import UIKit +import CometChatSDK +import CometChatUIKitSwift +import CometChatPushNotificationsSwift +#if canImport(CometChatCallsSDK) +import CometChatCallsSDK +#endif + +@main +class AppDelegate: UIResponder, UIApplicationDelegate { + + func application(_ application: UIApplication, + didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool { + // Your existing CometChat UI Kit / Chat SDK initialization runs elsewhere + // (typically in SceneDelegate) — the push SDK does not need to run before it. + + let config = CometChatPushNotificationsConfig( + providerId: AppConstants.PROVIDER_ID, + enableBadgeCount: true, + showInAppNotifications: true, + foregroundCallPresentation: .callKit // native CallKit UI, foreground included + ) + CometChatPushNotifications.shared.initialize(config: config) + CometChatPushNotifications.shared.delegate = self + CometChatPushNotifications.shared.registerForVoIPPushes() + return true + } + + // MARK: - APNs callbacks (forward all three to the SDK) + + func application(_ application: UIApplication, + didRegisterForRemoteNotificationsWithDeviceToken deviceToken: Data) { + CometChatPushNotifications.shared.registerDeviceToken(deviceToken) + } + + func application(_ application: UIApplication, + didFailToRegisterForRemoteNotificationsWithError error: Error) { + CometChatPushNotifications.shared.handleRegistrationFailure(error) + } + + func application(_ application: UIApplication, + didReceiveRemoteNotification userInfo: [AnyHashable: Any], + fetchCompletionHandler completionHandler: @escaping (UIBackgroundFetchResult) -> Void) { + CometChatPushNotifications.shared.handleBackgroundNotification(userInfo: userInfo) + completionHandler(.newData) + } +} +``` + +`CometChatPushNotificationsConfig` accepts: + +| Field | Default | Purpose | +| --- | --- | --- | +| `providerId` | required | The **APNs Device** provider ID from your dashboard. | +| `enableBadgeCount` | `false` | Set the app icon badge from the payload's `unreadMessageCount`. Pair with `clearBadgeCount()` on `sceneDidBecomeActive`. | +| `showInAppNotifications` | `false` | Show a banner for chat pushes while the app is in the foreground. Suppressed automatically for the currently open chat (see step 6). | +| `foregroundCallPresentation` | `.callKit` | How to render an incoming VoIP call that arrives in the foreground. `.callKit` uses the native iOS call screen; use your custom UI only if you disable the UI Kit's in-app incoming call. | + + +Token registration and rotation are handled automatically. After a successful `CometChat.login(...)`, the SDK binds the stored APNs and VoIP tokens to the user, and it re-registers them if either token changes. You do **not** need to call any `registerToken` API yourself. + + +## 5. Wire `SceneDelegate` — notify Calls SDK and clear badge + +Call `notifyCallsSDKReady()` **after** `CometChatUIKit.init(...)` succeeds so the SDK can present any incoming call that arrived during a cold start. Clear the badge every time the scene becomes active. Also disable the UI Kit's in-app incoming call so CallKit is not doubled up: + +```swift SceneDelegate.swift +import UIKit +import CometChatUIKitSwift +import CometChatSDK +import CometChatPushNotificationsSwift + +class SceneDelegate: UIResponder, UIWindowSceneDelegate { + + var window: UIWindow? + + func scene(_ scene: UIScene, willConnectTo session: UISceneSession, + options: UIScene.ConnectionOptions) { + let settings = UIKitSettings() + .set(appID: AppConstants.APP_ID) + .set(authKey: AppConstants.AUTH_KEY) + .set(region: AppConstants.REGION) + .enable(inAppIncomingCall: false) // let CallKit own the incoming call UI + .build() + + CometChatUIKit.init(uiKitSettings: settings) { result in + if case .success = result { + // Tell the push SDK the Calls stack is ready so a call that arrived + // during cold start (VoIP push wakes the app in the killed state) can be presented. + CometChatPushNotifications.shared.notifyCallsSDKReady() + } + } + } + + func sceneDidBecomeActive(_ scene: UIScene) { + CometChatPushNotifications.shared.clearBadgeCount() + } +} +``` + +## 6. Suppress notifications for the open chat + +On every chat screen, tell the SDK which conversation is active so it doesn't show a banner for messages you can already see. Clear it when the screen goes away: + +```swift MessagesVC.swift +import CometChatUIKitSwift +import CometChatPushNotificationsSwift + +override func viewWillAppear(_ animated: Bool) { + super.viewWillAppear(animated) + if let uid = user?.uid { + CometChatPushNotifications.shared.setActiveConversation(userId: uid) + } else if let guid = group?.guid { + CometChatPushNotifications.shared.setActiveConversation(groupId: guid) + } +} + +override func viewWillDisappear(_ animated: Bool) { + super.viewWillDisappear(animated) + CometChatPushNotifications.shared.clearActiveConversation() +} +``` + +## 7. Handle taps and calls via the delegate + +Adopt `CometChatPushNotificationsDelegate` to route notification taps and present the ongoing call screen once the SDK has accepted a call via CallKit: + +```swift AppDelegate+PN.swift +import CometChatSDK +import CometChatUIKitSwift +import CometChatPushNotificationsSwift +#if canImport(CometChatCallsSDK) +import CometChatCallsSDK +#endif + +extension AppDelegate: CometChatPushNotificationsDelegate { + + // MARK: - Chat notification taps + func navigateToChat(for user: CometChatSDK.User) { + pushMessages(user: user, group: nil) + } + + func navigateToChat(for group: CometChatSDK.Group) { + pushMessages(user: nil, group: group) + } + + func navigateToDefaultScreen() { + DispatchQueue.main.async { + self.topMostNavigationController()?.popToRootViewController(animated: true) + } + } + + // MARK: - Calls (CallKit already accepted — just present your ongoing-call screen) + #if canImport(CometChatCallsSDK) + func presentCallScreen(for call: CometChatSDK.Call, sessionId: String) { + DispatchQueue.main.async { + let ongoing = CometChatOngoingCall() + let isAudioOnly = (call.callType == .audio) + let builder = CometChatCallsSDK.CallSettingsBuilder() + .setIsAudioOnly(isAudioOnly) + ongoing.set(callSettingsBuilder: builder) + ongoing.set(callWorkFlow: .defaultCalling) + ongoing.set(sessionId: sessionId) + ongoing.modalPresentationStyle = .fullScreen + ongoing.setOnCallEnded { _ in + // ALWAYS report call end back to CallKit so the system UI dismisses. + CometChatPushNotifications.shared.endCallKitSession() + ongoing.dismiss(animated: true) + } + self.topMostViewController()?.present(ongoing, animated: true) + } + } + + func onCallCleanupComplete() { + // Called when the SDK fully ended the call (e.g. the user hit End on the + // native CallKit screen). Dismiss the ongoing-call screen if still on top. + DispatchQueue.main.async { + self.dismissIfPresenting(CometChatOngoingCall.self) + } + } + + func onCallMissed(call: CometChatSDK.Call, reason: CometChatMissedCallReason) { + // The SDK already posted the missed-call notification — hook for call logs / analytics. + } + + func onCallMuteStateChanged(isMuted: Bool) { + // Fires when the CallKit screen (or the Calls SDK) toggles mute. + } + #endif + + func onPushTokenRegistrationFailed(platform: CometChatPushTokenPlatform, + error: CometChatSDK.CometChatException) { + // Fires for automatic registrations too (VoIP capture, login re-registration). + print("Push token registration failed [\(platform.rawValue)]: \(error.errorDescription)") + } +} +``` + + +`pushMessages(user:group:)`, `topMostViewController()`, `topMostNavigationController()`, and `dismissIfPresenting(_:)` above are helpers you define in your `AppDelegate` — they know your app's navigation structure. Delegate callbacks can also fire from background threads (PushKit and notification-response callbacks in particular), so wrap all UIKit work in `DispatchQueue.main.async`. + + +## 8. Badge count + +CometChat's Enhanced Push Notification payload includes an `unreadMessageCount` field (total unread across all conversations). Enable it once on the dashboard: + +1. **CometChat Dashboard → Notification Engine → Settings → Preferences → Push Notification Preferences**. +2. Enable the **Unread Badge Count** toggle. + +With `enableBadgeCount: true` on the config, iOS updates the app-icon badge from the APNs payload automatically — no extra client code is needed to display it. Clear the badge and dismiss stale notifications every time the app resumes by calling `clearBadgeCount()` from `sceneDidBecomeActive` (already wired in step 5). + +## 9. Testing checklist + +1. Install on a **physical device**. Grant notification, microphone, and camera permissions when prompted. +2. Log in and confirm the delegate does **not** report `onPushTokenRegistrationFailed` — that means both the APNs device token and the VoIP token were registered against your providers. +3. Send a message from another user: + - App foregrounded on a different chat: banner appears (if `showInAppNotifications: true`). + - App foregrounded on the **same** chat: no banner (foreground suppression from step 6). + - App backgrounded / killed: notification appears; tapping opens the right conversation via `navigateToChat(for:)`. + - Long-press the notification → **Reply** — the quick-reply text is sent as a message in that conversation. +4. Trigger an incoming CometChat call and confirm: + - The **CallKit** incoming screen shows the caller with Accept / Decline, even on the lock screen. + - Accepting presents your ongoing call screen from `presentCallScreen(for:sessionId:)`. + - Ending the call from either the CallKit screen or your ongoing screen tears down cleanly (via `endCallKitSession()` and `onCallCleanupComplete()`). + - A missed call fires `onCallMissed(call:reason:)` and posts a missed-call notification. +5. Force-quit the app, trigger a call, and confirm the CallKit UI still appears — this exercises the PushKit cold-start path. + +## 10. Troubleshooting + +| Symptom | Quick checks | +| --- | --- | +| No notifications received | Confirm **Push Notifications** capability is enabled, the target's bundle ID matches the APNs Device provider in the dashboard, and your `AppDelegate` forwards `didReceiveRemoteNotification` to `handleBackgroundNotification(userInfo:)`. Verify APNs environment: `.p8` covers both dev and prod but the `aps-environment` entitlement (`development` vs `production`) must match your build. | +| `onPushTokenRegistrationFailed` fires | Verify `PROVIDER_ID` matches the dashboard exactly and that CometChat login has succeeded before you expect a token to bind. | +| Incoming call screen never shows | Confirm **Background Modes → Voice over IP** is enabled, `registerForVoIPPushes()` runs in `didFinishLaunchingWithOptions`, and the APNs VoIP provider is configured in the dashboard. | +| Two incoming-call screens (CallKit + in-app) | Call `.enable(inAppIncomingCall: false)` in your `UIKitSettings` so only CallKit shows the incoming UI. | +| CallKit screen never dismisses after call ends | Always call `CometChatPushNotifications.shared.endCallKitSession()` from your ongoing call screen's on-ended callback. | +| Nothing happens on a killed-app call | Ensure `notifyCallsSDKReady()` runs after `CometChatUIKit.init` succeeds — otherwise the SDK cannot present the ongoing screen once the user accepts from CallKit. | +| Build fails with `Unable to resolve module dependency` (SPM) | Add `cometchat/chat-sdk-ios` (≥ 4.1.5) and `cometchat/calls-sdk-ios` (≥ 5.0.0) as separate SPM packages — SPM does not resolve transitive deps for prebuilt xcframeworks. | +| Testing on the simulator | APNs, PushKit, and CallKit require a real device — the simulator only confirms the app compiles. | + +## Resources + + + Source, releases, and installation instructions (SPM & CocoaPods) for the drop-in push & VoIP SDK. + diff --git a/notifications/push-getting-started.mdx b/notifications/push-getting-started.mdx new file mode 100644 index 000000000..b89d1b284 --- /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)** 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**. + + + 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). + + + Upload FCM service account JSON + + +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. + + + Firebase - Push Notifications + + +## 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. + + + + Enable Push Notifications and Background Modes for APNs + + +## 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-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 b6d372453..90e3825a6 100644 --- a/notifications/push-overview.mdx +++ b/notifications/push-overview.mdx @@ -32,46 +32,6 @@ CometChat listens for chat and call events, assembles payloads from your templat **Call notifications and group calls:** Call-related push notifications (e.g., initiated calls, missed calls) are sent as **custom messages**, not as group action messages. The **production mode toggle** in the dashboard only affects APNs endpoint routing (sandbox vs production) — it does not change how the Notification Engine constructs the push payload. If you experience APNs errors (e.g., 400 status) for call pushes, check your APNs provider credentials and certificate configuration rather than the production mode toggle. -## Before you integrate +## Next: Getting Started -1. **Dashboard setup**: Enable Push Notifications, add providers (FCM for Android/iOS, APNs + optional APNs VoIP for iOS), and copy Provider IDs. -2. **Credentials**: Keep App ID, Region, Auth Key handy. Upload the Firebase service account JSON and APNs `.p8` key (Team ID + Key ID). -3. **Client wiring**: Register tokens after login and unregister on logout. Follow the platform guide below for Gradle/Info.plist/CallKeep specifics. - -## Choose your platform - - - -} href="/notifications/android-push-notifications"> -UI Kit implementation - - -} href="/notifications/ios-apns-push-notifications"> -UI Kit implementation - - -} href="/notifications/ios-fcm-push-notifications"> -UI Kit implementation - - -} href="/notifications/flutter-push-notifications-android"> -UI Kit implementation - - -} href="/notifications/flutter-push-notifications-ios"> -UI Kit implementation - - -} 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 - - - +Ready to integrate? Head to **[Getting Started](/notifications/push-getting-started)** to enable push, add your providers, configure Firebase/APNs, and pick your platform guide. diff --git a/notifications/react-native-push-notifications-android.mdx b/notifications/react-native-push-notifications-android.mdx index a51e7a886..2b17ddee0 100644 --- a/notifications/react-native-push-notifications-android.mdx +++ b/notifications/react-native-push-notifications-android.mdx @@ -1,5 +1,5 @@ --- -title: "React Native Push Notification (Android)" +title: "React Native (Android)" description: "Bring the SampleAppWithPushNotifications experience—FCM + VoIP calls—into any React Native project using CometChat UI Kit." --- @@ -25,8 +25,7 @@ description: "Bring the SampleAppWithPushNotifications experience—FCM + VoIP c ## What this guide covers -- CometChat Dashboard setup (enable push, add FCM providers). -- Platform credentials (Firebase). +- Prerequisite: the shared dashboard + Firebase setup in the [Getting Started](/notifications/push-getting-started) guide. - Copying the sample notification stack and aligning IDs/provider IDs. - Native glue for Android (manifest permissions). - VoIP call alerts with FCM data-only pushes + CallKeep native dialer. @@ -44,32 +43,11 @@ description: "Bring the SampleAppWithPushNotifications experience—FCM + VoIP c - **CometChat provider holds your credentials:** The FCM provider you add (for React Native Android) stores your Firebase service account JSON. - **Registration flow:** Request permission → Android returns the FCM token → after `CometChat.login`, register with `CometChatNotifications.registerPushToken(token, platform, providerId)` using `FCM_REACT_NATIVE_ANDROID` → CometChat sends pushes to FCM on your behalf → the app handles taps/foreground events via Notifee. -## 1. Enable push and add providers (CometChat Dashboard) + +**Complete the [Getting Started](/notifications/push-getting-started) guide first** — enable Push Notifications, add your FCM provider, and configure Firebase (`google-services.json` + service account JSON). This guide covers only the React Native app wiring. + -1. Go to **Notifications → Settings** and enable **Push Notifications**. - - - Enable Push Notifications - - -2. Add an **FCM** provider for React Native Android; upload the Firebase service account JSON and copy the Provider ID. - - - Upload FCM service account JSON - - -## 2. Prepare platform credentials - -### 2.1 Firebase Console - -1. Register your Android package name (same as `applicationId` in `android/app/build.gradle`) and download `google-services.json` into `android/app`. -2. Enable Cloud Messaging. - - - Firebase - Push Notifications - - -## 3. Local configuration +## 1. Local configuration - Update `src/utils/AppConstants.tsx` with `appId`, `authKey`, `region`, and `fcmProviderId`. - Keep `app.json` name consistent with your bundle ID / applicationId. @@ -81,7 +59,7 @@ const REGION = ""; const DEMO_UID = "cometchat-uid-1"; ``` -### 3.1 Dependencies snapshot (from Sample App) +### 1.1 Dependencies snapshot (from Sample App) Install these dependencies in your React Native app: @@ -100,9 +78,9 @@ npm install \ Match these or newer compatible versions in your app. -## 4. Android App Setup +## 2. Android App Setup -### 4.1 Configure Firebase with Android credentials +### 2.1 Configure Firebase with Android credentials To allow Firebase on Android to use the credentials, the `google-services` plugin must be enabled on the project. This requires modification to two files in the Android directory. @@ -124,7 +102,7 @@ apply plugin: 'com.android.application' apply plugin: 'com.google.gms.google-services' ``` -### 4.2 Configure required permissions in `AndroidManifest.xml` as shown. +### 2.2 Configure required permissions in `AndroidManifest.xml` as shown. ```xml lines @@ -181,7 +159,7 @@ import { PermissionsAndroid, Platform } from "react-native"; } ``` -### 4.3 Register FCM token with CometChat +### 2.3 Register FCM token with CometChat Inside your main app file where you initialize CometChat, add the below code snippet after the user has logged in successfully. Initilize and register the FCM token for Android as shown: @@ -206,16 +184,16 @@ CometChatNotifications.registerPushToken( }); ``` -### 4.4 Unregister FCM token on logout +### 2.4 Unregister FCM token on logout Typically, push token unregistration should occur prior to user logout, using the `CometChat.logout()` method. For token unregistration, use the `CometChatNotifications.unregisterPushToken()` method provided by the SDKs. -## 5. VoIP call notifications +## 3. VoIP call notifications These steps are Android-only—copy/paste and fill your IDs. -### 5.1 Add CallKeep services to `android/app/src/main/AndroidManifest.xml` +### 3.1 Add CallKeep services to `android/app/src/main/AndroidManifest.xml` Inside the `` tag add: ```xml lines @@ -232,7 +210,7 @@ Inside the `` tag add: ``` -### 5.2 Background handler for call pushes (`index.js`) +### 3.2 Background handler for call pushes (`index.js`) Data-only FCM calls show the native dialer even when the app is killed. ```js lines @@ -279,7 +257,7 @@ if (Platform.OS === "android") { } ``` -### 5.3 Drop in `VoipNotificationHandler.ts` +### 3.3 Drop in `VoipNotificationHandler.ts` Handles CallKeep setup, shows the incoming call UI, accepts/rejects via CometChat, and defers acceptance if login/navigation isn’t ready. ```ts lines @@ -484,7 +462,7 @@ class VoipNotificationHandler { export const voipHandler = new VoipNotificationHandler(); ``` -### 5.4 Add `PendingCallManager.ts` +### 3.4 Add `PendingCallManager.ts` Stores an answered call during cold-start so you can accept it once login/navigation is ready. ```ts lines @@ -528,7 +506,7 @@ export function isPendingStale(p: PendingAnsweredCallPayload, maxAgeMs = 2 * 60 } ``` -### 5.5 Wire `App.tsx` to init VoIP + consume pending accepts +### 3.5 Wire `App.tsx` to init VoIP + consume pending accepts Add this after CometChat init/login: ```ts lines @@ -570,7 +548,7 @@ useEffect(() => { }, []); ``` -### 5.6 Call push payload (FCM data) +### 3.6 Call push payload (FCM data) Send a data-only FCM message like: ```json @@ -587,7 +565,7 @@ Send a data-only FCM message like: } ``` -### 5.7 Local notification helper (`LocalNotificationHandler.ts`) +### 3.7 Local notification helper (`LocalNotificationHandler.ts`) > Ensure `@notifee/react-native` is installed (listed in Dependencies above). Add this helper next to your `index.js` to show local alerts for non-call pushes: @@ -640,18 +618,18 @@ export async function displayLocalNotification(remoteMessage: any) { ``` - For a proper notification icon, create a dedicated `ic_notification.xml` (vector) or PNG in `android/app/src/main/res/drawable/`; Android expects a white glyph with transparency for best results. -## 6. Handling notification taps and navigation +## 4. Handling notification taps and navigation To handle notification taps and navigate to the appropriate chat screen, you need to set up handlers for both foreground and background notifications. {/* :TODO: Add code snippets and explanation for setting up Notifee handlers and navigation logic. */} -## 7. Badge Count Implementation +## 5. Badge Count Implementation CometChat's Enhanced Push Notification payload includes an `unreadMessageCount` field that represents the total number of unread messages across all conversations for the logged-in user. You can use this value to update the app icon badge, providing users with a visual indicator of unread messages. -### 7.1 Enable Unread Badge Count on the CometChat Dashboard +### 5.1 Enable Unread Badge Count on the CometChat Dashboard @@ -664,7 +642,7 @@ CometChat's Enhanced Push Notification payload includes an `unreadMessageCount` Once enabled, CometChat automatically includes the `unreadMessageCount` field in every push payload sent to your app. -### 7.2 Expected Payload Format +### 5.2 Expected Payload Format CometChat sends push notifications with the following structure: @@ -685,7 +663,7 @@ CometChat sends push notifications with the following structure: The `unreadMessageCount` field is a **string** representing the total unread messages across all conversations for the logged-in user. -### 7.3 Handle Badge Count in Background Messages +### 5.3 Handle Badge Count in Background Messages Update your FCM background message handler in `index.js` to extract and set the badge count: @@ -715,7 +693,7 @@ messaging().setBackgroundMessageHandler(async (remoteMessage) => { }); ``` -### 7.4 Handle Badge Count in Foreground Messages +### 5.4 Handle Badge Count in Foreground Messages In your `App.tsx`, set up a listener for foreground FCM messages: @@ -749,7 +727,7 @@ useEffect(() => { }, []); ``` -### 7.5 Display Local Notification with Badge Count +### 5.5 Display Local Notification with Badge Count Update your notification display function to include the badge count: @@ -810,7 +788,7 @@ export async function displayLocalNotification(remoteMessage: any) { } ``` -### 7.6 Clear Badge When App Becomes Active +### 5.6 Clear Badge When App Becomes Active Clear all notifications and reset the badge when the app returns to the foreground: @@ -832,7 +810,7 @@ useEffect(() => { }, []); ``` -### 7.7 Clear Badge on Logout +### 5.7 Clear Badge on Logout When a user logs out, clear the badge so it doesn't show a stale count on the login screen or for the next user: @@ -854,7 +832,7 @@ const handleLogout = async () => { }; ``` -### 7.8 Clear Badge on Fresh Install / No Logged-In User +### 5.8 Clear Badge on Fresh Install / No Logged-In User Clear the badge during app initialization when no user is logged in. This handles cases where badge count may persist after app reinstall: @@ -879,7 +857,7 @@ const initializeApp = async () => { }; ``` -### 7.9 Clear Badge in Login Listener (Safety Net) +### 5.9 Clear Badge in Login Listener (Safety Net) Register a login listener to clear the badge on logout as a backup mechanism: @@ -908,7 +886,7 @@ useEffect(() => { }, []); ``` -### 7.10 Key Implementation Notes +### 5.10 Key Implementation Notes | Consideration | Details | | --- | --- | @@ -920,7 +898,7 @@ useEffect(() => { | **Login listener safety net** | Use CometChat's login listener as a backup to ensure badge is cleared on logout. | | **Title enhancement** | Optionally display the unread count in the notification title (e.g., "John (5 unread)") for devices that don't support app icon badges. | -## 8. Testing Checklist +## 6. Testing Checklist 1. Install on a physical Android device, grant `POST_NOTIFICATIONS` permission, log in, and verify FCM token registration succeeds. 2. Send a message from another user: @@ -929,7 +907,7 @@ useEffect(() => { 3. **VoIP call:** Send a `callAction=initiated` push; expect the native dialer to appear. Answer and verify the call connects; send `callAction=ended` to dismiss it. 4. Rotate tokens (reinstall or revoke) and confirm `onTokenRefresh` re-registers the new token. -## 9. Troubleshooting +## 7. Troubleshooting | Symptom | Quick Checks | | --- | --- | diff --git a/notifications/react-native-push-notifications-ios.mdx b/notifications/react-native-push-notifications-ios.mdx index f5489fc6e..7a0f56551 100644 --- a/notifications/react-native-push-notifications-ios.mdx +++ b/notifications/react-native-push-notifications-ios.mdx @@ -1,5 +1,5 @@ --- -title: "React Native Push Notifications (iOS)" +title: "React Native (iOS)" description: "Bring the SampleAppWithPushNotifications experience—APNs + VoIP—into any React Native project using CometChat UI Kit." --- @@ -25,8 +25,7 @@ description: "Bring the SampleAppWithPushNotifications experience—APNs + VoIP ## What this guide covers -- CometChat Dashboard setup (enable push, add APNs provider). -- Platform credentials (Apple entitlements). +- Prerequisite: the shared dashboard + Apple/APNs setup in the [Getting Started](/notifications/push-getting-started) guide. - Copying the sample notification stack and aligning IDs/provider IDs. - Native glue for iOS (capabilities + PushKit/CallKit for VoIP). - Token registration, navigation from pushes, testing, and troubleshooting. @@ -43,86 +42,11 @@ description: "Bring the SampleAppWithPushNotifications experience—APNs + VoIP - **CometChat provider holds your credentials:** The APNs provider you add stores your `.p8` key/cert. - **Registration flow:** Request permission → APNs returns token → after `CometChat.login`, register with `CometChatNotifications.registerPushToken(token, platform, providerId)` using `APNS_REACT_NATIVE_DEVICE` → CometChat sends pushes to APNs on your behalf → the app handles taps/foreground events via `PushNotificationIOS`. -## 1. Enable push and add providers (CometChat Dashboard) + +**Complete the [Getting Started](/notifications/push-getting-started) guide first** — enable Push Notifications, add your **APNs** (and **APNs VoIP**) providers, and finish the Apple/Xcode setup (`.p8` key + capabilities). This guide covers only the React Native app wiring. + -1. Go to **Notifications → Settings** and enable **Push Notifications**. - - - Enable Push Notifications - - -2. Add an **APNs** provider for iOS and copy the Provider ID. - - - Upload APNs credentials - - -## 2. Prepare platform credentials - -### Apple Developer portal - -For iOS we use Apple Push Notification service (APNs) for both standard and VoIP pushes. Follow these steps to create the credentials you’ll upload to CometChat. - - - - 1. Open **Keychain Access** → Certificate Assistant → *Request a Certificate From a Certificate Authority*.
- - Apple Developer portal screenshot - - 2. In **Certificate Information**, enter your Apple Developer email and a common name; choose **Saved to disk**, then **Continue**. - 3. Save the CSR file locally—this contains your public/private key pair. -
- - - 1. Sign in to the [Apple Developer Member Center](https://developer.apple.com/membercenter) → **Certificates, Identifiers & Profiles**.
- - Apple Developer portal screenshot - - 2. Click **+** to add a certificate.
- - Apple Developer portal screenshot - - 3. Under **Services**, pick **Apple Push Notification service SSL (Sandbox & Production)**.
- - Apple Developer portal screenshot - - 4. Select your App ID, upload the CSR, continue, and download the generated `.cer` file.
- - Apple Developer portal screenshot - - - & - - - Apple Developer portal screenshot - - - & - - - Apple Developer portal screenshot - -
- - - 1. In **Certificates, IDs & Profiles**, open **Keys** → click **+**. - 2. Enter a key name, check **Apple Push Notification service (APNs)**, then **Continue** → **Register**. - 3. Download the `.p8` file and note the **Key ID**, **Team ID**, and your **Bundle ID**—you’ll enter these in CometChat. - 4. *(Optional)* If you still use `.p12`, export it from the downloaded key without an export password; keep it handy for upload. - - - **`.p12` certificates are deprecated.** Apple recommends using `.p8` Auth Keys for push notifications. `.p8` keys are simpler to manage (one key works for all your apps), never expire, and are the only format actively supported going forward. Migrate to `.p8` if you haven't already. - - -
- -Enable **Push Notifications** plus **Background Modes → Remote notifications** on the bundle ID. - - - Enable Push Notifications and Background Modes for APNs - - -## 3. Local configuration +## 1. Local configuration - Update `src/utils/AppConstants.tsx` with `appId`, `authKey`, `region`, and `apnProviderId`. - Keep `app.json` name consistent with your bundle ID / applicationId. @@ -134,7 +58,7 @@ const REGION = ""; const DEMO_UID = "cometchat-uid-1"; ``` -### 3.1 Dependencies snapshot (from Sample App) +### 1.1 Dependencies snapshot (from Sample App) Install these dependencies in your React Native app: @@ -153,9 +77,9 @@ npm install \ Match these or newer compatible versions in your app. -## 4. iOS App setup +## 2. iOS App setup -### 4.1 Project Setup +### 2.1 Project Setup Enable **Push Notifications** and **Background Modes** (Remote notifications) in Xcode. @@ -163,7 +87,7 @@ Enable **Push Notifications** and **Background Modes** (Remote notifications) in Enable Push Notifications -### 4.2 Install dependencies + pods +### 2.2 Install dependencies + pods After running the npm install above, install pods from the `ios` directory: ```bash lines @@ -171,7 +95,7 @@ cd ios pod install ``` -### 4.3 AppDelegate.swift modifications: +### 2.3 AppDelegate.swift modifications: Add imports at the top: ```swift lines @@ -250,7 +174,7 @@ pod install open YourProjectName.xcworkspace ``` -### 4.4 App.tsx modifications: +### 2.4 App.tsx modifications: Import CometChatNotifications and PushNotificationIOS: @@ -311,16 +235,16 @@ Prior to logout, unregister the APNs token: await CometChatNotifications.unregisterPushToken(); ``` -## 5. VoIP call notifications (iOS) +## 3. VoIP call notifications (iOS) These steps are iOS-only—copy/paste and fill your IDs. -### 5.1 Enable capabilities in Xcode +### 3.1 Enable capabilities in Xcode - Target ➜ Signing & Capabilities: add **Push Notifications**. - Add **Background Modes** → enable **Voice over IP** and **Remote notifications**. - Run on a real device (PushKit/CallKit don’t work on the simulator). -### 5.2 AppDelegate.swift (PushKit + CallKit bridge) +### 3.2 AppDelegate.swift (PushKit + CallKit bridge) Update your `AppDelegate` to register for VoIP pushes ASAP and forward events to JS/CallKeep: ```swift lines @@ -355,7 +279,7 @@ class AppDelegate: UIResponder, UIApplicationDelegate, UNUserNotificationCenterD } ``` -### 5.3 Drop in `VoipNotificationHandler.ts` +### 3.3 Drop in `VoipNotificationHandler.ts` Handles CallKeep UI, defers acceptance until login, and listens for PushKit events. ```ts lines @@ -474,7 +398,7 @@ class VoipNotificationHandler { export const voipHandler = new VoipNotificationHandler(); ``` -### 5.4 Add `PendingCallManager.ts` +### 3.4 Add `PendingCallManager.ts` Stores an answered call during cold start so you can accept it after login/navigation is ready. ```ts lines @@ -499,7 +423,7 @@ export function isPendingStale(p: PendingAnsweredCallPayload, maxAgeMs = 2 * 60 } ``` -### 5.5 Wire `App.tsx` for APNs + VoIP token registration and handler init +### 3.5 Wire `App.tsx` for APNs + VoIP token registration and handler init ```tsx lines import PushNotificationIOS from "@react-native-community/push-notification-ios"; @@ -553,7 +477,7 @@ useEffect(() => { }, [loggedIn]); ``` -### 5.6 VoIP push payload (APNs / PushKit) +### 3.6 VoIP push payload (APNs / PushKit) Send a VoIP push with `push_type=voip` via APNs using a payload shaped like: ```json @@ -567,18 +491,18 @@ Send a VoIP push with `push_type=voip` via APNs using a payload shaped like: } ``` -## 6. Handling notification taps and navigation +## 4. Handling notification taps and navigation To handle notification taps and navigate to the appropriate chat screen, you need to set up handlers for both foreground and background notifications. {/* :TODO: Add code snippets and explanation for setting up Notifee handlers and navigation logic. */} -## 7. Badge Count Implementation +## 5. Badge Count Implementation CometChat's Enhanced Push Notification payload includes an `unreadMessageCount` field that represents the total number of unread messages across all conversations for the logged-in user. You can use this value to update the app icon badge, providing users with a visual indicator of unread messages. -### 7.1 Enable Unread Badge Count on the CometChat Dashboard +### 5.1 Enable Unread Badge Count on the CometChat Dashboard @@ -591,7 +515,7 @@ CometChat's Enhanced Push Notification payload includes an `unreadMessageCount` Once enabled, CometChat automatically includes the `unreadMessageCount` field in every push payload sent to your app. -### 7.2 Expected Payload Format +### 5.2 Expected Payload Format CometChat sends APNs payloads with the following structure: @@ -614,7 +538,7 @@ CometChat sends APNs payloads with the following structure: The `aps.badge` field is set server-side by CometChat. iOS automatically updates the app icon badge when the push notification is delivered. -### 7.3 Handle Badge Count from Notifications +### 5.3 Handle Badge Count from Notifications Update your iOS notification handler to set the badge count programmatically: @@ -645,7 +569,7 @@ export async function onRemoteNotificationIOS(notification: any) { } ``` -### 7.4 Register Notification Listener +### 5.4 Register Notification Listener In your `App.tsx`, set up the notification listener: @@ -671,7 +595,7 @@ useEffect(() => { }, []); ``` -### 7.5 Clear Badge When App Becomes Active +### 5.5 Clear Badge When App Becomes Active Clear the badge count when the app launches or returns to the foreground: @@ -692,7 +616,7 @@ useEffect(() => { }, []); ``` -### 7.6 Clear Badge on Logout +### 5.6 Clear Badge on Logout When a user logs out, clear the badge so it doesn't show a stale count on the login screen or for the next user: @@ -713,7 +637,7 @@ const handleLogout = async () => { }; ``` -### 7.7 Clear Badge on Fresh Install / No Logged-In User +### 5.7 Clear Badge on Fresh Install / No Logged-In User On iOS, the badge count may persist after app uninstall and reinstall in certain scenarios. Clear the badge during app initialization when no user is logged in: @@ -737,7 +661,7 @@ const initializeApp = async () => { }; ``` -### 7.8 Clear Badge in Login Listener (Safety Net) +### 5.8 Clear Badge in Login Listener (Safety Net) Register a login listener to clear the badge on logout as a backup mechanism: @@ -765,7 +689,7 @@ useEffect(() => { }, []); ``` -### 7.9 Key Implementation Notes +### 5.9 Key Implementation Notes | Consideration | Details | | --- | --- | @@ -777,7 +701,7 @@ useEffect(() => { | **Login listener safety net** | Use CometChat's login listener as a backup to ensure badge is cleared on logout. | | **Title enhancement** | Optionally display the unread count in the notification title (e.g., "John (5 unread)") for additional visibility. | -### 7.10 Cross-Platform App State Handler +### 5.10 Cross-Platform App State Handler If you're building a cross-platform app, use this combined handler for both iOS and Android: @@ -807,7 +731,7 @@ useEffect(() => { }, []); ``` -## 8. Testing Checklist +## 6. Testing Checklist 1. Install on a physical iOS device, log in, and verify APNs token registration succeeds. 2. Send a message from another user: @@ -816,7 +740,7 @@ useEffect(() => { 3. **VoIP:** Send a PushKit VoIP push (payload above); expect CallKit incoming UI; answer and confirm CometChat call connects; end clears the dialer. 4. Rotate tokens (reinstall or revoke) and confirm `onTokenRefresh` re-registers the new token. -## 9. Troubleshooting +## 7. Troubleshooting | Symptom | Quick Checks | | --- | --- | diff --git a/notifications/web-push-notifications.mdx b/notifications/web-push-notifications.mdx index 17c583e43..124482e05 100644 --- a/notifications/web-push-notifications.mdx +++ b/notifications/web-push-notifications.mdx @@ -1,5 +1,5 @@ --- -title: "Web Push Notifications" +title: "Web" description: "Set up FCM web push for CometChat React UI Kit—service worker, VAPID keys, token registration, and foreground/background handlers." --- @@ -13,7 +13,7 @@ description: "Set up FCM web push for CometChat React UI Kit—service worker, V ## What this guide covers -- CometChat dashboard setup (FCM provider + Provider ID) and Firebase web config + VAPID key. +- Prerequisite: the shared dashboard + Firebase web config/VAPID setup in the [Getting Started](/notifications/push-getting-started) guide. - Service worker + Firebase Messaging wiring for foreground/background pushes. - Token registration/unregistration with `CometChatNotifications` and navigation on notification click. - Testing and troubleshooting tips for web push. @@ -32,26 +32,11 @@ description: "Set up FCM web push for CometChat React UI Kit—service worker, V - **Handlers**: Foreground messages come via `messaging.onMessage`; background uses `firebase-messaging-sw.js` with `onBackgroundMessage`. - **Navigation**: Service worker sends a postMessage or focuses the client; the app routes to the right view. -## 1. Dashboard: enable push + add FCM provider + +**Complete the [Getting Started](/notifications/push-getting-started) guide first** — enable Push Notifications, add your FCM provider, and set up your Firebase **web app config** and **VAPID key**. This guide covers only the web app wiring. + -1. Go to **Notifications → Settings** and enable **Push Notifications**. -2. Add/configure an **FCM** provider and copy the **Provider ID** (used in code). - - - Firebase - Push Notifications - - -## 2. Firebase setup (web app + VAPID) - -1. In Firebase Console, create/select a project. -2. Add a **Web** app (``), copy the **Firebase config** object. -3. In **Project settings → Cloud Messaging**, generate/copy the **VAPID key** under Web Push certificates. - - - Firebase - Push Notifications - - -## 3. Install dependencies +## 1. Install dependencies Install firebase SDK: @@ -59,7 +44,7 @@ Install firebase SDK: npm install firebase@^10.3.1 ``` -## 4. Constants +## 2. Constants File: `src/AppConstants.js` (or equivalent) @@ -86,7 +71,7 @@ export const FIREBASE_CONFIG = { export const FIREBASE_VAPID_KEY = ""; ``` -## 4. Configure Firebase (frontend) +## 3. Configure Firebase (frontend) File: `src/firebase.js` (or equivalent) @@ -226,7 +211,7 @@ export async function registerPushTokenAfterLogin() { Use your **VAPID key** in `getToken` calls. -## 5. Service worker (background pushes) +## 4. Service worker (background pushes) File: `public/firebase-messaging-sw.js` @@ -347,7 +332,7 @@ try { Ensure your app registers the service worker (e.g., in `index.tsx`) and listens for `message` events to navigate. -## 6. Request permission + register token after login +## 5. Request permission + register token after login In your app initialization (e.g., `App.tsx`): @@ -444,13 +429,13 @@ async function boot() { boot(); ``` -## 7. Foreground + background handling +## 6. Foreground + background handling - **Foreground**: `messaging.onMessage` → show a `Notification` or in-app toast; deep link using payload data. - **Background/killed**: service worker `onBackgroundMessage` shows the notification; `notificationclick` focuses the tab and sends a message for navigation. - Suppress duplicates if the conversation is already active. -## 8. Testing checklist +## 7. Testing checklist 1. Service worker registered (DevTools → Application → Service Workers shows “activated”). 2. Permission prompt appears and is granted (`Notification.permission === "granted"`). @@ -459,7 +444,7 @@ boot(); 5. Background/tab inactive message shows a notification; click focuses tab and routes correctly. 6. Logout → `unregisterPushToken` runs without errors. -## 9. Troubleshooting +## 8. Troubleshooting | Symptom | Quick checks | | --- | --- | diff --git a/ui-kit/flutter/call-features.mdx b/ui-kit/flutter/call-features.mdx index 503c018ad..1e871bdfe 100644 --- a/ui-kit/flutter/call-features.mdx +++ b/ui-kit/flutter/call-features.mdx @@ -15,7 +15,7 @@ Add the dependency to your `pubspec.yaml`: ```yaml pubspec.yaml dependencies: - cometchat_chat_uikit: ^6.0.5 + cometchat_chat_uikit: ^6.1.0 ``` *** diff --git a/ui-kit/flutter/getting-started.mdx b/ui-kit/flutter/getting-started.mdx index 42fd68740..a2e729524 100644 --- a/ui-kit/flutter/getting-started.mdx +++ b/ui-kit/flutter/getting-started.mdx @@ -43,7 +43,7 @@ To get started, create a new flutter application project. ```yaml pubspec.yaml dependencies: - cometchat_chat_uikit: ^6.0.5 + cometchat_chat_uikit: ^6.1.0 ``` Final `pubspec.yaml` @@ -63,7 +63,7 @@ dependencies: flutter: sdk: flutter - cometchat_chat_uikit: ^6.0.5 + cometchat_chat_uikit: ^6.1.0 cupertino_icons: ^1.0.8 diff --git a/ui-kit/flutter/upgrading-from-v5.mdx b/ui-kit/flutter/upgrading-from-v5.mdx index 13d5b79a4..a40de3b26 100644 --- a/ui-kit/flutter/upgrading-from-v5.mdx +++ b/ui-kit/flutter/upgrading-from-v5.mdx @@ -65,7 +65,7 @@ MessageTemplateUtils.getAllMessageTemplates(); ```yaml pubspec.yaml dependencies: - cometchat_chat_uikit: ^6.0.5 + cometchat_chat_uikit: ^6.1.0 ``` ### 5. State Management Migration (Advanced)