From 926d08233a00e6f7604ac4bbd96357098c125d9f Mon Sep 17 00:00:00 2001 From: Suraj Chauhan Date: Fri, 28 Aug 2026 12:35:34 +0530 Subject: [PATCH 01/20] docs(rn): unify React Native push notifications into a single guide Replace the split android/ios RN push guides with one unified notifications/react-native-push-notifications.mdx documenting the JS-first @cometchat/push-notifications-react-native package (FCM + APNs/VoIP, CallKit/lock-screen call UI, killed-state handling). Mirrors the Flutter restructure (cometchat/docs#440): - delete react-native-push-notifications-{android,ios}.mdx - docs.json: collapse the two nav entries into one; redirect the old split paths (and /extensions/... ) to the unified page - repoint cross-links in notifications.mdx, push-overview.mdx, calls/v4/react-native/overview.mdx, and the SDK html-stripping guide Co-Authored-By: Claude Opus 4.8 --- calls/v4/react-native/overview.mdx | 2 +- docs.json | 13 +- notifications.mdx | 3 +- notifications/push-overview.mdx | 6 +- ...eact-native-push-notifications-android.mdx | 959 ------------------ .../react-native-push-notifications-ios.mdx | 846 --------------- .../react-native-push-notifications.mdx | 375 +++++++ .../push-notification-html-stripping.mdx | 7 +- 8 files changed, 388 insertions(+), 1823 deletions(-) delete mode 100644 notifications/react-native-push-notifications-android.mdx delete mode 100644 notifications/react-native-push-notifications-ios.mdx create mode 100644 notifications/react-native-push-notifications.mdx diff --git a/calls/v4/react-native/overview.mdx b/calls/v4/react-native/overview.mdx index 5d8af9ad8..b4b75182c 100644 --- a/calls/v4/react-native/overview.mdx +++ b/calls/v4/react-native/overview.mdx @@ -105,7 +105,7 @@ Use this when you want: - **Calls not connecting:** Verify the Calls SDK is initialized after the Chat SDK and that both use the same App ID and Region - **No audio/video:** Check that camera and microphone permissions are granted on both Android and iOS - - **Push notifications not arriving:** Ensure push notification setup is complete — see the [Push Notifications](/notifications/react-native-push-notifications-android) guide + - **Push notifications not arriving:** Ensure push notification setup is complete — see the [Push Notifications](/notifications/react-native-push-notifications) guide - **iOS build fails:** Run `pod install` in the `ios` directory after adding the Calls SDK dependency - **Android minSdkVersion error:** Set `minSdkVersion` to 24 or higher in your `build.gradle` diff --git a/docs.json b/docs.json index 86152f233..c38c351a4 100644 --- a/docs.json +++ b/docs.json @@ -6515,8 +6515,7 @@ "notifications/ios-fcm-push-notifications", "notifications/flutter-push-notifications-android", "notifications/flutter-push-notifications-ios", - "notifications/react-native-push-notifications-android", - "notifications/react-native-push-notifications-ios", + "notifications/react-native-push-notifications", "notifications/web-push-notifications" ] }, @@ -7135,7 +7134,7 @@ }, { "source": "/extensions/react-native-push-notifications", - "destination": "/notifications/react-native-push-notifications-android" + "destination": "/notifications/react-native-push-notifications" }, { "source": "/extensions/capacitor-cordova-ionic-push-notifications", @@ -7430,8 +7429,12 @@ "destination": "https://assets.cometchat.io/legacy-docs/notifications/push-notification-extension-legacy.html" }, { - "source": "/notifications/react-native-push-notifications", - "destination": "/notifications/react-native-push-notifications-android" + "source": "/notifications/react-native-push-notifications-android", + "destination": "/notifications/react-native-push-notifications" + }, + { + "source": "/notifications/react-native-push-notifications-ios", + "destination": "/notifications/react-native-push-notifications" }, { "source": "/rest-api/messages/list-reactions-with-a-specific-emojiunicodes", diff --git a/notifications.mdx b/notifications.mdx index 0eb31ff02..e3ad91797 100644 --- a/notifications.mdx +++ b/notifications.mdx @@ -66,8 +66,7 @@ canonical: "https://cometchat.com/docs" } href="/notifications/flutter-push-notifications-android" horizontal /> } href="/notifications/flutter-push-notifications-ios" horizontal /> - } href="/notifications/react-native-push-notifications-android" horizontal /> - } href="/notifications/react-native-push-notifications-ios" horizontal /> + } href="/notifications/react-native-push-notifications" horizontal /> } href="/notifications/web-push-notifications" horizontal /> diff --git a/notifications/push-overview.mdx b/notifications/push-overview.mdx index 6d83a545f..f8fb45c27 100644 --- a/notifications/push-overview.mdx +++ b/notifications/push-overview.mdx @@ -63,11 +63,7 @@ UI Kit implementation UI Kit implementation -} href="/notifications/react-native-push-notifications-android"> -UI Kit implementation - - -} href="/notifications/react-native-push-notifications-ios"> +} href="/notifications/react-native-push-notifications"> UI Kit implementation diff --git a/notifications/react-native-push-notifications-android.mdx b/notifications/react-native-push-notifications-android.mdx deleted file mode 100644 index a51e7a886..000000000 --- a/notifications/react-native-push-notifications-android.mdx +++ /dev/null @@ -1,959 +0,0 @@ ---- -title: "React Native Push Notification (Android)" -description: "Bring the SampleAppWithPushNotifications experience—FCM + VoIP calls—into any React Native project using CometChat UI Kit." ---- - - - -| Field | Value | -| --- | --- | -| Platform | Android (FCM) | -| Key Classes | `CometChatNotifications`, `VoipNotificationHandler`, `PendingCallManager` | -| Key Methods | `registerPushToken()`, `unregisterPushToken()`, `messaging().getToken()` | -| Push Platform | `CometChatNotifications.PushPlatforms.FCM_REACT_NATIVE_ANDROID` | -| Prerequisites | CometChat SDK initialized, user logged in, FCM configured, `google-services.json` in `android/app` | - - - - - Reference implementation of React Native UI Kit, FCM and Push Notification Setup. - - -## What this guide covers - -- CometChat Dashboard setup (enable push, add FCM providers). -- Platform credentials (Firebase). -- 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. -- Token registration, navigation from pushes, testing, and troubleshooting. - -## What you need first - -- CometChat app credentials (App ID, Region, Auth Key) and Push Notifications enabled with an **FCM provider (React Native Android)**. -- Firebase project with an Android app (`google-services.json` in `android/app`) and Cloud Messaging enabled. -- React Native 0.81+, Node 18+, physical Android devices for reliable push/call testing. - -## How FCM + CometChat work together - -- **FCM (Android) is the transport:** Firebase issues the Android FCM token and delivers payloads to devices. -- **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) - -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 - -- Update `src/utils/AppConstants.tsx` with `appId`, `authKey`, `region`, and `fcmProviderId`. -- Keep `app.json` name consistent with your bundle ID / applicationId. - -```ts lines -const APP_ID = ""; -const AUTH_KEY = ""; -const REGION = ""; -const DEMO_UID = "cometchat-uid-1"; -``` - -### 3.1 Dependencies snapshot (from Sample App) - -Install these dependencies in your React Native app: - -```npm lines -npm install \ - @react-native-firebase/app@23.4.0 \ - @react-native-firebase/messaging@23.4.0 \ - @notifee/react-native@9.1.8 \ - @cometchat/chat-sdk-react-native@4.0.18 \ - @cometchat/calls-sdk-react-native@4.4.0 \ - @cometchat/chat-uikit-react-native@5.2.6 \ - @react-native-async-storage/async-storage@2.2.0 \ - react-native-callkeep@github:cometchat/react-native-callkeep \ - react-native-voip-push-notification@3.3.3 -``` - -Match these or newer compatible versions in your app. - -## 4. Android App Setup - -### 4.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. - -First, add the google-services plugin as a dependency inside of your `/android/build.gradle` file: - -```android lines -buildscript { - dependencies { - // ... other dependencies - classpath("com.google.gms:google-services:4.4.4") - } -} -``` - -Lastly, execute the plugin by adding the following to your `/android/app/build.gradle` file: - -```android lines -apply plugin: 'com.android.application' -apply plugin: 'com.google.gms.google-services' -``` - -### 4.2 Configure required permissions in `AndroidManifest.xml` as shown. - -```xml lines - - - - - - - - - - - - - - - - -``` - -and ask for runtime permissions where needed (e.g. `POST_NOTIFICATIONS` on Android 13+). - -```tsx lines -import { PermissionsAndroid, Platform } from "react-native"; - - const requestAndroidPermissions = async () => { - if (Platform.OS !== 'android') return; - - try { - // Ask for push‑notification permission - const authStatus = await messaging().requestPermission(); - const enabled = - authStatus === messaging.AuthorizationStatus.AUTHORIZED || - authStatus === messaging.AuthorizationStatus.PROVISIONAL; - - if (!enabled) { - console.warn('Notification permission denied (FCM).'); - } - } catch (error) { - console.warn('FCM permission request error:', error); - } - - try { - await PermissionsAndroid.requestMultiple([ - PermissionsAndroid.PERMISSIONS.WRITE_EXTERNAL_STORAGE, - PermissionsAndroid.PERMISSIONS.READ_EXTERNAL_STORAGE, - PermissionsAndroid.PERMISSIONS.CAMERA, - PermissionsAndroid.PERMISSIONS.RECORD_AUDIO, - PermissionsAndroid.PERMISSIONS.POST_NOTIFICATIONS, - ]); - } catch (err) { - console.warn('Android permissions error:', err); - } -} -``` - -### 4.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: - -```ts lines -requestAndroidPermissions(); - -const FCM_TOKEN = await messaging().getToken(); -console.log("FCM Token:", FCM_TOKEN); - -// For React Native Android -CometChatNotifications.registerPushToken( - FCM_TOKEN, - CometChatNotifications.PushPlatforms.FCM_REACT_NATIVE_ANDROID, - "YOUR_FCM_PROVIDER_ID" // from CometChat Dashboard - ) - .then(() => { - console.log("Token registration successful"); - }) - .catch((err) => { - console.log("Token registration failed:", err); - }); -``` - -### 4.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 - -These steps are Android-only—copy/paste and fill your IDs. - -### 5.1 Add CallKeep services to `android/app/src/main/AndroidManifest.xml` -Inside the `` tag add: - -```xml lines - - - - - - - -``` - -### 5.2 Background handler for call pushes (`index.js`) -Data-only FCM calls show the native dialer even when the app is killed. - -```js lines -import messaging from "@react-native-firebase/messaging"; -import { Platform } from "react-native"; -import { CometChat } from "@cometchat/chat-sdk-react-native"; -import { voipHandler } from "./VoipNotificationHandler"; -import { displayLocalNotification } from "./LocalNotificationHandler"; - -if (Platform.OS === "android") { - messaging().setBackgroundMessageHandler(async remoteMessage => { - const data = remoteMessage.data || {}; - if (data.type === "call") { - await voipHandler.initialize(); - switch (data.callAction) { - case "initiated": - voipHandler.msg = data; - await voipHandler.displayCallAndroid(); - break; - case "ended": - case "unanswered": - case "busy": - case "rejected": - case "cancelled": - CometChat.clearActiveCall(); - if (voipHandler?.callerId) { - voipHandler.removeCallDialerWithUUID(voipHandler.callerId); - } - await voipHandler.endCall({ callUUID: voipHandler.callerId }); - break; - case "ongoing": - voipHandler.displayNotification({ - title: data?.receiverName || "", - body: "ongoing call", - }); - break; - default: - break; - } - return; - } - await displayLocalNotification(remoteMessage); - }); -} -``` - -### 5.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 -import { Platform } from "react-native"; -import notifee, { AndroidImportance } from "@notifee/react-native"; -import RNCallKeep, { IOptions } from "react-native-callkeep"; -import { CometChat } from "@cometchat/chat-sdk-react-native"; -import { setPendingAnsweredCall } from "./PendingCallManager"; - -const options: IOptions = { - android: { - alertTitle: "VoIP permissions", - alertDescription: "Allow phone account access to show incoming calls", - cancelButton: "Cancel", - okButton: "OK", - imageName: "ic_notification", - additionalPermissions: [], - foregroundService: { - channelId: "com.cometchat.sampleapp.reactnative.android", - channelName: "Sampleapp Channel", - notificationTitle: "Sampleapp is running in the background", - }, - }, - ios: { appName: "Sampleapp" }, -}; - -function uuid() { - return "xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx".replace(/[xy]/g, (c) => { - const r = Math.floor(Math.random() * 16); - const v = c === "x" ? r : (r & 0x3) | 0x8; - return v.toString(16); - }); -} - -class VoipNotificationHandler { - channelId = ""; - isRinging = false; - isAnswered = false; - pendingAcceptance = false; - callerId = ""; - msg: any = {}; - initialized = false; - private setupPromise: Promise | null = null; - private listenersAttached = false; - - async initialize() { - if (this.initialized && this.setupPromise) { - await this.setupPromise; - return; - } - if (!this.setupPromise) { - this.setupPromise = (async () => { - if (Platform.OS === "android") { - await this.createNotificationChannel(); - } - await this.getPermissions(); - this.setupEventListeners(); - this.initialized = true; - })().catch((err) => { - this.setupPromise = null; - throw err; - }); - } - await this.setupPromise; - } - - async getPermissions() { - await RNCallKeep.setup(options); - RNCallKeep.setAvailable(true); - RNCallKeep.setReachable(); - try { - await RNCallKeep.checkPhoneAccountEnabled(); - } catch {} - } - - async createNotificationChannel() { - this.channelId = await notifee.createChannel({ - id: "message", - name: "Messages", - lights: true, - vibration: true, - importance: AndroidImportance.HIGH, - }); - } - - async displayNotification({ - title, - body, - data, - }: { - title: string; - body: string; - data?: any; - }) { - if (Platform.OS === "android" && !this.channelId) - await this.createNotificationChannel(); - await notifee.displayNotification({ - title, - body, - data, - android: this.channelId - ? { channelId: this.channelId, smallIcon: "ic_launcher" } - : undefined, - }); - } - - async displayCallAndroid() { - if (this.isAnswered || this.pendingAcceptance) return; - await this.initialize(); - this.isRinging = true; - this.callerId = uuid(); - const callerName = this.msg?.senderName || "Incoming Call"; - await RNCallKeep.displayIncomingCall( - this.callerId, - callerName, - callerName, - "generic", - ); - } - - onAnswerCall = async ({ callUUID }: { callUUID: string }) => { - if (this.isAnswered) return; - this.isRinging = false; - this.isAnswered = true; - const sessionID = this.msg?.sessionId; - if (!sessionID) return; - - setTimeout(async () => { - const loggedInUser = await CometChat.getLoggedinUser().catch(() => null); - if (!loggedInUser) { - this.pendingAcceptance = true; - await setPendingAnsweredCall({ - sessionId: sessionID, - raw: this.msg, - storedAt: Date.now(), - }); - try { - RNCallKeep.backToForeground(); - } catch (err) { - // Activity may not exist yet if app was killed - the pending call will be handled when app opens - console.log( - "[VoIP] backToForeground failed, pending call saved:", - err, - ); - } - return; - } - try { - await CometChat.acceptCall(sessionID); - } catch (error: any) { - if (error?.code !== "ERR_CALL_USER_ALREADY_JOINED") throw error; - } - RNCallKeep.endAllCalls(); - this.pendingAcceptance = false; - }, 600); - }; - - endCall = async ({ callUUID }: { callUUID: string }) => { - if (this.msg?.type === "call") { - const sessionID = this.msg.sessionId; - if (this.isAnswered && sessionID) { - this.isAnswered = false; - CometChat.endCall(sessionID); - } else if (sessionID) { - const loggedInUser = await CometChat.getLoggedinUser().catch( - () => null, - ); - if (loggedInUser) { - setTimeout(() => { - CometChat.rejectCall(sessionID, CometChat.CALL_STATUS.REJECTED); - }, 300); - } - } - } - const id = callUUID || this.callerId; - if (id) RNCallKeep.endCall(id); - RNCallKeep.endAllCalls(); - this.isRinging = false; - this.isAnswered = false; - this.pendingAcceptance = false; - this.callerId = ""; - this.msg = {}; - }; - - removeCallDialerWithUUID = (callerId: string) => { - const id = callerId || this.callerId; - if (id) RNCallKeep.reportEndCallWithUUID(id, 6); - }; - - setupEventListeners() { - if (this.listenersAttached) return; - RNCallKeep.addEventListener("answerCall", this.onAnswerCall); - RNCallKeep.addEventListener("endCall", this.endCall); - RNCallKeep.addEventListener("didDisplayIncomingCall", ({ callUUID }) => { - if (callUUID) this.callerId = callUUID; - this.isRinging = true; - }); - this.listenersAttached = true; - } -} - -export const voipHandler = new VoipNotificationHandler(); -``` - -### 5.4 Add `PendingCallManager.ts` -Stores an answered call during cold-start so you can accept it once login/navigation is ready. - -```ts lines -import AsyncStorage from "@react-native-async-storage/async-storage"; - -export interface PendingAnsweredCallPayload { - sessionId: string; - raw: any; - storedAt: number; -} - -let inMemoryPending: PendingAnsweredCallPayload | null = null; -const STORAGE_KEY = "pendingAnsweredCall"; - -export async function setPendingAnsweredCall(payload: PendingAnsweredCallPayload) { - inMemoryPending = payload; - try { await AsyncStorage.setItem(STORAGE_KEY, JSON.stringify(payload)); } catch {} -} - -export async function consumePendingAnsweredCall(): Promise { - if (inMemoryPending) { - const tmp = inMemoryPending; - inMemoryPending = null; - try { await AsyncStorage.removeItem(STORAGE_KEY); } catch {} - return tmp; - } - try { - const raw = await AsyncStorage.getItem(STORAGE_KEY); - if (raw) { - await AsyncStorage.removeItem(STORAGE_KEY); - const parsed: PendingAnsweredCallPayload = JSON.parse(raw); - inMemoryPending = null; - return parsed; - } - } catch {} - return null; -} - -export function isPendingStale(p: PendingAnsweredCallPayload, maxAgeMs = 2 * 60 * 1000) { - return Date.now() - p.storedAt > maxAgeMs; -} -``` - -### 5.5 Wire `App.tsx` to init VoIP + consume pending accepts -Add this after CometChat init/login: - -```ts lines -import { Platform } from "react-native"; -import messaging from "@react-native-firebase/messaging"; -import { CometChat, CometChatNotifications } from "@cometchat/chat-sdk-react-native"; -import { voipHandler } from "./VoipNotificationHandler"; -import { consumePendingAnsweredCall, isPendingStale } from "./PendingCallManager"; - -if (Platform.OS === "android") { - const fcmToken = await messaging().getToken(); - await CometChatNotifications.registerPushToken( - fcmToken, - CometChatNotifications.PushPlatforms.FCM_REACT_NATIVE_ANDROID, - "YOUR_FCM_PROVIDER_ID" - ); -} - -useEffect(() => { - if (Platform.OS === "android" && loggedIn) { - const t = setTimeout(() => voipHandler.initialize(), 3000); - return () => clearTimeout(t); - } -}, [loggedIn]); - -// Handle pending calls in a useEffect -useEffect(() => { - const handlePendingCall = async () => { - const pending = await consumePendingAnsweredCall(); - if (pending && !isPendingStale(pending)) { - try { - await CometChat.acceptCall(pending.sessionId); - } catch (err) { - console.log(err); - } - } - }; - handlePendingCall(); -}, []); -``` - -### 5.6 Call push payload (FCM data) -Send a data-only FCM message like: - -```json -{ - "to": "", - "priority": "high", - "data": { - "type": "call", - "callAction": "initiated", - "sessionId": "", - "senderName": "Alice", - "receiverName": "Bob" - } -} -``` - -### 5.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: - -```ts lines -import { Platform } from "react-native"; -import notifee, { AndroidImportance } from "@notifee/react-native"; - -const CHANNEL_ID = "default"; - -async function ensureChannel(): Promise { - if (Platform.OS !== "android") return undefined; - return notifee.createChannel({ - id: CHANNEL_ID, - name: "Default", - lights: true, - vibration: true, - importance: AndroidImportance.HIGH, - }); -} - -export async function displayLocalNotification(remoteMessage: any) { - try { - const { notification = {}, data = {} } = remoteMessage || {}; - const title = notification?.title || data?.title || "Notification"; - const body = notification?.body || data?.body || ""; - - if (Platform.OS === "ios") { - await notifee.requestPermission(); - } - - const channelId = await ensureChannel(); - - await notifee.displayNotification({ - title, - body, - data, - android: channelId - ? { - channelId, - pressAction: { id: "default" }, - importance: AndroidImportance.HIGH, - smallIcon: "ic_launcher", - } - : undefined, - }); - } catch (error) { - console.error("[LocalNotificationHandler] Failed to display notification", error); - } -} -``` -- 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 - -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 - -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 - - - - Go to **CometChat Dashboard → Notifications Engine → Settings → Preferences → Push Notification Preferences**. - - - Scroll down and enable the **Unread Badge Count** toggle. - - - -Once enabled, CometChat automatically includes the `unreadMessageCount` field in every push payload sent to your app. - -### 7.2 Expected Payload Format - -CometChat sends push notifications with the following structure: - -```json -{ - "data": { - "unreadMessageCount": "5", - "title": "New Message", - "body": "John: Hello!", - "conversationId": "user_abc123", - "receiverType": "user", - "type": "chat" - } -} -``` - - -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 - -Update your FCM background message handler in `index.js` to extract and set the badge count: - -```javascript -import messaging from "@react-native-firebase/messaging"; -import notifee from "@notifee/react-native"; - -messaging().setBackgroundMessageHandler(async (remoteMessage) => { - const data = remoteMessage.data || {}; - - // Extract and set badge count from push payload - const unreadCount = data?.unreadMessageCount; - if (unreadCount !== undefined && unreadCount !== null) { - const count = parseInt(unreadCount, 10); - if (!isNaN(count) && count >= 0) { - try { - await notifee.setBadgeCount(count); - console.log("Badge count updated (Android):", count); - } catch (error) { - console.error("Error setting badge:", error); - } - } - } - - // Display local notification - await displayLocalNotification(remoteMessage); -}); -``` - -### 7.4 Handle Badge Count in Foreground Messages - -In your `App.tsx`, set up a listener for foreground FCM messages: - -```typescript -import messaging from "@react-native-firebase/messaging"; -import notifee from "@notifee/react-native"; - -useEffect(() => { - if (Platform.OS === "android") { - const unsubscribe = messaging().onMessage(async (remoteMessage) => { - // Extract and set badge count from push payload - const unreadCount = remoteMessage.data?.unreadMessageCount; - if (unreadCount !== undefined && unreadCount !== null) { - const count = parseInt(unreadCount as string, 10); - if (!isNaN(count) && count >= 0) { - try { - await notifee.setBadgeCount(count); - console.log("Badge count updated (Android):", count); - } catch (error) { - console.error("Error setting badge:", error); - } - } - } - - // Display local notification - await displayLocalNotification(remoteMessage); - }); - - return () => unsubscribe(); - } -}, []); -``` - -### 7.5 Display Local Notification with Badge Count - -Update your notification display function to include the badge count: - -```typescript -import notifee, { AndroidImportance } from "@notifee/react-native"; - -export async function displayLocalNotification(remoteMessage: any) { - const { title, body, senderAvatar } = remoteMessage.data || {}; - - // Create notification channel - const channelId = await notifee.createChannel({ - id: "chat-messages", - name: "Chat Messages", - vibration: true, - importance: AndroidImportance.HIGH, - }); - - // Parse badge count from payload - const unreadCount = remoteMessage.data?.unreadMessageCount; - const badgeCount = unreadCount ? parseInt(unreadCount, 10) : undefined; - - // Optionally enhance title with unread count - const displayTitle = - badgeCount && badgeCount > 1 - ? `${title || "New Message"} (${badgeCount} unread)` - : title || "New Message"; - - // Update badge count - if (badgeCount && badgeCount > 0) { - await notifee.setBadgeCount(badgeCount); - } - - // Display notification with fixed ID to prevent badge accumulation - // on devices that sum badge counts from multiple notifications - await notifee.displayNotification({ - id: "chat-notification", - title: displayTitle, - body: body || "You received a new message.", - android: { - channelId, - autoCancel: true, - smallIcon: "ic_notification", - largeIcon: - senderAvatar || - "https://cdn-icons-png.flaticon.com/512/149/149071.png", - importance: AndroidImportance.HIGH, - badgeCount: badgeCount, - pressAction: { - id: "default", - }, - }, - data: { - receiverType: remoteMessage.data?.receiverType, - sender: remoteMessage.data?.sender, - conversationId: remoteMessage.data?.conversationId, - }, - }); -} -``` - -### 7.6 Clear Badge When App Becomes Active - -Clear all notifications and reset the badge when the app returns to the foreground: - -```typescript -import { AppState, AppStateStatus, Platform } from "react-native"; -import notifee from "@notifee/react-native"; - -useEffect(() => { - const handleAppStateChange = async (nextState: AppStateStatus) => { - if (nextState === "active" && Platform.OS === "android") { - // Clear all notifications (also resets badge count) - await notifee.cancelAllNotifications(); - console.log("Notifications cleared (Android)"); - } - }; - - const subscription = AppState.addEventListener("change", handleAppStateChange); - return () => subscription.remove(); -}, []); -``` - -### 7.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: - -```typescript -import notifee from "@notifee/react-native"; -import { CometChat, CometChatNotifications } from "@cometchat/chat-sdk-react-native"; - -const handleLogout = async () => { - // Unregister push token first - await CometChatNotifications.unregisterPushToken(); - - // Clear badge before logout - await notifee.setBadgeCount(0); - await notifee.cancelAllNotifications(); - - // Logout from CometChat - await CometChat.logout(); - console.log("User logged out, badge cleared"); -}; -``` - -### 7.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: - -```typescript -import notifee from "@notifee/react-native"; -import { CometChat } from "@cometchat/chat-sdk-react-native"; - -// During app initialization, after CometChat.init() -const initializeApp = async () => { - // Initialize CometChat first - await CometChatUIKit.init(uiKitSettings); - - // Check if user is logged in - const loggedInUser = await CometChat.getLoggedinUser(); - - if (!loggedInUser) { - // No user logged in - clear any stale badge - await notifee.setBadgeCount(0); - await notifee.cancelAllNotifications(); - console.log("No logged-in user, badge cleared"); - } -}; -``` - -### 7.9 Clear Badge in Login Listener (Safety Net) - -Register a login listener to clear the badge on logout as a backup mechanism: - -```typescript -import notifee from "@notifee/react-native"; -import { CometChat } from "@cometchat/chat-sdk-react-native"; - -useEffect(() => { - const listenerID = "BADGE_LOGOUT_LISTENER"; - - CometChat.addLoginListener( - listenerID, - new CometChat.LoginListener({ - logoutOnSuccess: async () => { - // Safety net: clear badge when logout succeeds - await notifee.setBadgeCount(0); - await notifee.cancelAllNotifications(); - console.log("Logout listener: badge cleared"); - }, - }) - ); - - return () => { - CometChat.removeLoginListener(listenerID); - }; -}, []); -``` - -### 7.10 Key Implementation Notes - -| Consideration | Details | -| --- | --- | -| **Backend-driven badge count** | The `unreadMessageCount` value comes directly from CometChat's backend via the push payload, ensuring consistency across all devices. | -| **Fixed notification ID** | Using a fixed notification ID (`'chat-notification'`) prevents certain devices from accumulating badge counts across multiple notifications. The badge always reflects the exact `unreadMessageCount` from the backend. | -| **Clear on app active** | Always clear the badge when the app becomes active. New notifications will update the badge with the fresh `unreadMessageCount` from the backend. | -| **Clear on logout** | Always clear the badge when a user logs out to prevent stale counts for the next user. | -| **Clear on fresh install** | Clear the badge during app initialization when no user is logged in to handle reinstall scenarios. | -| **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 - -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: - - **Foreground:** Notifee banner appears unless that chat is already open. - - **Background/terminated:** Tap opens the correct conversation; Notifee background handler runs. -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 - -| Symptom | Quick Checks | -| --- | --- | -| No pushes | Confirm `google-services.json` location, package IDs match Firebase, Push extension enabled with correct provider IDs, permissions granted. | -| Token registration fails | Ensure registration runs **after login**, provider IDs are set, and `registerDeviceForRemoteMessages()` is called. | -{/* | Notification taps do nothing | Keep Notifee foreground/background handlers and ensure the navigation ref is ready before routing. | */} -{/* | Call UI not showing | Verify CallKeep setup, telecom permissions, and that `VoipNotificationHandler.initialize()` runs post-login. | */} -{/* | Inline reply needed | Extend Notifee action buttons; CometChat expects you to send the message manually after reading `remoteMessage.data`. | */} - ---- - -## Next Steps - - - -Set up APNs push notifications for iOS - - -Strip HTML tags and customize notification content - - -Learn how to send different types of messages - - -Handle incoming messages in real time - - diff --git a/notifications/react-native-push-notifications-ios.mdx b/notifications/react-native-push-notifications-ios.mdx deleted file mode 100644 index f5489fc6e..000000000 --- a/notifications/react-native-push-notifications-ios.mdx +++ /dev/null @@ -1,846 +0,0 @@ ---- -title: "React Native Push Notifications (iOS)" -description: "Bring the SampleAppWithPushNotifications experience—APNs + VoIP—into any React Native project using CometChat UI Kit." ---- - - - -| Field | Value | -| --- | --- | -| Platform | iOS (APNs + PushKit/CallKit) | -| Key Classes | `CometChatNotifications`, `VoipNotificationHandler`, `PendingCallManager` | -| Key Methods | `registerPushToken()`, `unregisterPushToken()`, `PushNotificationIOS.requestPermissions()` | -| Push Platforms | `APNS_REACT_NATIVE_DEVICE`, `APNS_REACT_NATIVE_VOIP` | -| Prerequisites | CometChat SDK initialized, user logged in, APNs `.p8` key uploaded, physical iOS device | - - - - - Reference implementation of React Native UI Kit and APNs Push Notification setup. - - -## What this guide covers - -- CometChat Dashboard setup (enable push, add APNs provider). -- Platform credentials (Apple entitlements). -- 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. - -## What you need first - -- CometChat app credentials (App ID, Region, Auth Key) and Push Notifications enabled with an **APNs provider (React Native iOS)**; add an **APNs VoIP provider** if you plan to receive call invites via PushKit. -- Apple push setup: APNs `.p8` key/cert in CometChat, iOS project with Push Notifications + Background Modes (Remote notifications) permissions. -- React Native 0.81+, Node 18+, physical iOS device for reliable push/call testing. - -## How APNs + CometChat work together - -- **APNs (iOS) is the transport:** Apple issues the APNs token and delivers payloads to devices. -- **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) - -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 - -- Update `src/utils/AppConstants.tsx` with `appId`, `authKey`, `region`, and `apnProviderId`. -- Keep `app.json` name consistent with your bundle ID / applicationId. - -```ts lines -const APP_ID = ""; -const AUTH_KEY = ""; -const REGION = ""; -const DEMO_UID = "cometchat-uid-1"; -``` - -### 3.1 Dependencies snapshot (from Sample App) - -Install these dependencies in your React Native app: - -```npm lines -npm install \ - @cometchat/chat-sdk-react-native@4.0.18 \ - @cometchat/calls-sdk-react-native@4.4.0 \ - @cometchat/chat-uikit-react-native@5.2.6 \ - @notifee/react-native@9.1.8 \ - @react-native-async-storage/async-storage@2.2.0 \ - @react-native-community/push-notification-ios@1.12.0 \ - react-native-push-notification@8.1.1 \ - react-native-callkeep@4.3.16 \ - react-native-voip-push-notification@3.3.3 -``` - -Match these or newer compatible versions in your app. - -## 4. iOS App setup - -### 4.1 Project Setup - -Enable **Push Notifications** and **Background Modes** (Remote notifications) in Xcode. - - - Enable Push Notifications - - -### 4.2 Install dependencies + pods - -After running the npm install above, install pods from the `ios` directory: -```bash lines -cd ios -pod install -``` - -### 4.3 AppDelegate.swift modifications: - -Add imports at the top: -```swift lines -import UserNotifications -import RNCPushNotificationIOS -``` - -Add `UNUserNotificationCenterDelegate` to the `AppDelegate` class declaration: -```swift -class AppDelegate: UIResponder, UIApplicationDelegate, UNUserNotificationCenterDelegate -``` - -Add the following inside the `didFinishLaunchingWithOptions` method: -```swift lines -UNUserNotificationCenter.current().delegate = self - -UNUserNotificationCenter.current().requestAuthorization( - options: [.alert, .badge, .sound] -) { - granted, - error in - if granted { - DispatchQueue.main.async { - application.registerForRemoteNotifications() - } - } else { - print("Push Notification permission not granted: \(String(describing: error))") - } -} -``` - -Add the following methods to handle push notification events: -```swift lines -func application(_ application: UIApplication, didRegisterForRemoteNotificationsWithDeviceToken deviceToken: Data) { - print("APNs device token received: \(deviceToken)") - RNCPushNotificationIOS.didRegisterForRemoteNotifications(withDeviceToken: deviceToken) -} - -func application(_ application: UIApplication, didFailToRegisterForRemoteNotificationsWithError error: Error) { - print("APNs registration failed: \(error)") - RNCPushNotificationIOS.didFailToRegisterForRemoteNotificationsWithError(error) -} - -func application(_ application: UIApplication, didReceiveRemoteNotification userInfo: [AnyHashable: Any], fetchCompletionHandler completionHandler: @escaping (UIBackgroundFetchResult) -> Void) { - RNCPushNotificationIOS.didReceiveRemoteNotification(userInfo, fetchCompletionHandler: completionHandler) -} - -func userNotificationCenter(_ center: UNUserNotificationCenter, willPresent notification: UNNotification, withCompletionHandler completionHandler: @escaping (UNNotificationPresentationOptions) -> Void) { - completionHandler([.banner, .sound, .badge]) -} - -func userNotificationCenter(_ center: UNUserNotificationCenter, didReceive response: UNNotificationResponse, withCompletionHandler completionHandler: @escaping () -> Void) { - RNCPushNotificationIOS.didReceive(response) - completionHandler() -} -``` - -Add the following to `Podfile` to avoid framework linkage issues: -```ruby -use_frameworks! :linkage => :static -``` - -You might have to remove below code if already present in your Podfile: -```ruby lines -linkage = ENV['USE_FRAMEWORKS'] -if linkage != nil - Pod::UI.puts "Configuring Pod with #{linkage}ally linked Frameworks".green - use_frameworks! :linkage => linkage.to_sym -end -``` - -Then lets install pods and open the workspace: -```bash lines -cd ios -pod install -open YourProjectName.xcworkspace -``` - -### 4.4 App.tsx modifications: - -Import CometChatNotifications and PushNotificationIOS: - -```tsx -import { CometChat, CometChatNotifications } from "@cometchat/chat-sdk-react-native"; -import PushNotificationIOS from "@react-native-community/push-notification-ios"; -``` - -Get device token and store it in a ref: -Also, define your APNs provider ID from the CometChat Dashboard. -And request permissions on mount: - -```tsx lines -const APNS_PROVIDER_ID = 'YOUR_APNS_PROVIDER_ID'; // from CometChat Dashboard -const apnsTokenRef = useRef < string | null > (null); - -useEffect(() => { - if (Platform.OS !== 'ios') return; - - const onRegister = (deviceToken: string) => { - console.log(' APNs device token captured:', deviceToken); - apnsTokenRef.current = deviceToken; - }; - - PushNotificationIOS.addEventListener('register', onRegister); - - PushNotificationIOS.addEventListener('registrationError', error => { - console.error(' APNs registration error:', error); - }); - - // Trigger permission + native registration - PushNotificationIOS.requestPermissions().then(p => - console.log('Push permissions:', p), - ); - - return () => { - PushNotificationIOS.removeEventListener('register'); - PushNotificationIOS.removeEventListener('registrationError'); - }; -}, []); -``` - -After user login, register the APNs token: -```tsx lines -// Register token ONLY if we already have it -if (apnsTokenRef.current) { - await CometChatNotifications.registerPushToken( - apnsTokenRef.current, - CometChatNotifications.PushPlatforms.APNS_REACT_NATIVE_DEVICE, - APNS_PROVIDER_ID - ); - console.log(' APNs token registered with CometChat'); -} -``` - -Prior to logout, unregister the APNs token: -```tsx -await CometChatNotifications.unregisterPushToken(); -``` - -## 5. VoIP call notifications (iOS) - -These steps are iOS-only—copy/paste and fill your IDs. - -### 5.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) -Update your `AppDelegate` to register for VoIP pushes ASAP and forward events to JS/CallKeep: - -```swift lines -import PushKit -import RNVoipPushNotification -import RNCallKeep -// ... -@main -class AppDelegate: UIResponder, UIApplicationDelegate, UNUserNotificationCenterDelegate, PKPushRegistryDelegate { - // ... - func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? = nil) -> Bool { - // existing UNUserNotificationCenter code ... - RNVoipPushNotificationManager.voipRegistration() // triggers PushKit token - return true - } - - // APNs device token handlers stay unchanged - - // PushKit token -> JS - func pushRegistry(_ registry: PKPushRegistry, didUpdate pushCredentials: PKPushCredentials, for type: PKPushType) { - RNVoipPushNotificationManager.didUpdate(pushCredentials, forType: type.rawValue) - } - - // Incoming VoIP push -> CallKit + JS - func pushRegistry(_ registry: PKPushRegistry, didReceiveIncomingPushWith payload: PKPushPayload, for type: PKPushType, completion: @escaping () -> Void) { - let dict = payload.dictionaryPayload - let uuid = (dict["uuid"] as? String) ?? UUID().uuidString - RNVoipPushNotificationManager.addCompletionHandler(uuid, completionHandler: completion) - RNVoipPushNotificationManager.didReceiveIncomingPush(with: payload, forType: type.rawValue) - RNCallKeep.reportNewIncomingCall(uuid, handle: (dict["handle"] as? String) ?? "Unknown", handleType: "generic", hasVideo: false, localizedCallerName: (dict["callerName"] as? String) ?? "Incoming Call", supportsHolding: true, supportsDTMF: true, supportsGrouping: true, supportsUngrouping: true, fromPushKit: true, payload: nil) - } -} -``` - -### 5.3 Drop in `VoipNotificationHandler.ts` -Handles CallKeep UI, defers acceptance until login, and listens for PushKit events. - -```ts lines -import { Platform } from "react-native"; -import notifee, { AndroidImportance } from "@notifee/react-native"; -import RNCallKeep, { IOptions } from "react-native-callkeep"; -import { CometChat } from "@cometchat/chat-sdk-react-native"; -import VoipPushNotification from "react-native-voip-push-notification"; -import { setPendingAnsweredCall } from "./PendingCallManager"; - -const options: IOptions = { - ios: { appName: "YourAppName" }, - android: { alertTitle: "VOIP required", alertDescription: "Allow phone account access", cancelButton: "Cancel", okButton: "OK", imageName: "ic_notification" }, -}; - -type IncomingPayload = { sessionId?: string; senderName?: string; callerName?: string; name?: string; type?: string; [k: string]: any; }; - -class VoipNotificationHandler { - channelId = ""; - isRinging = false; - isAnswered = false; - pendingAcceptance = false; - callerId = ""; - msg: IncomingPayload | null = null; - initialized = false; - private setupPromise: Promise | null = null; - private listenersAttached = false; - private lastSessionId: string | null = null; - private lastRingAt = 0; - - async initialize() { - if (this.initialized && this.setupPromise) { await this.setupPromise; return; } - if (!this.setupPromise) { - this.setupPromise = (async () => { - if (Platform.OS === "android") { await this.createNotificationChannel(); } - await this.setupCallKeep(); - this.setupEventListeners(); - this.initialized = true; - })().catch(err => { this.setupPromise = null; throw err; }); - } - await this.setupPromise; - } - - private async setupCallKeep() { - await RNCallKeep.setup(options); - RNCallKeep.setAvailable(true); - if (Platform.OS === "android") { RNCallKeep.setReachable(); } - } - - private async createNotificationChannel() { - this.channelId = await notifee.createChannel({ id: "message", name: "Messages", lights: true, vibration: true, importance: AndroidImportance.HIGH }); - } - - async displayIncomingCall(payload: IncomingPayload) { - this.msg = payload || {}; - const sessionId = this.msg?.sessionId; - const now = Date.now(); - if (sessionId && this.lastSessionId === sessionId && now - this.lastRingAt < 5000) return; - if (this.isAnswered || this.pendingAcceptance) return; - await this.initialize(); - - const callerName = this.msg?.senderName || this.msg?.callerName || this.msg?.name || "Incoming Call"; - this.callerId = this.callerId || Math.random().toString(); - this.isRinging = true; - - await RNCallKeep.displayIncomingCall(this.callerId, callerName, callerName, "generic", true); - this.lastSessionId = sessionId || null; - this.lastRingAt = now; - } - - onAnswerCall = async ({ callUUID }: { callUUID: string }) => { - if (this.isAnswered) return; - this.isRinging = false; this.isAnswered = true; - const sessionID = this.msg?.sessionId; if (!sessionID) return; - RNCallKeep.backToForeground(); - setTimeout(async () => { - const loggedInUser = await CometChat.getLoggedinUser().catch(() => null); - if (!loggedInUser) { this.pendingAcceptance = true; await setPendingAnsweredCall({ sessionId: sessionID, raw: this.msg, storedAt: Date.now() }); return; } - try { await CometChat.acceptCall(sessionID); } catch (error: any) { if (error?.code !== "ERR_CALL_USER_ALREADY_JOINED") throw error; } - RNCallKeep.endAllCalls(); this.pendingAcceptance = false; - }, 350); - }; - - endCall = async ({ callUUID }: { callUUID: string }) => { - const sessionID = this.msg?.sessionId; - if (sessionID) { - const loggedInUser = await CometChat.getLoggedinUser().catch(() => null); - if (this.isAnswered) { await CometChat.endCall(sessionID).catch(() => {}); } - else if (loggedInUser) { await CometChat.rejectCall(sessionID, CometChat.CALL_STATUS.REJECTED).catch(() => {}); } - } - const id = callUUID || this.callerId; - if (id) RNCallKeep.endCall(id); - RNCallKeep.endAllCalls(); - this.isRinging = false; this.isAnswered = false; this.pendingAcceptance = false; this.callerId = ""; this.msg = null; this.lastSessionId = null; this.lastRingAt = 0; - }; - - setupEventListeners() { - if (this.listenersAttached) return; - if (Platform.OS === "ios") { - VoipPushNotification.addEventListener("notification", (notification: any) => this.displayIncomingCall(notification)); - VoipPushNotification.addEventListener("didLoadWithEvents", (events: any[]) => { - (events || []).forEach(event => { - if (event?.name === VoipPushNotification.RNVoipPushRemoteNotificationReceivedEvent) { - this.displayIncomingCall(event.data); - } - }); - }); - } - RNCallKeep.addEventListener("answerCall", this.onAnswerCall); - RNCallKeep.addEventListener("endCall", this.endCall); - RNCallKeep.addEventListener("didDisplayIncomingCall", ({ callUUID }) => { if (callUUID) this.callerId = callUUID; this.isRinging = true; }); - this.listenersAttached = true; - } -} - -export const voipHandler = new VoipNotificationHandler(); -``` - -### 5.4 Add `PendingCallManager.ts` -Stores an answered call during cold start so you can accept it after login/navigation is ready. - -```ts lines -import AsyncStorage from "@react-native-async-storage/async-storage"; - -export interface PendingAnsweredCallPayload { sessionId: string; raw: any; storedAt: number; } -let inMemoryPending: PendingAnsweredCallPayload | null = null; -const STORAGE_KEY = "pendingAnsweredCall"; - -export async function setPendingAnsweredCall(payload: PendingAnsweredCallPayload) { - inMemoryPending = payload; try { await AsyncStorage.setItem(STORAGE_KEY, JSON.stringify(payload)); } catch {} -} - -export async function consumePendingAnsweredCall(): Promise { - if (inMemoryPending) { const tmp = inMemoryPending; inMemoryPending = null; try { await AsyncStorage.removeItem(STORAGE_KEY); } catch {} return tmp; } - try { const raw = await AsyncStorage.getItem(STORAGE_KEY); if (raw) { await AsyncStorage.removeItem(STORAGE_KEY); return JSON.parse(raw); } } catch {} - return null; -} - -export function isPendingStale(p: PendingAnsweredCallPayload, maxAgeMs = 2 * 60 * 1000) { - return Date.now() - p.storedAt > maxAgeMs; -} -``` - -### 5.5 Wire `App.tsx` for APNs + VoIP token registration and handler init - -```tsx lines -import PushNotificationIOS from "@react-native-community/push-notification-ios"; -import VoipPushNotification from "react-native-voip-push-notification"; -import { voipHandler } from "./VoipNotificationHandler"; -import { consumePendingAnsweredCall, isPendingStale } from "./PendingCallManager"; - -const APNS_PROVIDER_ID = "YOUR_APNS_PROVIDER_ID"; - -// Capture APNs device token -useEffect(() => { - if (Platform.OS !== "ios") return; - const onRegister = (deviceToken: string) => { apnsTokenRef.current = deviceToken; }; - PushNotificationIOS.addEventListener("register", onRegister); - PushNotificationIOS.requestPermissions(); - return () => PushNotificationIOS.removeEventListener("register"); -}, []); - -// Capture VoIP token -useEffect(() => { - if (Platform.OS !== "ios") return; - const onVoipRegister = (token: string) => { - CometChatNotifications.registerPushToken( - token, - CometChatNotifications.PushPlatforms.APNS_REACT_NATIVE_VOIP, - APNS_PROVIDER_ID - ).catch(err => console.log("[VoIP] register failed", err)); - }; - VoipPushNotification.addEventListener("register", onVoipRegister); - // token request is triggered in AppDelegate via RNVoipPushNotificationManager.voipRegistration() - return () => VoipPushNotification.removeEventListener("register"); -}, []); - -// After login: register APNs token + init VoIP handler + consume pending accepts -useEffect(() => { - const run = async () => { - if (!loggedIn || Platform.OS !== "ios") return; - const pending = await consumePendingAnsweredCall(); - if (pending && !isPendingStale(pending)) { await CometChat.acceptCall(pending.sessionId).catch(console.log); } - const token = apnsTokenRef.current; - if (token) { - await CometChatNotifications.registerPushToken( - token, - CometChatNotifications.PushPlatforms.APNS_REACT_NATIVE_DEVICE, - APNS_PROVIDER_ID - ); - } - await voipHandler.initialize(); - }; - run(); -}, [loggedIn]); -``` - -### 5.6 VoIP push payload (APNs / PushKit) -Send a VoIP push with `push_type=voip` via APNs using a payload shaped like: - -```json -{ - "aps": { "alert": { "title": "Alice", "body": "Incoming call" }, "content-available": 1 }, - "sessionId": "", - "callerName": "Alice", - "handle": "alice", - "type": "call", - "uuid": "" -} -``` - -## 6. 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 - -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 - - - - Go to **CometChat Dashboard → Notifications Engine → Settings → Preferences → Push Notification Preferences**. - - - Scroll down and enable the **Unread Badge Count** toggle. - - - -Once enabled, CometChat automatically includes the `unreadMessageCount` field in every push payload sent to your app. - -### 7.2 Expected Payload Format - -CometChat sends APNs payloads with the following structure: - -```json -{ - "aps": { - "alert": { - "title": "New Message", - "body": "John: Hello!" - }, - "badge": 5, - "sound": "default" - }, - "unreadMessageCount": "5", - "conversationId": "user_abc123" -} -``` - - -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 - -Update your iOS notification handler to set the badge count programmatically: - -```typescript -import PushNotificationIOS from "@react-native-community/push-notification-ios"; - -export async function onRemoteNotificationIOS(notification: any) { - // Extract badge count from push payload - const data = notification.getData(); - const unreadCount = data?.unreadMessageCount; - - if (unreadCount !== undefined && unreadCount !== null) { - const count = parseInt(unreadCount, 10); - if (!isNaN(count) && count >= 0) { - PushNotificationIOS.setApplicationIconBadgeNumber(count); - console.log("Badge count updated (iOS):", count); - } - } - - // Handle notification tap - const isClicked = data?.userInteraction === 1; - if (isClicked && data?.type === "chat") { - // Navigate to conversation... - } - - // Required: Notify iOS that processing is complete - notification.finish(PushNotificationIOS.FetchResult.NoData); -} -``` - -### 7.4 Register Notification Listener - -In your `App.tsx`, set up the notification listener: - -```typescript -import PushNotificationIOS from "@react-native-community/push-notification-ios"; - -useEffect(() => { - if (Platform.OS === "ios") { - const onNotification = async (notification: any) => { - try { - await onRemoteNotificationIOS(notification); - } catch (error) { - console.log("Error in onRemoteNotificationIOS:", error); - } - }; - - PushNotificationIOS.addEventListener("notification", onNotification); - - return () => { - PushNotificationIOS.removeEventListener("notification"); - }; - } -}, []); -``` - -### 7.5 Clear Badge When App Becomes Active - -Clear the badge count when the app launches or returns to the foreground: - -```typescript -import { AppState, AppStateStatus, Platform } from "react-native"; -import PushNotificationIOS from "@react-native-community/push-notification-ios"; - -useEffect(() => { - const handleAppStateChange = async (nextState: AppStateStatus) => { - if (nextState === "active" && Platform.OS === "ios") { - PushNotificationIOS.setApplicationIconBadgeNumber(0); - console.log("Badge cleared (iOS)"); - } - }; - - const subscription = AppState.addEventListener("change", handleAppStateChange); - return () => subscription.remove(); -}, []); -``` - -### 7.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: - -```typescript -import PushNotificationIOS from "@react-native-community/push-notification-ios"; -import { CometChat, CometChatNotifications } from "@cometchat/chat-sdk-react-native"; - -const handleLogout = async () => { - // Unregister push token first - await CometChatNotifications.unregisterPushToken(); - - // Clear badge before logout - PushNotificationIOS.setApplicationIconBadgeNumber(0); - - // Logout from CometChat - await CometChat.logout(); - console.log("User logged out, badge cleared"); -}; -``` - -### 7.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: - -```typescript -import PushNotificationIOS from "@react-native-community/push-notification-ios"; -import { CometChat } from "@cometchat/chat-sdk-react-native"; - -// During app initialization, after CometChat.init() -const initializeApp = async () => { - // Initialize CometChat first - await CometChatUIKit.init(uiKitSettings); - - // Check if user is logged in - const loggedInUser = await CometChat.getLoggedinUser(); - - if (!loggedInUser) { - // No user logged in - clear any stale badge - PushNotificationIOS.setApplicationIconBadgeNumber(0); - console.log("No logged-in user, badge cleared"); - } -}; -``` - -### 7.8 Clear Badge in Login Listener (Safety Net) - -Register a login listener to clear the badge on logout as a backup mechanism: - -```typescript -import PushNotificationIOS from "@react-native-community/push-notification-ios"; -import { CometChat } from "@cometchat/chat-sdk-react-native"; - -useEffect(() => { - const listenerID = "BADGE_LOGOUT_LISTENER"; - - CometChat.addLoginListener( - listenerID, - new CometChat.LoginListener({ - logoutOnSuccess: () => { - // Safety net: clear badge when logout succeeds - PushNotificationIOS.setApplicationIconBadgeNumber(0); - console.log("Logout listener: badge cleared"); - }, - }) - ); - - return () => { - CometChat.removeLoginListener(listenerID); - }; -}, []); -``` - -### 7.9 Key Implementation Notes - -| Consideration | Details | -| --- | --- | -| **Backend-driven badge count** | The `unreadMessageCount` value comes directly from CometChat's backend via the push payload, ensuring consistency across all devices. | -| **iOS server-side badge** | For iOS using APNs, the `aps.badge` field is set server-side by CometChat, so the badge updates automatically even without client-side code. However, you still need to clear it when the app opens. | -| **Clear on app active** | Always clear the badge when the app becomes active. New notifications will update the badge with the fresh `unreadMessageCount` from the backend. | -| **Clear on logout** | Always clear the badge when a user logs out to prevent stale counts for the next user. | -| **Clear on fresh install** | On iOS, the badge count may persist after app reinstall in certain scenarios. Clear the badge during app initialization when no user is logged in. | -| **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 - -If you're building a cross-platform app, use this combined handler for both iOS and Android: - -```typescript -import { AppState, AppStateStatus, Platform } from "react-native"; -import PushNotificationIOS from "@react-native-community/push-notification-ios"; -import notifee from "@notifee/react-native"; - -useEffect(() => { - const handleAppStateChange = async (nextState: AppStateStatus) => { - if (nextState === "active") { - // Clear badge for iOS - if (Platform.OS === "ios") { - PushNotificationIOS.setApplicationIconBadgeNumber(0); - console.log("Badge cleared (iOS)"); - } - // Clear all notifications for Android (also resets badge) - else if (Platform.OS === "android") { - await notifee.cancelAllNotifications(); - console.log("Notifications cleared (Android)"); - } - } - }; - - const subscription = AppState.addEventListener("change", handleAppStateChange); - return () => subscription.remove(); -}, []); -``` - -## 8. Testing Checklist - -1. Install on a physical iOS device, log in, and verify APNs token registration succeeds. -2. Send a message from another user: - - **Foreground:** Banner appears unless that chat is already open. - - **Background/terminated:** Tap opens the correct conversation; handler runs. -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 - -| Symptom | Quick Checks | -| --- | --- | -| No pushes | Confirm APNs key uploaded, bundle ID matches, Push extension enabled with correct provider IDs, permissions granted. | -| Token registration fails | Ensure registration runs **after login**, provider IDs are set, and `registerForRemoteNotifications()` is called. | -{/* | Notification taps do nothing | Keep foreground/background handlers and ensure navigation ref is ready before routing. | */} -{/* | Call UI not showing | Verify PushKit VoIP capability, CallKeep entitlements/permissions, and that `voipHandler.initialize()` runs after login. | */} -{/* | Inline reply needed | Extend Notifee action buttons; CometChat expects you to send the message manually after reading `remoteMessage.data`. | */} - ---- - -## Next Steps - - - -Set up FCM push notifications for Android - - -Strip HTML tags and customize notification content - - -Learn how to send different types of messages - - -Handle incoming messages in real time - - diff --git a/notifications/react-native-push-notifications.mdx b/notifications/react-native-push-notifications.mdx new file mode 100644 index 000000000..a0ce3c8cb --- /dev/null +++ b/notifications/react-native-push-notifications.mdx @@ -0,0 +1,375 @@ +--- +title: "React Native" +description: "Add CometChat push notifications and VoIP calls to a React Native app (Android + iOS) with the drop-in @cometchat/push-notifications-react-native package." +--- + +## What this guide covers + +- Adding the `@cometchat/push-notifications-react-native` package and initializing it. +- Platform wiring: Firebase/`google-services.json` on Android, the setup CLI + PushKit forwarding on iOS. +- Requesting permission and registering tokens (FCM on Android, APNs + VoIP on iOS) after login. +- Receiving pushes and letting the package render chat notifications and full-screen / CallKit calls. +- Handling notification taps (including thread deep-links), incoming-call navigation, and Android OEM permissions. +- Testing and troubleshooting. + + +The `@cometchat/push-notifications-react-native` package replaces the previous approach of copying the sample app's `notifications` stack and hand-wiring `@react-native-firebase/messaging`, `notifee`, `react-native-callkeep`, and `react-native-voip-push-notification`. Token registration, foreground presentation, notification taps, and the full incoming-call experience (the Android lock-screen call activity and iOS CallKit) are handled inside the package — the design is **JS-first**: native code only shows the UI and captures tokens, while every CometChat action (register token, accept/reject/end call) runs in JavaScript through the Chat SDK your app already ships. + + +## How it works + +- **Android (FCM):** Firebase issues the registration token and delivers the CometChat payload as a data message. The package ships its **own** `FirebaseMessagingService`, so it receives the message and shows the notification or full-screen call itself — **you write no FCM handling code**. +- **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 package. Your `AppDelegate` forwards the tokens and incoming VoIP pushes to the package (the setup CLI generates this). +- **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 package's role:** it retrieves the tokens, registers them with CometChat, parses payloads, drives the call UI, and calls the Chat SDK to accept/reject/end. It requires [`@cometchat/chat-sdk-react-native`](https://www.npmjs.com/package/@cometchat/chat-sdk-react-native) as a peer dependency — the one Chat SDK your app already uses, so there is no second SDK to version-align. + +## Prerequisites + +- The providers, Firebase project, and Apple/APNs credentials from **[Getting Started](/notifications/push-getting-started)** (this guide assumes those are done). +- React Native **0.65+**, and an app already initializing and logging in with `@cometchat/chat-sdk-react-native` (or the UI Kit). +- **Android:** `google-services.json` in `android/app/`, the `com.google.gms.google-services` plugin, `minSdkVersion 24`+. +- **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 React Native 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: + +```ts lines +export const AppCredentials = { + appId: "YOUR_APP_ID", + region: "YOUR_REGION", + authKey: "YOUR_AUTH_KEY", + + // Android + fcmProviderId: "FCM-PROVIDER-ID", + + // iOS — one provider covers the APNs device token and the VoIP token + apnsProviderId: "APNS-PROVIDER-ID", +}; +``` + +## 2. Add the package and configure the platform + +Install the package (the Chat SDK peer is already in your app): + +```bash +npm install @cometchat/push-notifications-react-native +# or: yarn add @cometchat/push-notifications-react-native +``` + + + + With `google-services.json` already in `android/app/` (from [Getting Started](/notifications/push-getting-started)): + + 1. Apply the Google Services plugin and Firebase Messaging in your Gradle files: + + ```groovy lines + // android/build.gradle + buildscript { + dependencies { + classpath("com.google.gms:google-services:4.4.2") + } + } + ``` + + ```groovy lines + // android/app/build.gradle + apply plugin: "com.google.gms.google-services" + + dependencies { + implementation platform("com.google.firebase:firebase-bom:33.16.0") + implementation "com.google.firebase:firebase-messaging" + } + ``` + + 2. Keep `minSdkVersion 24` or higher. + + + You do **not** need to add notification, call, full-screen-intent, or lock-screen permissions to your `AndroidManifest.xml`, and you write **no** FCM/JS message-handling code. The package's library manifest contributes everything it needs — the `FirebaseMessagingService`, the incoming-call foreground service, the full-screen lock-screen `CallRingingActivity`, the notification trampoline/decline receiver, and `POST_NOTIFICATIONS` — 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. Run the setup CLI from your project root — it adds the required `UIBackgroundModes` (`voip`, `remote-notification`, `audio`) and the mic/camera usage strings to `Info.plist`, and generates `ios//CometChatPushNotifications+AppDelegate.swift`: + + ```bash + npx cometchat-pn setup + ``` + + 3. In Xcode, **add the generated `CometChatPushNotifications+AppDelegate.swift` to your app target**, and enable the **Push Notifications** and **Background Modes** capabilities (the latter with *Voice over IP* + *Remote notifications* + *Audio*). + + 4. Forward the PushKit/APNs events to the package from your `AppDelegate`. Create the `PKPushRegistry` on a **background queue** — on a killed-app cold start iOS delivers the incoming push on that queue, and a `.main` queue would sit behind React Native's startup and miss iOS's ~5s "report a call" deadline (iOS then terminates the app with no CallKit UI): + + ```swift lines + import PushKit + + // in application(_:didFinishLaunchingWithOptions:) + let registry = PKPushRegistry(queue: DispatchQueue(label: "com.cometchat.voip.pushkit")) + registry.delegate = self + registry.desiredPushTypes = [.voIP] + + // APNs device token (chat/alert pushes) + override func application(_ application: UIApplication, + didRegisterForRemoteNotificationsWithDeviceToken deviceToken: Data) { + CometChatPushNotificationsAppDelegate.didRegisterAPNsToken(deviceToken) + } + + // VoIP token + incoming VoIP push (PKPushRegistryDelegate) + func pushRegistry(_ registry: PKPushRegistry, + didUpdate credentials: PKPushCredentials, for type: PKPushType) { + CometChatPushNotificationsAppDelegate.didUpdateVoIPToken(credentials.token) + } + func pushRegistry(_ registry: PKPushRegistry, + didReceiveIncomingPushWith payload: PKPushPayload, for type: PKPushType, + completion: @escaping () -> Void) { + CometChatPushNotificationsAppDelegate.didReceiveIncomingVoIPPush(payload.dictionaryPayload) + completion() + } + ``` + + 5. Verify the wiring at any time: + + ```bash + npx cometchat-pn doctor + ``` + + + +## 3. Initialize the SDK + +Register the killed-state background task **at module scope** in `index.js` (before any component renders), then initialize the package **after the user logs in**. + +```js lines +// index.js +import { AppRegistry } from 'react-native'; +import { CometChat } from '@cometchat/chat-sdk-react-native'; +import { registerBackgroundCallTask } from '@cometchat/push-notifications-react-native'; +import App from './App'; +import { name as appName } from './app.json'; + +// Android only: runs when the app is FULLY KILLED and the user taps Decline on the +// call notification. A killed app has no JS alive, so this headless task boots just +// enough to reject the call — otherwise the caller only times out. (iOS declines +// natively via CallKit, so this is a no-op there.) +registerBackgroundCallTask(async (action, info) => { + if (action === 'decline' && info.sessionId) { + // (re)initialize + login your CometChat session here, then: + await CometChat.rejectCall(info.sessionId, CometChat.CALL_STATUS.REJECTED); + } +}); + +AppRegistry.registerComponent(appName, () => App); +``` + +```ts lines +// after CometChat.init(...) + login succeeds — e.g. a setupPush() you call on login: +import { + CometChatPushNotifications, + CometChatPNHelper, +} from '@cometchat/push-notifications-react-native'; + +let subscriptions: Array<() => void> = []; + +export async function setupPush() { + await CometChatPNHelper.requestNotificationPermission(); + await CometChatPNHelper.requestCallPermissions(); // mic + camera (Android) before a call connects + + subscriptions.push( + CometChatPushNotifications.onNotificationTap(handleTap), + CometChatPushNotifications.onCallAccepted(info => + navigate('OngoingCall', { sessionId: info.sessionId, callType: info.callType })), + CometChatPushNotifications.onCallEnded(handleCallEnded), + CometChatPushNotifications.onMessageReceived(data => + console.log('data push:', data)), + ); + + await CometChatPushNotifications.init({ + fcmProviderId: AppCredentials.fcmProviderId, // Android + apnsProviderId: AppCredentials.apnsProviderId, // iOS (APNs device + VoIP) + // Foreground pushes are rendered in-app (chat UI / WebSocket call overlay), + // so suppress the duplicate system banner/CallKit while the app is active: + showInForeground: false, + }); +} +``` + +`init` wires the native events to the Chat SDK, **auto-registers** the device tokens, and drains any cold-start tap/call the app was launched from. It is safe to call again on re-login. + +`init` also accepts: `voip` (default `true`), `notificationSmallIcon`, `androidChannelId`, and `androidChannelName`. + +## 4. Request permission and register tokens + +Permission is requested via `CometChatPNHelper` (call it before pushes/calls arrive): + +```ts lines +await CometChatPNHelper.requestNotificationPermission(); // POST_NOTIFICATIONS (Android 13+) / iOS +await CometChatPNHelper.requestCallPermissions(); // mic + camera (Android 14+ call FGS) +// non-prompting check: +const enabled = await CometChatPNHelper.hasNotificationPermission(); +``` + +**Token registration is automatic** — `init()` registers the FCM token (Android) and the APNs device + VoIP tokens (iOS) with CometChat, and re-registers on refresh. You rarely need to do it by hand, but you can: + +```ts lines +await CometChatPushNotifications.registerToken('fcm', token); // or 'apns' / 'voip' +``` + +**On logout**, unsubscribe your handlers and unregister the token so the device stops receiving pushes for that user: + +```ts lines +export async function teardownPush() { + subscriptions.forEach(unsub => unsub()); + subscriptions = []; + await CometChatPushNotifications.unregister(); +} +``` + + +Not unsubscribing on logout leaves the callbacks registered, so a logout → login cycle would fire each handler twice (e.g. navigating to a tapped message twice). + + +## 5. Notification taps and call events + +Subscribe once (in `setupPush` above). Each subscribe returns an unsubscribe function. + +**Notification tap** — open the conversation, or the **thread** when the push is a thread reply: + +```ts lines +async function handleTap(info) { + // info: { receiverType, sender, receiver, conversationId, messageId, parentMessageId, senderName } + if (info.parentMessageId) { + const parent = await CometChat.getMessageDetails(info.parentMessageId); + navigate('ThreadView', { message: parent, highlightMessageId: info.messageId }); + return; + } + navigateToConversation({ + receiverType: info.receiverType, + sender: info.sender, + conversationId: info.conversationId, + }); +} +``` + +**Call accepted** — the package has *already* accepted the call via the Chat SDK; just open your call screen: + +```ts lines +CometChatPushNotifications.onCallAccepted(info => { + navigate('OngoingCall', { sessionId: info.sessionId, callType: info.callType }); +}); +``` + +**Call ended** — a ringing call was cancelled/declined/ended, **or** the user ended the call from the iOS CallKit UI. The Calls SDK's own listener doesn't see a CallKit-initiated end, so tear the call down here: + +```ts lines +import { CometChatCalls } from '@cometchat/calls-sdk-react-native'; + +function handleCallEnded(info) { + if (info.sessionId) CometChat.endCall(info.sessionId).catch(() => {}); + try { CometChatCalls.endSession(); } catch {} + CometChat.clearActiveCall?.(); + // leave the ongoing-call screen if you're on it +} +``` + + +**Cold-start VoIP handling (iOS):** when the app is killed and a VoIP push arrives, the package presents CallKit natively via PushKit before React Native is ready. When the user answers, the app cold-starts, `init()` replays the accepted call, `onCallAccepted` fires, and the package has already called `CometChat.acceptCall` — set up your call session/screen there. (This path is why the `PKPushRegistry` must be on a background queue — see step 2.) + + +## 6. Android: OEM permissions for lock-screen calls + +The package declares the standard permissions and uses the correct `setShowWhenLocked` / `setTurnScreenOn` flags, so full-screen calls over the lock screen work out of the box on stock Android (including Android 14+). **OEM skins (MIUI/Redmi/POCO, Oppo, Vivo) additionally gate background-launched full-screen activities** behind their own toggles — without them, a locked/killed call shows only a heads-up notification (with ringtone), and the full-screen screen appears only after unlock. + +Guide users to grant, on those devices: + +- **Autostart** — Settings → Apps → *your app* → Autostart (or the Security app). +- **Display pop-up windows while running in background** — Settings → Apps → *your app* → Other permissions. +- **Show on lock screen** — same "Other permissions" screen. +- Disable **battery optimization** for the app. + +These OEM settings cannot be granted programmatically (the OS blocks it); open the app's settings page so the user can toggle them: + +```ts lines +import { Linking, Platform } from 'react-native'; +if (Platform.OS === 'android') Linking.openSettings(); +``` + +## 7. Badge count + +CometChat's Enhanced Push payload includes an `unreadMessageCount` field (total unread across conversations). + + + + With 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. + + + Android has no OS-level app-icon badge API. If you want a launcher badge, read `unreadMessageCount` from the payload in `onMessageReceived` and apply it with your own badge library — the push package does not manage launcher badges. + + ```ts lines + CometChatPushNotifications.onMessageReceived(data => { + const count = Number(data.unreadMessageCount ?? 0); + // hand `count` to your badge library + }); + ``` + + + +## 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 system banner (with `showInForeground: false`); your in-app UI shows it. + - Background: a notification appears; tapping opens the right conversation via `onNotificationTap` (and the thread, for a thread reply). +3. Force-quit the app, send another message, tap the notification, and confirm it cold-starts 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. + - **Accept** joins the call (audio works both ways) and the screen tears down when the call ends. + - **Decline** rejects the call promptly on the caller side — including from a killed state. + - **Caller cancels** while it's ringing → the callee ring dismisses. +5. On an OEM device (MIUI/Oppo/Vivo), grant the section-6 permissions and re-check locked/killed calls. + +## 9. Troubleshooting + +| Symptom | Platform | Quick checks | +| --- | --- | --- | +| No notifications received | Android | Confirm `google-services.json` is in `android/app/`, the package name matches Firebase, `firebase-messaging` + the `google-services` plugin are applied, and `POST_NOTIFICATIONS` is granted (Android 13+). | +| Killed app doesn't ring for a VoIP push | iOS | Ensure the `PKPushRegistry` is created on a **background queue** and the `AppDelegate` forwards `didReceiveIncomingVoIPPush` to `CometChatPushNotificationsAppDelegate`. Run `npx cometchat-pn doctor`. | +| Accepted call connects but has no audio | iOS | Confirm the `audio` background mode is present (the setup CLI adds it) and `react-native-webrtc` (via the Calls SDK) is linked in the app — the package coordinates CallKit's audio session with WebRTC automatically. | +| Full-screen call UI not showing on lock screen | Android | OEM gate — grant Autostart / "Display pop-up while running in background" / "Show on lock screen" and disable battery optimization (section 6). | +| Declining a killed-state call doesn't reject the caller | Both | Android: ensure `registerBackgroundCallTask` is registered at module scope in `index.js`. iOS: the package handles it via CallKit — verify `AppDelegate` forwarding. | +| Foreground call shows twice (in-app + CallKit/banner) | Both | Set `showInForeground: false` in `init()` so foreground calls use your in-app UI only. | +| Duplicate navigation after re-login | Both | Unsubscribe every handler and call `unregister()` on logout (step 4). | +| No VoIP pushes | iOS | Ensure Push Notifications + Background Modes (Voice over IP) are enabled, `aps-environment` is correct (`production` for release), the bundle ID matches the CometChat APNs VoIP provider, and the VoIP cert is uploaded to the dashboard. | +| Token registration errors | Both | Verify the provider IDs match the dashboard exactly and that `init()` runs **after** login. | + +## Resources + + + + The drop-in push & VoIP package on npm. + + + The peer Chat SDK the package registers tokens and drives calls through. + + diff --git a/sdk/react-native/push-notification-html-stripping.mdx b/sdk/react-native/push-notification-html-stripping.mdx index 8e8ccdc6a..eef10604c 100644 --- a/sdk/react-native/push-notification-html-stripping.mdx +++ b/sdk/react-native/push-notification-html-stripping.mdx @@ -243,11 +243,8 @@ If your project uses React Native Firebase (e.g., for FCM on Android), you may a ## Next Steps - -Set up FCM push notifications for Android - - -Set up APNs push notifications for iOS + +Set up FCM (Android) and APNs/VoIP (iOS) push notifications Learn how to send different types of messages From 26a23260c0840ee0e8b1b47df074175aae2a0cab Mon Sep 17 00:00:00 2001 From: Suraj Chauhan Date: Fri, 28 Aug 2026 12:43:03 +0530 Subject: [PATCH 02/20] docs(rn): point prerequisite links at push-overview (exists on main) The push-getting-started page is introduced by the still-open Flutter PR cometchat/docs#440; until it merges, link the shared setup prerequisite to the existing notifications/push-overview page so the RN guide has no dead links on main. Co-Authored-By: Claude Opus 4.8 --- notifications/react-native-push-notifications.mdx | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/notifications/react-native-push-notifications.mdx b/notifications/react-native-push-notifications.mdx index a0ce3c8cb..522c620b3 100644 --- a/notifications/react-native-push-notifications.mdx +++ b/notifications/react-native-push-notifications.mdx @@ -25,14 +25,14 @@ The `@cometchat/push-notifications-react-native` package replaces the previous a ## Prerequisites -- The providers, Firebase project, and Apple/APNs credentials from **[Getting Started](/notifications/push-getting-started)** (this guide assumes those are done). +- The providers, Firebase project, and Apple/APNs credentials from **[Getting Started](/notifications/push-overview)** (this guide assumes those are done). - React Native **0.65+**, and an app already initializing and logging in with `@cometchat/chat-sdk-react-native` (or the UI Kit). - **Android:** `google-services.json` in `android/app/`, the `com.google.gms.google-services` plugin, `minSdkVersion 24`+. - **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 React Native app wiring. +**Complete the [Getting Started](/notifications/push-overview) 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 React Native app wiring. ## 1. Store your credentials @@ -64,7 +64,7 @@ npm install @cometchat/push-notifications-react-native - With `google-services.json` already in `android/app/` (from [Getting Started](/notifications/push-getting-started)): + With `google-services.json` already in `android/app/` (from [Getting Started](/notifications/push-overview)): 1. Apply the Google Services plugin and Firebase Messaging in your Gradle files: From f2df784f5d3cbf9d5ef3673b360f80b12d533df7 Mon Sep 17 00:00:00 2001 From: Suraj Chauhan Date: Thu, 3 Sep 2026 14:46:22 +0530 Subject: [PATCH 03/20] docs(rn): add AI Integration Quick Reference accordion Matches the convention used across the React Native UI Kit pages: a machine-readable Field/Value table in an accordion directly after the frontmatter, so agents and skills can generate correct typed code without inferring the API from prose. Covers the surface the narrative guide left implicit: exported type names and their union values, the full PNConfiguration defaults, the complete PNCallInfo / PNNotificationTapInfo / PNCallEndEvent field lists, dispose(), isCometChatNotification(), the cold-start replay behaviour, and the Android permission set. Generated from the package type definitions. Co-Authored-By: Claude Opus 4.8 --- .../react-native-push-notifications.mdx | 30 +++++++++++++++++++ 1 file changed, 30 insertions(+) diff --git a/notifications/react-native-push-notifications.mdx b/notifications/react-native-push-notifications.mdx index 522c620b3..28a9d6d87 100644 --- a/notifications/react-native-push-notifications.mdx +++ b/notifications/react-native-push-notifications.mdx @@ -3,6 +3,36 @@ title: "React Native" description: "Add CometChat push notifications and VoIP calls to a React Native app (Android + iOS) with the drop-in @cometchat/push-notifications-react-native package." --- + + +| Field | Value | +| --- | --- | +| Goal | Add push notifications and VoIP calling (FCM on Android, APNs + PushKit on iOS) to a React Native app | +| Package | `@cometchat/push-notifications-react-native` | +| Peer dependency | `@cometchat/chat-sdk-react-native` — must be initialized **and logged in** before `init()` | +| Import | `import { CometChatPushNotifications, CometChatPNHelper, registerBackgroundCallTask } from "@cometchat/push-notifications-react-native";` | +| Android setup | **No app-side code.** Add `google-services.json` + the `com.google.gms.google-services` plugin. The package manifest declares the FCM service, ringing activity, and call foreground service. | +| iOS setup | `npx cometchat-pn setup` — adds `UIBackgroundModes` (`voip`, `remote-notification`, `audio`) + mic/camera usage strings, generates `ios//CometChatPushNotifications+AppDelegate.swift`. Add that file to the target; enable **Push Notifications** + **Background Modes**. | +| iOS AppDelegate | Forward to `CometChatPushNotificationsAppDelegate`: `.didRegisterAPNsToken(_:)`, `.didUpdateVoIPToken(_:)`, `.didReceiveIncomingVoIPPush(_:)`, `.didReceiveRemoteNotification(_:)`. Create `PKPushRegistry` on a **background** queue (a `.main` queue misses iOS's ~5s report deadline on a killed-app cold start). | +| Initialize | `await CometChatPushNotifications.init(config)` — call **after** `CometChat.init(...)` and login | +| Config (`PNConfiguration`) | `fcmProviderId?`, `apnsProviderId?`, `notificationSmallIcon?`, `androidChannelId?` (default `"cometchat_messages"`), `androidChannelName?` (default `"Messages"`), `voip?` (default `true`), `showInForeground?` (default `false`) | +| Permissions | `CometChatPNHelper.requestNotificationPermission(): Promise` · `.hasNotificationPermission(): Promise` · `.requestCallPermissions(): Promise` (mic + camera, required before a call connects) | +| Token registration | Automatic on `init()`. Manual: `CometChatPushNotifications.registerToken(platform, token)` where `PNTokenPlatform` = `"fcm"` \| `"apns"` \| `"voip"` | +| Events (each returns `Unsubscribe`) | `onNotificationTap(cb)` · `onCallAccepted(cb)` · `onCallEnded(cb)` · `onMessageReceived(cb)` | +| Cold-start replay | `onNotificationTap` and `onCallAccepted` replay a buffered event that fired before JS was ready — subscribe once at startup, not lazily | +| `PNNotificationTapInfo` | `receiverType?` · `sender?` · `senderName?` · `receiver?` · `conversationId?` · `messageId?` · `parentMessageId?` (present for thread replies) | +| `PNCallInfo` | `sessionId` · `callerUid?` · `callerName?` · `callerAvatar?` · `receiverId?` · `receiverType?` · `callType?` · `callAction?` | +| `PNCallEndEvent` | `sessionId` · `reason?` (`"cancelled"` \| `"unanswered"` \| `"rejected"` \| `"busy"` \| `"ended"`) | +| Killed-state decline | `registerBackgroundCallTask(async (action, info) => { ... })` in `index.js`, outside the component tree. `PNBackgroundCallAction` = `"decline"` only. Android-only; iOS declines natively via CallKit. | +| Teardown | Call every `Unsubscribe`, then `await CometChatPushNotifications.unregister()` before logout. Skipping this fires each handler twice after a re-login. `dispose()` detaches native listeners (app-facing subscriptions stay intact). | +| Coexist with existing push | `CometChatPNHelper.isCometChatNotification(data): boolean` — detect CometChat payloads when the app has its own FCM handler | +| Exported types | `PNConfiguration` · `PNCallInfo` · `PNCallEndEvent` · `PNNotificationTapInfo` · `PNTokenPlatform` · `PNBackgroundCallAction` · `PNReceiverType` (`"user"` \| `"group"`) · `PNCallType` (`"audio"` \| `"video"`) · `PNCallEndReason` · `PNTokenEvent` | +| Android permissions | Declared by the package: `POST_NOTIFICATIONS`, `USE_FULL_SCREEN_INTENT`, `FOREGROUND_SERVICE`(+`_PHONE_CALL`), `WAKE_LOCK`, `VIBRATE`, `RECORD_AUDIO`, `CAMERA`, `BLUETOOTH_CONNECT`. **No telecom/calling-account permission** — the lock-screen ring is a full-screen-intent activity, not a `ConnectionService`. | +| CLI | `npx cometchat-pn setup` (configure iOS) · `npx cometchat-pn doctor` (diagnose) | +| Related | [Push Overview](/notifications/push-overview) · [Templates & Sounds](/notifications/templates-and-sounds) · [Badge Count](/notifications/badge-count) | + + + ## What this guide covers - Adding the `@cometchat/push-notifications-react-native` package and initializing it. From 4958805c8ecf8c7ff784afdb8e600e0a76181be3 Mon Sep 17 00:00:00 2001 From: Suraj Chauhan Date: Thu, 3 Sep 2026 15:10:36 +0530 Subject: [PATCH 04/20] docs(rn): promote logout teardown to its own section, rename to unregisterPushOnLogout Android and iOS both document this as a dedicated numbered step titled 'Unregister the token on logout'. The RN guide had it buried inside the token-registration step under an invented name (teardownPush), so align it: own section 5, house-style name, remaining sections renumbered and the troubleshooting cross-reference updated. Keeps the unsubscribe step (which the other platforms don't need) and now says why: the package holds callbacks in a set that survives unregister(), so skipping it double-fires handlers after a re-login. Co-Authored-By: Claude Opus 4.8 --- .../react-native-push-notifications.mdx | 22 +++++++++++-------- 1 file changed, 13 insertions(+), 9 deletions(-) diff --git a/notifications/react-native-push-notifications.mdx b/notifications/react-native-push-notifications.mdx index 28a9d6d87..401e660d0 100644 --- a/notifications/react-native-push-notifications.mdx +++ b/notifications/react-native-push-notifications.mdx @@ -258,21 +258,25 @@ const enabled = await CometChatPNHelper.hasNotificationPermission(); await CometChatPushNotifications.registerToken('fcm', token); // or 'apns' / 'voip' ``` -**On logout**, unsubscribe your handlers and unregister the token so the device stops receiving pushes for that user: +## 5. Unregister the token on logout + +Before logging the user out, unregister the push token so the device stops receiving notifications for that user — and unsubscribe your handlers: ```ts lines -export async function teardownPush() { +export async function unregisterPushOnLogout() { subscriptions.forEach(unsub => unsub()); subscriptions = []; await CometChatPushNotifications.unregister(); } + +// then: await CometChat.logout(); ``` -Not unsubscribing on logout leaves the callbacks registered, so a logout → login cycle would fire each handler twice (e.g. navigating to a tapped message twice). +Unlike the other platforms, React Native needs the unsubscribe step too: the package keeps your callbacks in a set that survives `unregister()`, so a logout → login cycle would register a second copy of each handler and fire it twice (e.g. navigating to a tapped message twice). -## 5. Notification taps and call events +## 6. Notification taps and call events Subscribe once (in `setupPush` above). Each subscribe returns an unsubscribe function. @@ -319,7 +323,7 @@ function handleCallEnded(info) { **Cold-start VoIP handling (iOS):** when the app is killed and a VoIP push arrives, the package presents CallKit natively via PushKit before React Native is ready. When the user answers, the app cold-starts, `init()` replays the accepted call, `onCallAccepted` fires, and the package has already called `CometChat.acceptCall` — set up your call session/screen there. (This path is why the `PKPushRegistry` must be on a background queue — see step 2.) -## 6. Android: OEM permissions for lock-screen calls +## 7. Android: OEM permissions for lock-screen calls The package declares the standard permissions and uses the correct `setShowWhenLocked` / `setTurnScreenOn` flags, so full-screen calls over the lock screen work out of the box on stock Android (including Android 14+). **OEM skins (MIUI/Redmi/POCO, Oppo, Vivo) additionally gate background-launched full-screen activities** behind their own toggles — without them, a locked/killed call shows only a heads-up notification (with ringtone), and the full-screen screen appears only after unlock. @@ -337,7 +341,7 @@ import { Linking, Platform } from 'react-native'; if (Platform.OS === 'android') Linking.openSettings(); ``` -## 7. Badge count +## 8. Badge count CometChat's Enhanced Push payload includes an `unreadMessageCount` field (total unread across conversations). @@ -357,7 +361,7 @@ CometChat's Enhanced Push payload includes an `unreadMessageCount` field (total -## 8. Testing checklist +## 9. 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: @@ -371,7 +375,7 @@ CometChat's Enhanced Push payload includes an `unreadMessageCount` field (total - **Caller cancels** while it's ringing → the callee ring dismisses. 5. On an OEM device (MIUI/Oppo/Vivo), grant the section-6 permissions and re-check locked/killed calls. -## 9. Troubleshooting +## 10. Troubleshooting | Symptom | Platform | Quick checks | | --- | --- | --- | @@ -381,7 +385,7 @@ CometChat's Enhanced Push payload includes an `unreadMessageCount` field (total | Full-screen call UI not showing on lock screen | Android | OEM gate — grant Autostart / "Display pop-up while running in background" / "Show on lock screen" and disable battery optimization (section 6). | | Declining a killed-state call doesn't reject the caller | Both | Android: ensure `registerBackgroundCallTask` is registered at module scope in `index.js`. iOS: the package handles it via CallKit — verify `AppDelegate` forwarding. | | Foreground call shows twice (in-app + CallKit/banner) | Both | Set `showInForeground: false` in `init()` so foreground calls use your in-app UI only. | -| Duplicate navigation after re-login | Both | Unsubscribe every handler and call `unregister()` on logout (step 4). | +| Duplicate navigation after re-login | Both | Unsubscribe every handler and call `unregister()` on logout (step 5). | | No VoIP pushes | iOS | Ensure Push Notifications + Background Modes (Voice over IP) are enabled, `aps-environment` is correct (`production` for release), the bundle ID matches the CometChat APNs VoIP provider, and the VoIP cert is uploaded to the dashboard. | | Token registration errors | Both | Verify the provider IDs match the dashboard exactly and that `init()` runs **after** login. | From b16c9c08d39b0d7969326f9a21be6077a49e9a78 Mon Sep 17 00:00:00 2001 From: Suraj Chauhan Date: Thu, 3 Sep 2026 15:19:11 +0530 Subject: [PATCH 05/20] docs(rn): rename setupPush to setupPushOnLogin for a symmetric pair Pairs with unregisterPushOnLogout, so both names state when to call them. Matches the reference app, which uses the same two names. Co-Authored-By: Claude Opus 4.8 --- notifications/react-native-push-notifications.mdx | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/notifications/react-native-push-notifications.mdx b/notifications/react-native-push-notifications.mdx index 401e660d0..dd9ebd231 100644 --- a/notifications/react-native-push-notifications.mdx +++ b/notifications/react-native-push-notifications.mdx @@ -206,7 +206,7 @@ AppRegistry.registerComponent(appName, () => App); ``` ```ts lines -// after CometChat.init(...) + login succeeds — e.g. a setupPush() you call on login: +// call this once, after CometChat.init(...) and login succeed: import { CometChatPushNotifications, CometChatPNHelper, @@ -214,7 +214,7 @@ import { let subscriptions: Array<() => void> = []; -export async function setupPush() { +export async function setupPushOnLogin() { await CometChatPNHelper.requestNotificationPermission(); await CometChatPNHelper.requestCallPermissions(); // mic + camera (Android) before a call connects @@ -278,7 +278,7 @@ Unlike the other platforms, React Native needs the unsubscribe step too: the pac ## 6. Notification taps and call events -Subscribe once (in `setupPush` above). Each subscribe returns an unsubscribe function. +Subscribe once (in `setupPushOnLogin` above). Each subscribe returns an unsubscribe function. **Notification tap** — open the conversation, or the **thread** when the push is a thread reply: From 077ff008d3ee6a4ecf0fedbf543ab5a3bbcf7860 Mon Sep 17 00:00:00 2001 From: Suraj Chauhan Date: Thu, 3 Sep 2026 15:38:47 +0530 Subject: [PATCH 06/20] docs(rn): align the AI Quick Reference with the renamed functions The reference still said 'Teardown' and named neither function, so it disagreed with the 'Unregister the token on logout' section and the reference app. Name setupPushOnLogin / unregisterPushOnLogout in the Initialize and logout rows so an agent reading only the accordion generates the same names the guide and sample app use. Co-Authored-By: Claude Opus 4.8 --- notifications/react-native-push-notifications.mdx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/notifications/react-native-push-notifications.mdx b/notifications/react-native-push-notifications.mdx index dd9ebd231..1cd28025e 100644 --- a/notifications/react-native-push-notifications.mdx +++ b/notifications/react-native-push-notifications.mdx @@ -14,7 +14,7 @@ description: "Add CometChat push notifications and VoIP calls to a React Native | Android setup | **No app-side code.** Add `google-services.json` + the `com.google.gms.google-services` plugin. The package manifest declares the FCM service, ringing activity, and call foreground service. | | iOS setup | `npx cometchat-pn setup` — adds `UIBackgroundModes` (`voip`, `remote-notification`, `audio`) + mic/camera usage strings, generates `ios//CometChatPushNotifications+AppDelegate.swift`. Add that file to the target; enable **Push Notifications** + **Background Modes**. | | iOS AppDelegate | Forward to `CometChatPushNotificationsAppDelegate`: `.didRegisterAPNsToken(_:)`, `.didUpdateVoIPToken(_:)`, `.didReceiveIncomingVoIPPush(_:)`, `.didReceiveRemoteNotification(_:)`. Create `PKPushRegistry` on a **background** queue (a `.main` queue misses iOS's ~5s report deadline on a killed-app cold start). | -| Initialize | `await CometChatPushNotifications.init(config)` — call **after** `CometChat.init(...)` and login | +| Initialize | In a `setupPushOnLogin()`: request permissions, subscribe handlers, then `await CometChatPushNotifications.init(config)` — call **after** `CometChat.init(...)` and login | | Config (`PNConfiguration`) | `fcmProviderId?`, `apnsProviderId?`, `notificationSmallIcon?`, `androidChannelId?` (default `"cometchat_messages"`), `androidChannelName?` (default `"Messages"`), `voip?` (default `true`), `showInForeground?` (default `false`) | | Permissions | `CometChatPNHelper.requestNotificationPermission(): Promise` · `.hasNotificationPermission(): Promise` · `.requestCallPermissions(): Promise` (mic + camera, required before a call connects) | | Token registration | Automatic on `init()`. Manual: `CometChatPushNotifications.registerToken(platform, token)` where `PNTokenPlatform` = `"fcm"` \| `"apns"` \| `"voip"` | @@ -24,7 +24,7 @@ description: "Add CometChat push notifications and VoIP calls to a React Native | `PNCallInfo` | `sessionId` · `callerUid?` · `callerName?` · `callerAvatar?` · `receiverId?` · `receiverType?` · `callType?` · `callAction?` | | `PNCallEndEvent` | `sessionId` · `reason?` (`"cancelled"` \| `"unanswered"` \| `"rejected"` \| `"busy"` \| `"ended"`) | | Killed-state decline | `registerBackgroundCallTask(async (action, info) => { ... })` in `index.js`, outside the component tree. `PNBackgroundCallAction` = `"decline"` only. Android-only; iOS declines natively via CallKit. | -| Teardown | Call every `Unsubscribe`, then `await CometChatPushNotifications.unregister()` before logout. Skipping this fires each handler twice after a re-login. `dispose()` detaches native listeners (app-facing subscriptions stay intact). | +| Unregister on logout | In a `unregisterPushOnLogout()`: call every `Unsubscribe`, then `await CometChatPushNotifications.unregister()`, then `CometChat.logout()`. Skipping the unsubscribe fires each handler twice after a re-login. `dispose()` detaches native listeners (app-facing subscriptions stay intact). | | Coexist with existing push | `CometChatPNHelper.isCometChatNotification(data): boolean` — detect CometChat payloads when the app has its own FCM handler | | Exported types | `PNConfiguration` · `PNCallInfo` · `PNCallEndEvent` · `PNNotificationTapInfo` · `PNTokenPlatform` · `PNBackgroundCallAction` · `PNReceiverType` (`"user"` \| `"group"`) · `PNCallType` (`"audio"` \| `"video"`) · `PNCallEndReason` · `PNTokenEvent` | | Android permissions | Declared by the package: `POST_NOTIFICATIONS`, `USE_FULL_SCREEN_INTENT`, `FOREGROUND_SERVICE`(+`_PHONE_CALL`), `WAKE_LOCK`, `VIBRATE`, `RECORD_AUDIO`, `CAMERA`, `BLUETOOTH_CONNECT`. **No telecom/calling-account permission** — the lock-screen ring is a full-screen-intent activity, not a `ConnectionService`. | From bacb8930ae6f4c776e9f7956f92fc58e44cd5e78 Mon Sep 17 00:00:00 2001 From: Suraj Chauhan Date: Tue, 15 Sep 2026 17:53:56 +0530 Subject: [PATCH 07/20] docs(rn): foreground chat notifications, cold-start tap navigation, fixes from QA Bring the React Native push guide in line with what was verified on device during ENG-38228 QA. - init sample: showInForeground: true (one chat notification while the app is open) and notificationSmallIcon (otherwise Android shows a solid square). - Correct what showInForeground does: it gates chat notifications only. The troubleshooting row claimed it prevents foreground calls showing twice; the package suppresses CallKit / the system ring in the foreground on its own. - Notification taps: when a tap launches a killed app it arrives before NavigationContainer mounts, so the example now waits for navigationRef isReady() / the 'ready' event before navigating. - Troubleshooting: duplicate chat notifications (a leftover RNFirebase receiver), no foreground notification, cold-start tap not opening the chat. - Fix two references to the OEM section that still said 6 after renumbering. Co-Authored-By: Claude Opus 5 --- .../react-native-push-notifications.mdx | 45 ++++++++++++++----- 1 file changed, 34 insertions(+), 11 deletions(-) diff --git a/notifications/react-native-push-notifications.mdx b/notifications/react-native-push-notifications.mdx index 1cd28025e..abe4df580 100644 --- a/notifications/react-native-push-notifications.mdx +++ b/notifications/react-native-push-notifications.mdx @@ -15,11 +15,11 @@ description: "Add CometChat push notifications and VoIP calls to a React Native | iOS setup | `npx cometchat-pn setup` — adds `UIBackgroundModes` (`voip`, `remote-notification`, `audio`) + mic/camera usage strings, generates `ios//CometChatPushNotifications+AppDelegate.swift`. Add that file to the target; enable **Push Notifications** + **Background Modes**. | | iOS AppDelegate | Forward to `CometChatPushNotificationsAppDelegate`: `.didRegisterAPNsToken(_:)`, `.didUpdateVoIPToken(_:)`, `.didReceiveIncomingVoIPPush(_:)`, `.didReceiveRemoteNotification(_:)`. Create `PKPushRegistry` on a **background** queue (a `.main` queue misses iOS's ~5s report deadline on a killed-app cold start). | | Initialize | In a `setupPushOnLogin()`: request permissions, subscribe handlers, then `await CometChatPushNotifications.init(config)` — call **after** `CometChat.init(...)` and login | -| Config (`PNConfiguration`) | `fcmProviderId?`, `apnsProviderId?`, `notificationSmallIcon?`, `androidChannelId?` (default `"cometchat_messages"`), `androidChannelName?` (default `"Messages"`), `voip?` (default `true`), `showInForeground?` (default `false`) | +| Config (`PNConfiguration`) | `fcmProviderId?`, `apnsProviderId?`, `notificationSmallIcon?`, `androidChannelId?` (default `"cometchat_messages"`), `androidChannelName?` (default `"Messages"`), `voip?` (default `true`), `showInForeground?` (default `false`; chat notifications only — foreground calls always use the in-app UI) | | Permissions | `CometChatPNHelper.requestNotificationPermission(): Promise` · `.hasNotificationPermission(): Promise` · `.requestCallPermissions(): Promise` (mic + camera, required before a call connects) | | Token registration | Automatic on `init()`. Manual: `CometChatPushNotifications.registerToken(platform, token)` where `PNTokenPlatform` = `"fcm"` \| `"apns"` \| `"voip"` | | Events (each returns `Unsubscribe`) | `onNotificationTap(cb)` · `onCallAccepted(cb)` · `onCallEnded(cb)` · `onMessageReceived(cb)` | -| Cold-start replay | `onNotificationTap` and `onCallAccepted` replay a buffered event that fired before JS was ready — subscribe once at startup, not lazily | +| Cold-start replay | `onNotificationTap` and `onCallAccepted` replay a buffered event that fired before JS was ready — subscribe once at startup, not lazily. The tap can arrive before `NavigationContainer` mounts: wait for `navigationRef.isReady()` / the ref's `'ready'` event before navigating | | `PNNotificationTapInfo` | `receiverType?` · `sender?` · `senderName?` · `receiver?` · `conversationId?` · `messageId?` · `parentMessageId?` (present for thread replies) | | `PNCallInfo` | `sessionId` · `callerUid?` · `callerName?` · `callerAvatar?` · `receiverId?` · `receiverType?` · `callType?` · `callAction?` | | `PNCallEndEvent` | `sessionId` · `reason?` (`"cancelled"` \| `"unanswered"` \| `"rejected"` \| `"busy"` \| `"ended"`) | @@ -230,16 +230,20 @@ export async function setupPushOnLogin() { await CometChatPushNotifications.init({ fcmProviderId: AppCredentials.fcmProviderId, // Android apnsProviderId: AppCredentials.apnsProviderId, // iOS (APNs device + VoIP) - // Foreground pushes are rendered in-app (chat UI / WebSocket call overlay), - // so suppress the duplicate system banner/CallKit while the app is active: - showInForeground: false, + // Android status-bar icon (a monochrome drawable); without it the launcher icon + // is used, which Android renders as a solid square. + notificationSmallIcon: 'ic_notification', + // Show one chat notification while the app is open too. Set false if your + // in-app UI already surfaces new messages. Calls are unaffected: a foreground + // call never shows CallKit / the system ring, only your in-app call UI. + showInForeground: true, }); } ``` `init` wires the native events to the Chat SDK, **auto-registers** the device tokens, and drains any cold-start tap/call the app was launched from. It is safe to call again on re-login. -`init` also accepts: `voip` (default `true`), `notificationSmallIcon`, `androidChannelId`, and `androidChannelName`. +`init` also accepts: `voip` (default `true`), `androidChannelId`, and `androidChannelName`. `showInForeground` defaults to `false` and affects **chat notifications only**. ## 4. Request permission and register tokens @@ -280,11 +284,28 @@ Unlike the other platforms, React Native needs the unsubscribe step too: the pac Subscribe once (in `setupPushOnLogin` above). Each subscribe returns an unsubscribe function. -**Notification tap** — open the conversation, or the **thread** when the push is a thread reply: +**Notification tap** — open the conversation, or the **thread** when the push is a thread reply. + +When a tap **launches a killed app**, it is delivered as soon as `init()` subscribes — which can be before your `NavigationContainer` has mounted. Wait for the navigator first, or the navigation is silently dropped and the app stays on its first screen: ```ts lines +import { navigationRef } from './NavigationService'; // createNavigationContainerRef() + +// React Navigation's documented readiness check. The ref queues listeners added +// before the container mounts, so this also works on a cold start. +function whenNavigationReady() { + if (navigationRef.isReady()) return Promise.resolve(); + return new Promise(resolve => { + const unsubscribe = navigationRef.addListener('ready', () => { + unsubscribe(); + resolve(); + }); + }); +} + async function handleTap(info) { // info: { receiverType, sender, receiver, conversationId, messageId, parentMessageId, senderName } + await whenNavigationReady(); if (info.parentMessageId) { const parent = await CometChat.getMessageDetails(info.parentMessageId); navigate('ThreadView', { message: parent, highlightMessageId: info.messageId }); @@ -365,7 +386,7 @@ CometChat's Enhanced Push payload includes an `unreadMessageCount` field (total 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 system banner (with `showInForeground: false`); your in-app UI shows it. + - Foreground: exactly **one** system notification (with `showInForeground: true`), or none with `false`. - Background: a notification appears; tapping opens the right conversation via `onNotificationTap` (and the thread, for a thread reply). 3. Force-quit the app, send another message, tap the notification, and confirm it cold-starts to the conversation. 4. Trigger an incoming CometChat call and confirm: @@ -373,7 +394,7 @@ CometChat's Enhanced Push payload includes an `unreadMessageCount` field (total - **Accept** joins the call (audio works both ways) and the screen tears down when the call ends. - **Decline** rejects the call promptly on the caller side — including from a killed state. - **Caller cancels** while it's ringing → the callee ring dismisses. -5. On an OEM device (MIUI/Oppo/Vivo), grant the section-6 permissions and re-check locked/killed calls. +5. On an OEM device (MIUI/Oppo/Vivo), grant the section-7 permissions and re-check locked/killed calls. ## 10. Troubleshooting @@ -382,9 +403,11 @@ CometChat's Enhanced Push payload includes an `unreadMessageCount` field (total | No notifications received | Android | Confirm `google-services.json` is in `android/app/`, the package name matches Firebase, `firebase-messaging` + the `google-services` plugin are applied, and `POST_NOTIFICATIONS` is granted (Android 13+). | | Killed app doesn't ring for a VoIP push | iOS | Ensure the `PKPushRegistry` is created on a **background queue** and the `AppDelegate` forwards `didReceiveIncomingVoIPPush` to `CometChatPushNotificationsAppDelegate`. Run `npx cometchat-pn doctor`. | | Accepted call connects but has no audio | iOS | Confirm the `audio` background mode is present (the setup CLI adds it) and `react-native-webrtc` (via the Calls SDK) is linked in the app — the package coordinates CallKit's audio session with WebRTC automatically. | -| Full-screen call UI not showing on lock screen | Android | OEM gate — grant Autostart / "Display pop-up while running in background" / "Show on lock screen" and disable battery optimization (section 6). | +| Full-screen call UI not showing on lock screen | Android | OEM gate — grant Autostart / "Display pop-up while running in background" / "Show on lock screen" and disable battery optimization (section 7). | | Declining a killed-state call doesn't reject the caller | Both | Android: ensure `registerBackgroundCallTask` is registered at module scope in `index.js`. iOS: the package handles it via CallKit — verify `AppDelegate` forwarding. | -| Foreground call shows twice (in-app + CallKit/banner) | Both | Set `showInForeground: false` in `init()` so foreground calls use your in-app UI only. | +| Two notifications for one chat message | Android | Another FCM receiver is still installed — usually `@react-native-firebase/messaging`, whose broadcast receiver handles every push on its own. Remove the legacy push libraries so the package's service is the only handler. | +| No notification while the app is open | Both | Expected with `showInForeground: false` (the default). Set it to `true` to show one. It affects chat notifications only; a foreground call always uses your in-app UI. | +| Tapping a notification when the app was killed opens the app but not the conversation | Both | Wait for the navigator before navigating (`whenNavigationReady()`, step 6) — the tap is delivered before `NavigationContainer` mounts. | | Duplicate navigation after re-login | Both | Unsubscribe every handler and call `unregister()` on logout (step 5). | | No VoIP pushes | iOS | Ensure Push Notifications + Background Modes (Voice over IP) are enabled, `aps-environment` is correct (`production` for release), the bundle ID matches the CometChat APNs VoIP provider, and the VoIP cert is uploaded to the dashboard. | | Token registration errors | Both | Verify the provider IDs match the dashboard exactly and that `init()` runs **after** login. | From 629c1cabf772451d17a0add453924c4fc0d72409 Mon Sep 17 00:00:00 2001 From: Suraj Chauhan Date: Wed, 16 Sep 2026 14:16:37 +0530 Subject: [PATCH 08/20] docs(rn): logout via the setup cleanup, and the permission contract MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Match what shipped and what QA found on a fresh install. - setupPushOnLogin() now returns its cleanup, wired to a useEffect keyed on the logged-in state, replacing the module-level `subscriptions` array and unregisterPushOnLogout(). Handlers subscribe before init() so a tap that launched the app is delivered. - Document that every permission request must be awaited before the next one: Android allows one pending request per activity, and a second request cancels the open dialog and is reported as denied without the user seeing it. On a fresh install that silently left the app with no notification permission. - Document that requestNotificationPermission() rejects (ERR_NO_ACTIVITY, ERR_PERMISSION_IN_FLIGHT, ERR_ACTIVITY_NOT_PERMISSION_AWARE) when it could not ask at all — distinct from the user declining, and not a reason to skip init(). - Spell out that unregister() must precede logout() because it needs the session's auth token, and that swallowing its error leaves the device on push. - Two troubleshooting rows: fresh-install-only failures, and push after logout. Co-Authored-By: Claude Opus 5 --- .../react-native-push-notifications.mdx | 108 +++++++++++++----- 1 file changed, 79 insertions(+), 29 deletions(-) diff --git a/notifications/react-native-push-notifications.mdx b/notifications/react-native-push-notifications.mdx index abe4df580..e25eb0925 100644 --- a/notifications/react-native-push-notifications.mdx +++ b/notifications/react-native-push-notifications.mdx @@ -14,9 +14,9 @@ description: "Add CometChat push notifications and VoIP calls to a React Native | Android setup | **No app-side code.** Add `google-services.json` + the `com.google.gms.google-services` plugin. The package manifest declares the FCM service, ringing activity, and call foreground service. | | iOS setup | `npx cometchat-pn setup` — adds `UIBackgroundModes` (`voip`, `remote-notification`, `audio`) + mic/camera usage strings, generates `ios//CometChatPushNotifications+AppDelegate.swift`. Add that file to the target; enable **Push Notifications** + **Background Modes**. | | iOS AppDelegate | Forward to `CometChatPushNotificationsAppDelegate`: `.didRegisterAPNsToken(_:)`, `.didUpdateVoIPToken(_:)`, `.didReceiveIncomingVoIPPush(_:)`, `.didReceiveRemoteNotification(_:)`. Create `PKPushRegistry` on a **background** queue (a `.main` queue misses iOS's ~5s report deadline on a killed-app cold start). | -| Initialize | In a `setupPushOnLogin()`: request permissions, subscribe handlers, then `await CometChatPushNotifications.init(config)` — call **after** `CometChat.init(...)` and login | +| Initialize | In a `setupPushOnLogin()`: subscribe handlers, then request permissions and `await CometChatPushNotifications.init(config)` — call **after** `CometChat.init(...)` and login. Returns a cleanup for a `useEffect`. | | Config (`PNConfiguration`) | `fcmProviderId?`, `apnsProviderId?`, `notificationSmallIcon?`, `androidChannelId?` (default `"cometchat_messages"`), `androidChannelName?` (default `"Messages"`), `voip?` (default `true`), `showInForeground?` (default `false`; chat notifications only — foreground calls always use the in-app UI) | -| Permissions | `CometChatPNHelper.requestNotificationPermission(): Promise` · `.hasNotificationPermission(): Promise` · `.requestCallPermissions(): Promise` (mic + camera, required before a call connects) | +| Permissions | `CometChatPNHelper.requestNotificationPermission(): Promise` (awaits the user's answer; **rejects** with `ERR_NO_ACTIVITY` / `ERR_PERMISSION_IN_FLIGHT` / `ERR_ACTIVITY_NOT_PERMISSION_AWARE` when it could not ask) · `.hasNotificationPermission(): Promise` · `.requestCallPermissions(): Promise` (mic + camera, required before a call connects). Await one before starting the next — Android allows one pending request per activity. | | Token registration | Automatic on `init()`. Manual: `CometChatPushNotifications.registerToken(platform, token)` where `PNTokenPlatform` = `"fcm"` \| `"apns"` \| `"voip"` | | Events (each returns `Unsubscribe`) | `onNotificationTap(cb)` · `onCallAccepted(cb)` · `onCallEnded(cb)` · `onMessageReceived(cb)` | | Cold-start replay | `onNotificationTap` and `onCallAccepted` replay a buffered event that fired before JS was ready — subscribe once at startup, not lazily. The tap can arrive before `NavigationContainer` mounts: wait for `navigationRef.isReady()` / the ref's `'ready'` event before navigating | @@ -24,7 +24,7 @@ description: "Add CometChat push notifications and VoIP calls to a React Native | `PNCallInfo` | `sessionId` · `callerUid?` · `callerName?` · `callerAvatar?` · `receiverId?` · `receiverType?` · `callType?` · `callAction?` | | `PNCallEndEvent` | `sessionId` · `reason?` (`"cancelled"` \| `"unanswered"` \| `"rejected"` \| `"busy"` \| `"ended"`) | | Killed-state decline | `registerBackgroundCallTask(async (action, info) => { ... })` in `index.js`, outside the component tree. `PNBackgroundCallAction` = `"decline"` only. Android-only; iOS declines natively via CallKit. | -| Unregister on logout | In a `unregisterPushOnLogout()`: call every `Unsubscribe`, then `await CometChatPushNotifications.unregister()`, then `CometChat.logout()`. Skipping the unsubscribe fires each handler twice after a re-login. `dispose()` detaches native listeners (app-facing subscriptions stay intact). | +| Unregister on logout | `await CometChatPushNotifications.unregister()` **before** `CometChat.logout()` (it needs the session's auth token). Handlers are removed by the cleanup `setupPushOnLogin()` returns — skipping it fires each handler twice after a re-login. `dispose()` detaches native listeners (app-facing subscriptions stay intact). | | Coexist with existing push | `CometChatPNHelper.isCometChatNotification(data): boolean` — detect CometChat payloads when the app has its own FCM handler | | Exported types | `PNConfiguration` · `PNCallInfo` · `PNCallEndEvent` · `PNNotificationTapInfo` · `PNTokenPlatform` · `PNBackgroundCallAction` · `PNReceiverType` (`"user"` \| `"group"`) · `PNCallType` (`"audio"` \| `"video"`) · `PNCallEndReason` · `PNTokenEvent` | | Android permissions | Declared by the package: `POST_NOTIFICATIONS`, `USE_FULL_SCREEN_INTENT`, `FOREGROUND_SERVICE`(+`_PHONE_CALL`), `WAKE_LOCK`, `VIBRATE`, `RECORD_AUDIO`, `CAMERA`, `BLUETOOTH_CONNECT`. **No telecom/calling-account permission** — the lock-screen ring is a full-screen-intent activity, not a `ConnectionService`. | @@ -212,32 +212,42 @@ import { CometChatPNHelper, } from '@cometchat/push-notifications-react-native'; -let subscriptions: Array<() => void> = []; - -export async function setupPushOnLogin() { - await CometChatPNHelper.requestNotificationPermission(); - await CometChatPNHelper.requestCallPermissions(); // mic + camera (Android) before a call connects - - subscriptions.push( +// Returns a cleanup that removes the handlers — wire it to a `useEffect` (step 5). +export function setupPushOnLogin(): () => void { + // Subscribe BEFORE init() so the tap or answered call that LAUNCHED the app + // is delivered to you rather than dropped. + const unsubscribes = [ CometChatPushNotifications.onNotificationTap(handleTap), CometChatPushNotifications.onCallAccepted(info => navigate('OngoingCall', { sessionId: info.sessionId, callType: info.callType })), CometChatPushNotifications.onCallEnded(handleCallEnded), CometChatPushNotifications.onMessageReceived(data => console.log('data push:', data)), - ); - - await CometChatPushNotifications.init({ - fcmProviderId: AppCredentials.fcmProviderId, // Android - apnsProviderId: AppCredentials.apnsProviderId, // iOS (APNs device + VoIP) - // Android status-bar icon (a monochrome drawable); without it the launcher icon - // is used, which Android renders as a solid square. - notificationSmallIcon: 'ic_notification', - // Show one chat notification while the app is open too. Set false if your - // in-app UI already surfaces new messages. Calls are unaffected: a foreground - // call never shows CallKit / the system ring, only your in-app call UI. - showInForeground: true, - }); + ]; + + const start = async () => { + // Await each permission request before the next: Android allows only ONE pending + // request per activity, and a second one cancels the dialog still on screen. + // This rejects when the OS could not be asked at all — which is not the user + // declining, and not a reason to skip init(): the token still has to register. + await CometChatPNHelper.requestNotificationPermission().catch(() => false); + await CometChatPNHelper.requestCallPermissions(); // mic + camera (Android) before a call connects + + await CometChatPushNotifications.init({ + fcmProviderId: AppCredentials.fcmProviderId, // Android + apnsProviderId: AppCredentials.apnsProviderId, // iOS (APNs device + VoIP) + // Android status-bar icon (a monochrome drawable); without it the launcher icon + // is used, which Android renders as a solid square. + notificationSmallIcon: 'ic_notification', + // Show one chat notification while the app is open too. Set false if your + // in-app UI already surfaces new messages. Calls are unaffected: a foreground + // call never shows CallKit / the system ring, only your in-app call UI. + showInForeground: true, + }); + }; + start().catch(e => console.log('push setup failed', e)); + + return () => unsubscribes.forEach(unsub => unsub()); } ``` @@ -256,6 +266,23 @@ await CometChatPNHelper.requestCallPermissions(); // mic + camera (Androi const enabled = await CometChatPNHelper.hasNotificationPermission(); ``` + +**Always `await` one permission request before starting the next.** Android allows only +one pending request per activity: a second request cancels the dialog still on screen and +the OS reports it as denied without the user ever seeing it. On a fresh install that leaves +the app with no notification permission — pushes arrive and are silently dropped. + + +`requestNotificationPermission()` resolves `true`/`false` on the user's answer. It **rejects** +when the permission could not be requested at all, which is a different situation from the +user declining and should not stop the rest of your setup — the token still registers: + +| Rejection | Meaning | +| --- | --- | +| `ERR_NO_ACTIVITY` | No foreground activity, so no dialog can be shown. Retry when the app is foreground. | +| `ERR_PERMISSION_IN_FLIGHT` | A request is already open. Await that one instead. | +| `ERR_ACTIVITY_NOT_PERMISSION_AWARE` | Your host activity does not extend `ReactActivity`. Fix the activity. | + **Token registration is automatic** — `init()` registers the FCM token (Android) and the APNs device + VoIP tokens (iOS) with CometChat, and re-registers on refresh. You rarely need to do it by hand, but you can: ```ts lines @@ -264,18 +291,39 @@ await CometChatPushNotifications.registerToken('fcm', token); // or 'apns' / ' ## 5. Unregister the token on logout -Before logging the user out, unregister the push token so the device stops receiving notifications for that user — and unsubscribe your handlers: +Two separate things have to happen on logout: the **token** must be unregistered so the device +stops receiving that user's notifications, and your **handlers** must be unsubscribed. + +Let React own the handlers. `setupPushOnLogin()` returns its cleanup, so returning it from a +`useEffect` keyed on the logged-in state removes the handlers on logout and on unmount: + +```tsx lines +// App.tsx +useEffect(() => { + if (!isLoggedIn) return; + return setupPushOnLogin(); // the cleanup runs on logout / unmount +}, [isLoggedIn]); +``` + +Unregister the token in your logout handler, **before** `logout()`: ```ts lines -export async function unregisterPushOnLogout() { - subscriptions.forEach(unsub => unsub()); - subscriptions = []; +// in your logout button / handler +try { await CometChatPushNotifications.unregister(); +} catch (error) { + console.error('Failed to unregister push token:', error); + return; // logging out anyway would leave the device receiving this user's push } - -// then: await CometChat.logout(); +await CometChat.logout(); ``` + +`unregister()` must run **before** `logout()`. It calls the Chat SDK's `unregisterPushToken()`, +which needs the session's auth token — after logout it fails, and the device keeps receiving +notifications for the user who just logged out. + + Unlike the other platforms, React Native needs the unsubscribe step too: the package keeps your callbacks in a set that survives `unregister()`, so a logout → login cycle would register a second copy of each handler and fire it twice (e.g. navigating to a tapped message twice). @@ -406,6 +454,8 @@ CometChat's Enhanced Push payload includes an `unreadMessageCount` field (total | Full-screen call UI not showing on lock screen | Android | OEM gate — grant Autostart / "Display pop-up while running in background" / "Show on lock screen" and disable battery optimization (section 7). | | Declining a killed-state call doesn't reject the caller | Both | Android: ensure `registerBackgroundCallTask` is registered at module scope in `index.js`. iOS: the package handles it via CallKit — verify `AppDelegate` forwarding. | | Two notifications for one chat message | Android | Another FCM receiver is still installed — usually `@react-native-firebase/messaging`, whose broadcast receiver handles every push on its own. Remove the legacy push libraries so the package's service is the only handler. | +| Notifications work on an existing install but not on a fresh one | Android | The permission dialog was cancelled before the user could answer it. `await` `requestNotificationPermission()` before requesting any other permission — Android allows one pending request per activity (step 4). Check with `adb shell dumpsys package \| grep POST_NOTIFICATIONS`: `granted=false` with no `USER_SET` flag means the dialog was never answered. | +| Notifications still arrive after logout | Both | `unregister()` must run **before** `logout()` — it needs the session's auth token (step 5). Check that its rejection is not being swallowed. | | No notification while the app is open | Both | Expected with `showInForeground: false` (the default). Set it to `true` to show one. It affects chat notifications only; a foreground call always uses your in-app UI. | | Tapping a notification when the app was killed opens the app but not the conversation | Both | Wait for the navigator before navigating (`whenNavigationReady()`, step 6) — the tap is delivered before `NavigationContainer` mounts. | | Duplicate navigation after re-login | Both | Unsubscribe every handler and call `unregister()` on logout (step 5). | From baab1079203cc73ec762fff2ce0e9f987debf145 Mon Sep 17 00:00:00 2001 From: Suraj Chauhan Date: Wed, 16 Sep 2026 17:06:25 +0530 Subject: [PATCH 09/20] docs(rn): align with the package's real setup, and with the Flutter guide MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Checked every claim against the package source; two were wrong. iOS — the guide told apps to hand-write a PKPushRegistry and the four delegate methods in AppDelegate, AFTER adding the setup CLI's generated file to the target. That file already owns the registry (on a background queue) and forwards all four events. Doing both is a compile error (didRegisterForRemoteNotifications… defined twice) or two registries, which double-delivers every VoIP push. And the one line an app actually needs, `CometChatVoIP.shared.register()`, was never mentioned. Replaced with that line, a warning against a second registry and on clashing with existing delegate methods, and a note for Objective-C AppDelegates. Android — the guide told apps to add firebase-bom + firebase-messaging. The package already depends on both; only the google-services plugin is needed. Updated the quick reference, the section 6 cold-start warning and four troubleshooting rows that repeated those instructions or the old logout helper. Structure, against Flutter's unified guide (cometchat/docs#440): - Badge: add the dashboard step that enables unreadMessageCount, and order tabs Android → iOS like every other tab group. - Testing: add a fresh-install check (Flutter has a reinstall item) and a logout check. Sections 3 and 4 keep a single code path rather than Flutter's per-platform tabs: Flutter's init and token registration genuinely differ per platform, while this package's are one identical JS call on both. Co-Authored-By: Claude Opus 5 --- .../react-native-push-notifications.mdx | 76 +++++++++---------- 1 file changed, 34 insertions(+), 42 deletions(-) diff --git a/notifications/react-native-push-notifications.mdx b/notifications/react-native-push-notifications.mdx index e25eb0925..2eb113dbb 100644 --- a/notifications/react-native-push-notifications.mdx +++ b/notifications/react-native-push-notifications.mdx @@ -11,9 +11,9 @@ description: "Add CometChat push notifications and VoIP calls to a React Native | Package | `@cometchat/push-notifications-react-native` | | Peer dependency | `@cometchat/chat-sdk-react-native` — must be initialized **and logged in** before `init()` | | Import | `import { CometChatPushNotifications, CometChatPNHelper, registerBackgroundCallTask } from "@cometchat/push-notifications-react-native";` | -| Android setup | **No app-side code.** Add `google-services.json` + the `com.google.gms.google-services` plugin. The package manifest declares the FCM service, ringing activity, and call foreground service. | +| Android setup | **No app-side code.** Add `google-services.json` + the `com.google.gms.google-services` plugin. The package already depends on `firebase-messaging` — do not add it. The package manifest declares the FCM service, ringing activity, and call foreground service. | | iOS setup | `npx cometchat-pn setup` — adds `UIBackgroundModes` (`voip`, `remote-notification`, `audio`) + mic/camera usage strings, generates `ios//CometChatPushNotifications+AppDelegate.swift`. Add that file to the target; enable **Push Notifications** + **Background Modes**. | -| iOS AppDelegate | Forward to `CometChatPushNotificationsAppDelegate`: `.didRegisterAPNsToken(_:)`, `.didUpdateVoIPToken(_:)`, `.didReceiveIncomingVoIPPush(_:)`, `.didReceiveRemoteNotification(_:)`. Create `PKPushRegistry` on a **background** queue (a `.main` queue misses iOS's ~5s report deadline on a killed-app cold start). | +| iOS AppDelegate | Add **one line** to `didFinishLaunchingWithOptions`: `CometChatVoIP.shared.register()`. The generated file owns the single `PKPushRegistry` (background queue) and forwards to `CometChatPushNotificationsAppDelegate`: `.didRegisterAPNsToken(_:)`, `.didUpdateVoIPToken(_:)`, `.didReceiveIncomingVoIPPush(_:)`, `.didReceiveRemoteNotification(_:)`. Never create a second `PKPushRegistry` (double-delivers every VoIP push). Swift `AppDelegate` (RN 0.77+); with Objective-C, call those methods yourself. | | Initialize | In a `setupPushOnLogin()`: subscribe handlers, then request permissions and `await CometChatPushNotifications.init(config)` — call **after** `CometChat.init(...)` and login. Returns a cleanup for a `useEffect`. | | Config (`PNConfiguration`) | `fcmProviderId?`, `apnsProviderId?`, `notificationSmallIcon?`, `androidChannelId?` (default `"cometchat_messages"`), `androidChannelName?` (default `"Messages"`), `voip?` (default `true`), `showInForeground?` (default `false`; chat notifications only — foreground calls always use the in-app UI) | | Permissions | `CometChatPNHelper.requestNotificationPermission(): Promise` (awaits the user's answer; **rejects** with `ERR_NO_ACTIVITY` / `ERR_PERMISSION_IN_FLIGHT` / `ERR_ACTIVITY_NOT_PERMISSION_AWARE` when it could not ask) · `.hasNotificationPermission(): Promise` · `.requestCallPermissions(): Promise` (mic + camera, required before a call connects). Await one before starting the next — Android allows one pending request per activity. | @@ -96,7 +96,7 @@ npm install @cometchat/push-notifications-react-native With `google-services.json` already in `android/app/` (from [Getting Started](/notifications/push-overview)): - 1. Apply the Google Services plugin and Firebase Messaging in your Gradle files: + 1. Apply the Google Services plugin — it reads `google-services.json` at build time. The package already depends on `firebase-messaging`, so don't add it yourself: ```groovy lines // android/build.gradle @@ -110,11 +110,6 @@ npm install @cometchat/push-notifications-react-native ```groovy lines // android/app/build.gradle apply plugin: "com.google.gms.google-services" - - dependencies { - implementation platform("com.google.firebase:firebase-bom:33.16.0") - implementation "com.google.firebase:firebase-messaging" - } ``` 2. Keep `minSdkVersion 24` or higher. @@ -142,35 +137,27 @@ npm install @cometchat/push-notifications-react-native 3. In Xcode, **add the generated `CometChatPushNotifications+AppDelegate.swift` to your app target**, and enable the **Push Notifications** and **Background Modes** capabilities (the latter with *Voice over IP* + *Remote notifications* + *Audio*). - 4. Forward the PushKit/APNs events to the package from your `AppDelegate`. Create the `PKPushRegistry` on a **background queue** — on a killed-app cold start iOS delivers the incoming push on that queue, and a `.main` queue would sit behind React Native's startup and miss iOS's ~5s "report a call" deadline (iOS then terminates the app with no CallKit UI): + 4. The generated file handles PushKit and the APNs forwarding for you — it owns the app's `PKPushRegistry` (on a background queue) and forwards the APNs device token, the VoIP token, and incoming VoIP pushes to the package. Start it with **one line** in your `AppDelegate`: ```swift lines - import PushKit - - // in application(_:didFinishLaunchingWithOptions:) - let registry = PKPushRegistry(queue: DispatchQueue(label: "com.cometchat.voip.pushkit")) - registry.delegate = self - registry.desiredPushTypes = [.voIP] - - // APNs device token (chat/alert pushes) - override func application(_ application: UIApplication, - didRegisterForRemoteNotificationsWithDeviceToken deviceToken: Data) { - CometChatPushNotificationsAppDelegate.didRegisterAPNsToken(deviceToken) - } - - // VoIP token + incoming VoIP push (PKPushRegistryDelegate) - func pushRegistry(_ registry: PKPushRegistry, - didUpdate credentials: PKPushCredentials, for type: PKPushType) { - CometChatPushNotificationsAppDelegate.didUpdateVoIPToken(credentials.token) - } - func pushRegistry(_ registry: PKPushRegistry, - didReceiveIncomingPushWith payload: PKPushPayload, for type: PKPushType, - completion: @escaping () -> Void) { - CometChatPushNotificationsAppDelegate.didReceiveIncomingVoIPPush(payload.dictionaryPayload) - completion() + override func application( + _ application: UIApplication, + didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? = nil + ) -> Bool { + // ... your existing React Native setup ... + CometChatVoIP.shared.register() // before `return true` + return true } ``` + + **Do not create a second `PKPushRegistry`** anywhere in your app — every VoIP push would be delivered twice. If your `AppDelegate` already implements `didRegisterForRemoteNotificationsWithDeviceToken` or `didReceiveRemoteNotification`, the generated extension will clash with it: move the single `CometChatPushNotificationsAppDelegate` call from the generated file into your existing method and delete that method from the generated file. + + + + The generated file is Swift and extends `AppDelegate`, which matches the React Native 0.77+ template. With an Objective-C `AppDelegate.mm`, call the same `CometChatPushNotificationsAppDelegate` methods from your Objective-C delegate methods instead. + + 5. Verify the wiring at any time: ```bash @@ -389,7 +376,7 @@ function handleCallEnded(info) { ``` -**Cold-start VoIP handling (iOS):** when the app is killed and a VoIP push arrives, the package presents CallKit natively via PushKit before React Native is ready. When the user answers, the app cold-starts, `init()` replays the accepted call, `onCallAccepted` fires, and the package has already called `CometChat.acceptCall` — set up your call session/screen there. (This path is why the `PKPushRegistry` must be on a background queue — see step 2.) +**Cold-start VoIP handling (iOS):** when the app is killed and a VoIP push arrives, the package presents CallKit natively via PushKit before React Native is ready. When the user answers, the app cold-starts, `init()` replays the accepted call, `onCallAccepted` fires, and the package has already called `CometChat.acceptCall` — set up your call session/screen there. (This path is why the generated `CometChatVoIP` keeps its `PKPushRegistry` on a background queue — call `CometChatVoIP.shared.register()` as shown in step 2.) ## 7. Android: OEM permissions for lock-screen calls @@ -412,12 +399,12 @@ if (Platform.OS === 'android') Linking.openSettings(); ## 8. Badge count -CometChat's Enhanced Push payload includes an `unreadMessageCount` field (total unread across conversations). +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. - - With 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. - Android has no OS-level app-icon badge API. If you want a launcher badge, read `unreadMessageCount` from the payload in `onMessageReceived` and apply it with your own badge library — the push package does not manage launcher badges. @@ -428,6 +415,9 @@ CometChat's Enhanced Push payload includes an `unreadMessageCount` field (total }); ``` + + With 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. + ## 9. Testing checklist @@ -442,23 +432,25 @@ CometChat's Enhanced Push payload includes an `unreadMessageCount` field (total - **Accept** joins the call (audio works both ways) and the screen tears down when the call ends. - **Decline** rejects the call promptly on the caller side — including from a killed state. - **Caller cancels** while it's ringing → the callee ring dismisses. -5. On an OEM device (MIUI/Oppo/Vivo), grant the section-7 permissions and re-check locked/killed calls. +5. Uninstall and reinstall, then log in: on Android 13+ the notification prompt must **wait** for your answer before the microphone/camera prompt appears, and pushes must arrive on that first install. +6. Log out and send a message from another user — the device must receive nothing. +7. On an OEM device (MIUI/Oppo/Vivo), grant the section-7 permissions and re-check locked/killed calls. ## 10. Troubleshooting | Symptom | Platform | Quick checks | | --- | --- | --- | -| No notifications received | Android | Confirm `google-services.json` is in `android/app/`, the package name matches Firebase, `firebase-messaging` + the `google-services` plugin are applied, and `POST_NOTIFICATIONS` is granted (Android 13+). | -| Killed app doesn't ring for a VoIP push | iOS | Ensure the `PKPushRegistry` is created on a **background queue** and the `AppDelegate` forwards `didReceiveIncomingVoIPPush` to `CometChatPushNotificationsAppDelegate`. Run `npx cometchat-pn doctor`. | +| No notifications received | Android | Confirm `google-services.json` is in `android/app/`, the package name matches Firebase, the `google-services` plugin is applied, and `POST_NOTIFICATIONS` is granted (Android 13+). | +| Killed app doesn't ring for a VoIP push | iOS | Ensure `CometChatVoIP.shared.register()` runs in `didFinishLaunchingWithOptions`, the generated file is in the app target, and no other code creates a `PKPushRegistry`. Run `npx cometchat-pn doctor`. | | Accepted call connects but has no audio | iOS | Confirm the `audio` background mode is present (the setup CLI adds it) and `react-native-webrtc` (via the Calls SDK) is linked in the app — the package coordinates CallKit's audio session with WebRTC automatically. | | Full-screen call UI not showing on lock screen | Android | OEM gate — grant Autostart / "Display pop-up while running in background" / "Show on lock screen" and disable battery optimization (section 7). | -| Declining a killed-state call doesn't reject the caller | Both | Android: ensure `registerBackgroundCallTask` is registered at module scope in `index.js`. iOS: the package handles it via CallKit — verify `AppDelegate` forwarding. | +| Declining a killed-state call doesn't reject the caller | Both | Android: ensure `registerBackgroundCallTask` is registered at module scope in `index.js`. iOS: the package handles it via CallKit — verify `CometChatVoIP.shared.register()` is called (step 2). | | Two notifications for one chat message | Android | Another FCM receiver is still installed — usually `@react-native-firebase/messaging`, whose broadcast receiver handles every push on its own. Remove the legacy push libraries so the package's service is the only handler. | | Notifications work on an existing install but not on a fresh one | Android | The permission dialog was cancelled before the user could answer it. `await` `requestNotificationPermission()` before requesting any other permission — Android allows one pending request per activity (step 4). Check with `adb shell dumpsys package \| grep POST_NOTIFICATIONS`: `granted=false` with no `USER_SET` flag means the dialog was never answered. | | Notifications still arrive after logout | Both | `unregister()` must run **before** `logout()` — it needs the session's auth token (step 5). Check that its rejection is not being swallowed. | | No notification while the app is open | Both | Expected with `showInForeground: false` (the default). Set it to `true` to show one. It affects chat notifications only; a foreground call always uses your in-app UI. | | Tapping a notification when the app was killed opens the app but not the conversation | Both | Wait for the navigator before navigating (`whenNavigationReady()`, step 6) — the tap is delivered before `NavigationContainer` mounts. | -| Duplicate navigation after re-login | Both | Unsubscribe every handler and call `unregister()` on logout (step 5). | +| Duplicate navigation after re-login | Both | Return `setupPushOnLogin()`'s cleanup from a `useEffect` keyed on the logged-in state so the handlers are removed on logout (step 5). | | No VoIP pushes | iOS | Ensure Push Notifications + Background Modes (Voice over IP) are enabled, `aps-environment` is correct (`production` for release), the bundle ID matches the CometChat APNs VoIP provider, and the VoIP cert is uploaded to the dashboard. | | Token registration errors | Both | Verify the provider IDs match the dashboard exactly and that `init()` runs **after** login. | From 5345d0bd3e40003e0a9d948b52a89e99eb1a76a9 Mon Sep 17 00:00:00 2001 From: Suraj Chauhan Date: Wed, 16 Sep 2026 17:19:28 +0530 Subject: [PATCH 10/20] docs(rn): document iOS setup directly; the setup CLI is removed MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The package no longer ships `npx cometchat-pn`. Like the Flutter, Android and iOS guides, the iOS tab now walks through the native setup itself: 1. Podfile platform 14.0 2. Push Notifications + Background Modes (Voice over IP, Remote notifications, Audio) 3. Microphone and camera usage strings in Info.plist 4. The full AppDelegate: one PKPushRegistry on a background queue, forwarding the APNs token, remote notifications, the VoIP token and incoming VoIP pushes to CometChatPushNotificationsAppDelegate The AppDelegate mirrors the one in the sample app QA signed off on — which never used the CLI — minus its Firebase setup, which the package's iOS side does not use. Also removes the CLI row from the quick reference and rewrites the rows, the cold-start warning and three troubleshooting entries that pointed at the CLI, the generated file or `doctor`. Co-Authored-By: Claude Opus 5 --- .../react-native-push-notifications.mdx | 116 +++++++++++++----- 1 file changed, 86 insertions(+), 30 deletions(-) diff --git a/notifications/react-native-push-notifications.mdx b/notifications/react-native-push-notifications.mdx index 2eb113dbb..b9aee6f72 100644 --- a/notifications/react-native-push-notifications.mdx +++ b/notifications/react-native-push-notifications.mdx @@ -12,8 +12,8 @@ description: "Add CometChat push notifications and VoIP calls to a React Native | Peer dependency | `@cometchat/chat-sdk-react-native` — must be initialized **and logged in** before `init()` | | Import | `import { CometChatPushNotifications, CometChatPNHelper, registerBackgroundCallTask } from "@cometchat/push-notifications-react-native";` | | Android setup | **No app-side code.** Add `google-services.json` + the `com.google.gms.google-services` plugin. The package already depends on `firebase-messaging` — do not add it. The package manifest declares the FCM service, ringing activity, and call foreground service. | -| iOS setup | `npx cometchat-pn setup` — adds `UIBackgroundModes` (`voip`, `remote-notification`, `audio`) + mic/camera usage strings, generates `ios//CometChatPushNotifications+AppDelegate.swift`. Add that file to the target; enable **Push Notifications** + **Background Modes**. | -| iOS AppDelegate | Add **one line** to `didFinishLaunchingWithOptions`: `CometChatVoIP.shared.register()`. The generated file owns the single `PKPushRegistry` (background queue) and forwards to `CometChatPushNotificationsAppDelegate`: `.didRegisterAPNsToken(_:)`, `.didUpdateVoIPToken(_:)`, `.didReceiveIncomingVoIPPush(_:)`, `.didReceiveRemoteNotification(_:)`. Never create a second `PKPushRegistry` (double-delivers every VoIP push). Swift `AppDelegate` (RN 0.77+); with Objective-C, call those methods yourself. | +| iOS setup | In Xcode enable **Push Notifications** + **Background Modes** (*Voice over IP*, *Remote notifications*, *Audio*). Add `NSMicrophoneUsageDescription` + `NSCameraUsageDescription` to `Info.plist`. Podfile `platform :ios, '14.0'`. | +| iOS AppDelegate | Conform to `PKPushRegistryDelegate`; in `didFinishLaunchingWithOptions` create **one** `PKPushRegistry` on a **background** queue (a `.main` queue misses iOS's ~5s report deadline on a killed-app cold start) with `desiredPushTypes = [.voIP]`. Forward to `CometChatPushNotificationsAppDelegate`: `.didRegisterAPNsToken(_:)`, `.didReceiveRemoteNotification(_:)`, `.didUpdateVoIPToken(_:)`, `.didReceiveIncomingVoIPPush(_:)` (then call `completion()`). A second `PKPushRegistry` double-delivers every VoIP push. Don't set a `UNUserNotificationCenter` delegate — the package installs one. | | Initialize | In a `setupPushOnLogin()`: subscribe handlers, then request permissions and `await CometChatPushNotifications.init(config)` — call **after** `CometChat.init(...)` and login. Returns a cleanup for a `useEffect`. | | Config (`PNConfiguration`) | `fcmProviderId?`, `apnsProviderId?`, `notificationSmallIcon?`, `androidChannelId?` (default `"cometchat_messages"`), `androidChannelName?` (default `"Messages"`), `voip?` (default `true`), `showInForeground?` (default `false`; chat notifications only — foreground calls always use the in-app UI) | | Permissions | `CometChatPNHelper.requestNotificationPermission(): Promise` (awaits the user's answer; **rejects** with `ERR_NO_ACTIVITY` / `ERR_PERMISSION_IN_FLIGHT` / `ERR_ACTIVITY_NOT_PERMISSION_AWARE` when it could not ask) · `.hasNotificationPermission(): Promise` · `.requestCallPermissions(): Promise` (mic + camera, required before a call connects). Await one before starting the next — Android allows one pending request per activity. | @@ -28,7 +28,6 @@ description: "Add CometChat push notifications and VoIP calls to a React Native | Coexist with existing push | `CometChatPNHelper.isCometChatNotification(data): boolean` — detect CometChat payloads when the app has its own FCM handler | | Exported types | `PNConfiguration` · `PNCallInfo` · `PNCallEndEvent` · `PNNotificationTapInfo` · `PNTokenPlatform` · `PNBackgroundCallAction` · `PNReceiverType` (`"user"` \| `"group"`) · `PNCallType` (`"audio"` \| `"video"`) · `PNCallEndReason` · `PNTokenEvent` | | Android permissions | Declared by the package: `POST_NOTIFICATIONS`, `USE_FULL_SCREEN_INTENT`, `FOREGROUND_SERVICE`(+`_PHONE_CALL`), `WAKE_LOCK`, `VIBRATE`, `RECORD_AUDIO`, `CAMERA`, `BLUETOOTH_CONNECT`. **No telecom/calling-account permission** — the lock-screen ring is a full-screen-intent activity, not a `ConnectionService`. | -| CLI | `npx cometchat-pn setup` (configure iOS) · `npx cometchat-pn doctor` (diagnose) | | Related | [Push Overview](/notifications/push-overview) · [Templates & Sounds](/notifications/templates-and-sounds) · [Badge Count](/notifications/badge-count) | @@ -36,7 +35,7 @@ description: "Add CometChat push notifications and VoIP calls to a React Native ## What this guide covers - Adding the `@cometchat/push-notifications-react-native` package and initializing it. -- Platform wiring: Firebase/`google-services.json` on Android, the setup CLI + PushKit forwarding on iOS. +- Platform wiring: Firebase/`google-services.json` on Android, capabilities + `AppDelegate` PushKit forwarding on iOS. - Requesting permission and registering tokens (FCM on Android, APNs + VoIP on iOS) after login. - Receiving pushes and letting the package render chat notifications and full-screen / CallKit calls. - Handling notification taps (including thread deep-links), incoming-call navigation, and Android OEM permissions. @@ -49,7 +48,7 @@ The `@cometchat/push-notifications-react-native` package replaces the previous a ## How it works - **Android (FCM):** Firebase issues the registration token and delivers the CometChat payload as a data message. The package ships its **own** `FirebaseMessagingService`, so it receives the message and shows the notification or full-screen call itself — **you write no FCM handling code**. -- **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 package. Your `AppDelegate` forwards the tokens and incoming VoIP pushes to the package (the setup CLI generates this). +- **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 package. Your `AppDelegate` forwards the tokens and incoming VoIP pushes to the package (step 2). - **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 package's role:** it retrieves the tokens, registers them with CometChat, parses payloads, drives the call UI, and calls the Chat SDK to accept/reject/end. It requires [`@cometchat/chat-sdk-react-native`](https://www.npmjs.com/package/@cometchat/chat-sdk-react-native) as a peer dependency — the one Chat SDK your app already uses, so there is no second SDK to version-align. @@ -129,40 +128,97 @@ npm install @cometchat/push-notifications-react-native cd ios && pod install && cd .. ``` - 2. Run the setup CLI from your project root — it adds the required `UIBackgroundModes` (`voip`, `remote-notification`, `audio`) and the mic/camera usage strings to `Info.plist`, and generates `ios//CometChatPushNotifications+AppDelegate.swift`: + 2. In Xcode, select your app target → **Signing & Capabilities** and add: + - **Push Notifications** + - **Background Modes**, with **Voice over IP**, **Remote notifications**, and **Audio, AirPlay, and Picture in Picture** checked - ```bash - npx cometchat-pn setup - ``` + 3. Add the microphone and camera usage strings to `ios//Info.plist` — iOS needs them before a call can use either: - 3. In Xcode, **add the generated `CometChatPushNotifications+AppDelegate.swift` to your app target**, and enable the **Push Notifications** and **Background Modes** capabilities (the latter with *Voice over IP* + *Remote notifications* + *Audio*). + ```xml lines + NSMicrophoneUsageDescription + Needed for voice and video calls + NSCameraUsageDescription + Needed for video calls + ``` - 4. The generated file handles PushKit and the APNs forwarding for you — it owns the app's `PKPushRegistry` (on a background queue) and forwards the APNs device token, the VoIP token, and incoming VoIP pushes to the package. Start it with **one line** in your `AppDelegate`: + 4. In `AppDelegate.swift`, create **one** `PKPushRegistry` and forward the APNs token, the VoIP token, and incoming VoIP pushes to the package: ```swift lines - override func application( - _ application: UIApplication, - didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? = nil - ) -> Bool { - // ... your existing React Native setup ... - CometChatVoIP.shared.register() // before `return true` - return true + import PushKit + import react_native_cometchat_push_notifications + + @main + class AppDelegate: UIResponder, UIApplicationDelegate, PKPushRegistryDelegate { + private var pushRegistry: PKPushRegistry? + + func application( + _ application: UIApplication, + didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? = nil + ) -> Bool { + // ... your existing React Native setup ... + + // BACKGROUND queue, not .main: on a killed-app cold start the main thread is busy + // starting React Native, and iOS terminates the app if the call isn't reported + // within ~5 seconds of the push. + pushRegistry = PKPushRegistry(queue: DispatchQueue(label: "com.cometchat.voip.pushkit")) + pushRegistry?.delegate = self + pushRegistry?.desiredPushTypes = [.voIP] + + return true + } + + // APNs device token — chat notifications + func application( + _ application: UIApplication, + didRegisterForRemoteNotificationsWithDeviceToken deviceToken: Data + ) { + CometChatPushNotificationsAppDelegate.didRegisterAPNsToken(deviceToken) + } + + func application( + _ application: UIApplication, + didReceiveRemoteNotification userInfo: [AnyHashable: Any], + fetchCompletionHandler completionHandler: @escaping (UIBackgroundFetchResult) -> Void + ) { + CometChatPushNotificationsAppDelegate.didReceiveRemoteNotification(userInfo) + completionHandler(.noData) + } + + // VoIP token — calls + func pushRegistry( + _ registry: PKPushRegistry, + didUpdate pushCredentials: PKPushCredentials, + for type: PKPushType + ) { + guard type == .voIP else { return } + CometChatPushNotificationsAppDelegate.didUpdateVoIPToken(pushCredentials.token) + } + + // Incoming VoIP push — the package reports it to CallKit before this returns + func pushRegistry( + _ registry: PKPushRegistry, + didReceiveIncomingPushWith payload: PKPushPayload, + for type: PKPushType, + completion: @escaping () -> Void + ) { + guard type == .voIP else { return completion() } + CometChatPushNotificationsAppDelegate.didReceiveIncomingVoIPPush(payload.dictionaryPayload) + completion() + } + + func pushRegistry(_ registry: PKPushRegistry, didInvalidatePushTokenFor type: PKPushType) {} } ``` + You don't set a `UNUserNotificationCenter` delegate — the package installs its own at launch to handle foreground presentation and notification taps. + - **Do not create a second `PKPushRegistry`** anywhere in your app — every VoIP push would be delivered twice. If your `AppDelegate` already implements `didRegisterForRemoteNotificationsWithDeviceToken` or `didReceiveRemoteNotification`, the generated extension will clash with it: move the single `CometChatPushNotificationsAppDelegate` call from the generated file into your existing method and delete that method from the generated file. + **Create exactly one `PKPushRegistry`.** A second one — your own, or from a library such as `react-native-voip-push-notification` — makes iOS deliver every VoIP push twice. Remove other VoIP push libraries. - The generated file is Swift and extends `AppDelegate`, which matches the React Native 0.77+ template. With an Objective-C `AppDelegate.mm`, call the same `CometChatPushNotificationsAppDelegate` methods from your Objective-C delegate methods instead. + With an Objective-C `AppDelegate.mm`, make the same `CometChatPushNotificationsAppDelegate` calls from your Objective-C delegate methods. - - 5. Verify the wiring at any time: - - ```bash - npx cometchat-pn doctor - ``` @@ -376,7 +432,7 @@ function handleCallEnded(info) { ``` -**Cold-start VoIP handling (iOS):** when the app is killed and a VoIP push arrives, the package presents CallKit natively via PushKit before React Native is ready. When the user answers, the app cold-starts, `init()` replays the accepted call, `onCallAccepted` fires, and the package has already called `CometChat.acceptCall` — set up your call session/screen there. (This path is why the generated `CometChatVoIP` keeps its `PKPushRegistry` on a background queue — call `CometChatVoIP.shared.register()` as shown in step 2.) +**Cold-start VoIP handling (iOS):** when the app is killed and a VoIP push arrives, the package presents CallKit natively via PushKit before React Native is ready. When the user answers, the app cold-starts, `init()` replays the accepted call, `onCallAccepted` fires, and the package has already called `CometChat.acceptCall` — set up your call session/screen there. (This path is why the `PKPushRegistry` must be created on a background queue — see step 2.) ## 7. Android: OEM permissions for lock-screen calls @@ -441,10 +497,10 @@ CometChat's Enhanced Push Notification payload includes an `unreadMessageCount` | Symptom | Platform | Quick checks | | --- | --- | --- | | No notifications received | Android | Confirm `google-services.json` is in `android/app/`, the package name matches Firebase, the `google-services` plugin is applied, and `POST_NOTIFICATIONS` is granted (Android 13+). | -| Killed app doesn't ring for a VoIP push | iOS | Ensure `CometChatVoIP.shared.register()` runs in `didFinishLaunchingWithOptions`, the generated file is in the app target, and no other code creates a `PKPushRegistry`. Run `npx cometchat-pn doctor`. | -| Accepted call connects but has no audio | iOS | Confirm the `audio` background mode is present (the setup CLI adds it) and `react-native-webrtc` (via the Calls SDK) is linked in the app — the package coordinates CallKit's audio session with WebRTC automatically. | +| Killed app doesn't ring for a VoIP push | iOS | Ensure the `PKPushRegistry` is created in `didFinishLaunchingWithOptions` on a **background queue**, `didReceiveIncomingPushWith` forwards to `CometChatPushNotificationsAppDelegate.didReceiveIncomingVoIPPush`, and nothing else creates a `PKPushRegistry` (step 2). | +| Accepted call connects but has no audio | iOS | Confirm the `audio` background mode is enabled (Background Modes → Audio, step 2) and `react-native-webrtc` (via the Calls SDK) is linked in the app — the package coordinates CallKit's audio session with WebRTC automatically. | | Full-screen call UI not showing on lock screen | Android | OEM gate — grant Autostart / "Display pop-up while running in background" / "Show on lock screen" and disable battery optimization (section 7). | -| Declining a killed-state call doesn't reject the caller | Both | Android: ensure `registerBackgroundCallTask` is registered at module scope in `index.js`. iOS: the package handles it via CallKit — verify `CometChatVoIP.shared.register()` is called (step 2). | +| Declining a killed-state call doesn't reject the caller | Both | Android: ensure `registerBackgroundCallTask` is registered at module scope in `index.js`. iOS: the package handles it via CallKit — verify the `AppDelegate` forwards `didReceiveIncomingVoIPPush` (step 2). | | Two notifications for one chat message | Android | Another FCM receiver is still installed — usually `@react-native-firebase/messaging`, whose broadcast receiver handles every push on its own. Remove the legacy push libraries so the package's service is the only handler. | | Notifications work on an existing install but not on a fresh one | Android | The permission dialog was cancelled before the user could answer it. `await` `requestNotificationPermission()` before requesting any other permission — Android allows one pending request per activity (step 4). Check with `adb shell dumpsys package \| grep POST_NOTIFICATIONS`: `granted=false` with no `USER_SET` flag means the dialog was never answered. | | Notifications still arrive after logout | Both | `unregister()` must run **before** `logout()` — it needs the session's auth token (step 5). Check that its rejection is not being swallowed. | From 7d431906206bccc97c3816c2ad0c2fb29785e9ec Mon Sep 17 00:00:00 2001 From: Suraj Chauhan Date: Thu, 17 Sep 2026 13:23:28 +0530 Subject: [PATCH 11/20] docs(react-native): make the push guide match the sample app, copy-paste ready The guide's code is now the sample app's implementation, as complete files that type-check in a strict React Native + UI Kit project: - src/AppCredentials.ts, src/navigation/navigationRef.ts (waits for the navigator), src/push/pushNotifications.ts (setupPushOnLogin + usePushOnLogin), src/push/logout.ts, and the App.tsx / index.js wiring - tap handling opens the thread with the user/group, marks the conversation read; call-ended tears the call down and leaves the call screen - push follows a fresh login and a restored session, and cleans up on logout Setup corrections found by following the guide on a clean app: - keep the Podfile's min_ios_version_supported; lowering it breaks the build - full Swift AppDelegate with the PushKit registry created before React Native starts - add the ic_notification drawable; registerBackgroundCallTask() now needs no handler - MANAGE_OWN_CALLS listed (Android 14+ ringing screen); PNBackgroundCallOptions exported - the forwarding API is Swift-only, so Objective-C AppDelegates must move to Swift - testing and troubleshooting cover fresh installs, killed-app decline, the Android 14+ ringing screen and logout Co-Authored-By: Claude Opus 5 --- .../react-native-push-notifications.mdx | 616 +++++++++++------- 1 file changed, 396 insertions(+), 220 deletions(-) diff --git a/notifications/react-native-push-notifications.mdx b/notifications/react-native-push-notifications.mdx index b9aee6f72..3613d00d8 100644 --- a/notifications/react-native-push-notifications.mdx +++ b/notifications/react-native-push-notifications.mdx @@ -9,25 +9,27 @@ description: "Add CometChat push notifications and VoIP calls to a React Native | --- | --- | | Goal | Add push notifications and VoIP calling (FCM on Android, APNs + PushKit on iOS) to a React Native app | | Package | `@cometchat/push-notifications-react-native` | -| Peer dependency | `@cometchat/chat-sdk-react-native` — must be initialized **and logged in** before `init()` | +| Peer dependencies | `@cometchat/chat-sdk-react-native` (initialized **and logged in** before `init()`); `@cometchat/calls-sdk-react-native` for calls | | Import | `import { CometChatPushNotifications, CometChatPNHelper, registerBackgroundCallTask } from "@cometchat/push-notifications-react-native";` | -| Android setup | **No app-side code.** Add `google-services.json` + the `com.google.gms.google-services` plugin. The package already depends on `firebase-messaging` — do not add it. The package manifest declares the FCM service, ringing activity, and call foreground service. | -| iOS setup | In Xcode enable **Push Notifications** + **Background Modes** (*Voice over IP*, *Remote notifications*, *Audio*). Add `NSMicrophoneUsageDescription` + `NSCameraUsageDescription` to `Info.plist`. Podfile `platform :ios, '14.0'`. | -| iOS AppDelegate | Conform to `PKPushRegistryDelegate`; in `didFinishLaunchingWithOptions` create **one** `PKPushRegistry` on a **background** queue (a `.main` queue misses iOS's ~5s report deadline on a killed-app cold start) with `desiredPushTypes = [.voIP]`. Forward to `CometChatPushNotificationsAppDelegate`: `.didRegisterAPNsToken(_:)`, `.didReceiveRemoteNotification(_:)`, `.didUpdateVoIPToken(_:)`, `.didReceiveIncomingVoIPPush(_:)` (then call `completion()`). A second `PKPushRegistry` double-delivers every VoIP push. Don't set a `UNUserNotificationCenter` delegate — the package installs one. | -| Initialize | In a `setupPushOnLogin()`: subscribe handlers, then request permissions and `await CometChatPushNotifications.init(config)` — call **after** `CometChat.init(...)` and login. Returns a cleanup for a `useEffect`. | -| Config (`PNConfiguration`) | `fcmProviderId?`, `apnsProviderId?`, `notificationSmallIcon?`, `androidChannelId?` (default `"cometchat_messages"`), `androidChannelName?` (default `"Messages"`), `voip?` (default `true`), `showInForeground?` (default `false`; chat notifications only — foreground calls always use the in-app UI) | -| Permissions | `CometChatPNHelper.requestNotificationPermission(): Promise` (awaits the user's answer; **rejects** with `ERR_NO_ACTIVITY` / `ERR_PERMISSION_IN_FLIGHT` / `ERR_ACTIVITY_NOT_PERMISSION_AWARE` when it could not ask) · `.hasNotificationPermission(): Promise` · `.requestCallPermissions(): Promise` (mic + camera, required before a call connects). Await one before starting the next — Android allows one pending request per activity. | -| Token registration | Automatic on `init()`. Manual: `CometChatPushNotifications.registerToken(platform, token)` where `PNTokenPlatform` = `"fcm"` \| `"apns"` \| `"voip"` | -| Events (each returns `Unsubscribe`) | `onNotificationTap(cb)` · `onCallAccepted(cb)` · `onCallEnded(cb)` · `onMessageReceived(cb)` | -| Cold-start replay | `onNotificationTap` and `onCallAccepted` replay a buffered event that fired before JS was ready — subscribe once at startup, not lazily. The tap can arrive before `NavigationContainer` mounts: wait for `navigationRef.isReady()` / the ref's `'ready'` event before navigating | -| `PNNotificationTapInfo` | `receiverType?` · `sender?` · `senderName?` · `receiver?` · `conversationId?` · `messageId?` · `parentMessageId?` (present for thread replies) | +| Files you add | `src/AppCredentials.ts` (provider IDs) · `src/navigation/navigationRef.ts` (`navigationRef`, `whenNavigationReady`, `navigate`) · `src/push/pushNotifications.ts` (`setupPushOnLogin`, `usePushOnLogin`) · `src/push/logout.ts` | +| Android setup | Apply the `com.google.gms.google-services` plugin (with `google-services.json` in `android/app/`); `minSdkVersion 24`+; add a monochrome `ic_notification` drawable. **No manifest changes** — the package declares its services, activities and permissions. Do **not** add `firebase-messaging` (the package brings it). Remove other push libraries (`@react-native-firebase/messaging`, `notifee`, `react-native-callkeep`, `react-native-voip-push-notification`). | +| iOS setup | Keep the Podfile's `platform :ios, min_ios_version_supported` (never lower it); `pod install`. Enable **Push Notifications** + **Background Modes** (*Voice over IP*, *Remote notifications*, *Audio*). Add `NSMicrophoneUsageDescription` + `NSCameraUsageDescription`. | +| iOS AppDelegate | Swift only. Conform to `PKPushRegistryDelegate`. In `didFinishLaunchingWithOptions`, create **one** `PKPushRegistry` on a **background** queue with `desiredPushTypes = [.voIP]` **before** starting React Native. Forward to `CometChatPushNotificationsAppDelegate`: `.didRegisterAPNsToken(_:)`, `.didReceiveRemoteNotification(_:)`, `.didUpdateVoIPToken(_:)`, `.didReceiveIncomingVoIPPush(_:)` then `completion()`. Don't set a `UNUserNotificationCenter` delegate — the package installs one. | +| index.js | `registerBackgroundCallTask();` at module scope — the package rejects a call declined while the Android app is killed. Optional `registerBackgroundCallTask(handler, { rejectDeclinedCalls })`: the handler runs **after** the package's reject; `rejectDeclinedCalls: false` hands the reject to the handler. No-op on iOS. | +| Initialize | Call `usePushOnLogin()` once in a component rendered **after** `CometChat.init` resolves. It runs `setupPushOnLogin()` after a fresh login **and** a restored session, and its cleanup on logout. `setupPushOnLogin()` subscribes the handlers **before** `init()`, requests permissions in order, then `await CometChatPushNotifications.init(config)`. | +| Config (`PNConfiguration`) | `fcmProviderId?`, `apnsProviderId?`, `notificationSmallIcon?`, `androidChannelId?` (default `"cometchat_messages"`), `androidChannelName?` (default `"Messages"`), `voip?` (default `true`), `showInForeground?` (default `false`; chat notifications only) | +| Permissions | `CometChatPNHelper.requestNotificationPermission(): Promise` (resolves on the user's answer; **rejects** with `ERR_NO_ACTIVITY` / `ERR_PERMISSION_IN_FLIGHT` / `ERR_ACTIVITY_NOT_PERMISSION_AWARE` when it could not ask — catch it, don't skip `init()`) · `.hasNotificationPermission()` · `.requestCallPermissions()` (mic + camera). Await each before the next. | +| Token registration | Automatic in `init()`, including refreshes. Manual: `CometChatPushNotifications.registerToken(platform, token)`, `PNTokenPlatform` = `"fcm"` \| `"apns"` \| `"voip"` | +| Events (each returns `Unsubscribe`) | `onNotificationTap(cb)` · `onCallAccepted(cb)` (call already accepted) · `onCallEnded(cb)` · `onMessageReceived(cb)` | +| Notification tap | Wait for the navigator (`whenNavigationReady()`), mark the conversation read, then open `ThreadView` with the parent message **and** the user/group when `parentMessageId` is set (fall back to the conversation), else `Messages` with the user/group. Group taps use `info.receiver` (the GUID). | +| Call ended | End the call on the server (`CometChat.endCall`), end the media session (`CometChatCalls.endSession()`), `CometChat.clearActiveCall()`, and leave the call screen — a call ended from the iOS call screen isn't seen by the Calls SDK. | +| Logout | `await CometChatPushNotifications.unregister()` **before** `CometChatUIKit.logout()` / `CometChat.logout()` (it needs the auth token); if it fails, don't log out. `usePushOnLogin()` removes the handlers when logout completes. | +| `PNNotificationTapInfo` | `receiverType?` · `sender?` · `senderName?` · `receiver?` · `conversationId?` · `messageId?` · `parentMessageId?` (thread replies) | | `PNCallInfo` | `sessionId` · `callerUid?` · `callerName?` · `callerAvatar?` · `receiverId?` · `receiverType?` · `callType?` · `callAction?` | | `PNCallEndEvent` | `sessionId` · `reason?` (`"cancelled"` \| `"unanswered"` \| `"rejected"` \| `"busy"` \| `"ended"`) | -| Killed-state decline | `registerBackgroundCallTask(async (action, info) => { ... })` in `index.js`, outside the component tree. `PNBackgroundCallAction` = `"decline"` only. Android-only; iOS declines natively via CallKit. | -| Unregister on logout | `await CometChatPushNotifications.unregister()` **before** `CometChat.logout()` (it needs the session's auth token). Handlers are removed by the cleanup `setupPushOnLogin()` returns — skipping it fires each handler twice after a re-login. `dispose()` detaches native listeners (app-facing subscriptions stay intact). | -| Coexist with existing push | `CometChatPNHelper.isCometChatNotification(data): boolean` — detect CometChat payloads when the app has its own FCM handler | -| Exported types | `PNConfiguration` · `PNCallInfo` · `PNCallEndEvent` · `PNNotificationTapInfo` · `PNTokenPlatform` · `PNBackgroundCallAction` · `PNReceiverType` (`"user"` \| `"group"`) · `PNCallType` (`"audio"` \| `"video"`) · `PNCallEndReason` · `PNTokenEvent` | -| Android permissions | Declared by the package: `POST_NOTIFICATIONS`, `USE_FULL_SCREEN_INTENT`, `FOREGROUND_SERVICE`(+`_PHONE_CALL`), `WAKE_LOCK`, `VIBRATE`, `RECORD_AUDIO`, `CAMERA`, `BLUETOOTH_CONNECT`. **No telecom/calling-account permission** — the lock-screen ring is a full-screen-intent activity, not a `ConnectionService`. | +| Coexist with existing push | `CometChatPNHelper.isCometChatNotification(data): boolean` | +| Exported types | `PNConfiguration` · `PNCallInfo` · `PNCallEndEvent` · `PNNotificationTapInfo` · `PNTokenPlatform` · `PNBackgroundCallAction` · `PNBackgroundCallOptions` · `PNReceiverType` · `PNCallType` · `PNCallEndReason` · `PNTokenEvent` | +| Android permissions | Declared by the package: `POST_NOTIFICATIONS`, `USE_FULL_SCREEN_INTENT`, `FOREGROUND_SERVICE`, `FOREGROUND_SERVICE_PHONE_CALL`, `MANAGE_OWN_CALLS`, `WAKE_LOCK`, `VIBRATE`, `RECORD_AUDIO`, `CAMERA`, `BLUETOOTH_CONNECT`. `MANAGE_OWN_CALLS` is required for the ringing foreground service on Android 14+; it is auto-granted and registers **no** calling account. | | Related | [Push Overview](/notifications/push-overview) · [Templates & Sounds](/notifications/templates-and-sounds) · [Badge Count](/notifications/badge-count) | @@ -55,42 +57,46 @@ The `@cometchat/push-notifications-react-native` package replaces the previous a ## Prerequisites - The providers, Firebase project, and Apple/APNs credentials from **[Getting Started](/notifications/push-overview)** (this guide assumes those are done). -- React Native **0.65+**, and an app already initializing and logging in with `@cometchat/chat-sdk-react-native` (or the UI Kit). +- React Native **0.65+**, an app already initializing and logging in with `@cometchat/chat-sdk-react-native` or the UI Kit, and `@cometchat/calls-sdk-react-native` for calls. The code below uses the UI Kit's screens and React Navigation — rename the screens to yours. - **Android:** `google-services.json` in `android/app/`, the `com.google.gms.google-services` plugin, `minSdkVersion 24`+. -- **iOS:** iOS 13.0+ (set the Podfile platform to **14.0** for VoIP/CallKit). +- **iOS:** iOS 13.0+. React Native already sets a higher minimum in your Podfile — keep it. - A physical device — background delivery, full-screen calls, and VoIP pushes are unreliable on emulators/simulators. -**Complete the [Getting Started](/notifications/push-overview) 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 React Native app wiring. +**Complete the [Getting Started](/notifications/push-overview) guide first** — enable Push Notifications, add your providers (FCM for Android; APNs for iOS — the package registers both the device token and the VoIP token with that one APNs provider), and finish the Firebase/Apple setup. This guide covers only the React Native 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: +Create `src/AppCredentials.ts` with the values from Getting Started. Only the fields for the platforms you ship are needed: -```ts lines +```ts src/AppCredentials.ts lines export const AppCredentials = { - appId: "YOUR_APP_ID", - region: "YOUR_REGION", - authKey: "YOUR_AUTH_KEY", + appId: 'YOUR_APP_ID', + region: 'YOUR_REGION', + authKey: 'YOUR_AUTH_KEY', - // Android - fcmProviderId: "FCM-PROVIDER-ID", + // Android — the FCM provider ID from the CometChat dashboard + fcmProviderId: 'FCM-PROVIDER-ID', - // iOS — one provider covers the APNs device token and the VoIP token - apnsProviderId: "APNS-PROVIDER-ID", + // iOS — one APNs provider covers both the device token and the VoIP token + apnsProviderId: 'APNS-PROVIDER-ID', }; ``` ## 2. Add the package and configure the platform -Install the package (the Chat SDK peer is already in your app): +Install the package (the Chat SDK and Calls SDK are already in your app): ```bash npm install @cometchat/push-notifications-react-native -# or: yarn add @cometchat/push-notifications-react-native +cd ios && pod install && cd .. ``` + +**Remove any other push library first** — `@react-native-firebase/messaging`, `@notifee/react-native`, `react-native-callkeep`, `react-native-voip-push-notification` — along with their code and native setup. Each registers its own push handler or call registry, and every notification or call then arrives twice. + + With `google-services.json` already in `android/app/` (from [Getting Started](/notifications/push-overview)): @@ -113,26 +119,20 @@ npm install @cometchat/push-notifications-react-native 2. Keep `minSdkVersion 24` or higher. + 3. Add the status-bar notification icon: a **white-on-transparent** drawable named `ic_notification` — for example `android/app/src/main/res/drawable/ic_notification.png`. Android Studio generates one: right-click `res` → **New → Image Asset**, icon type **Notification Icons**, name `ic_notification`. Without it, the package falls back to your launcher icon, which the status bar draws as a plain white shape. + - You do **not** need to add notification, call, full-screen-intent, or lock-screen permissions to your `AndroidManifest.xml`, and you write **no** FCM/JS message-handling code. The package's library manifest contributes everything it needs — the `FirebaseMessagingService`, the incoming-call foreground service, the full-screen lock-screen `CallRingingActivity`, the notification trampoline/decline receiver, and `POST_NOTIFICATIONS` — and Gradle merges them into your app automatically. + You do **not** edit `AndroidManifest.xml` and you write **no** FCM code. The package's manifest contributes the `FirebaseMessagingService`, the incoming-call foreground service, the full-screen `CallRingingActivity`, the notification action receivers, and every permission it needs — merged into your app by Gradle. - 1. Set the deployment target in `ios/Podfile`, then install pods: - - ```ruby - platform :ios, '14.0' - ``` - - ```bash - cd ios && pod install && cd .. - ``` + 1. **Keep** the Podfile's `platform :ios, min_ios_version_supported` from the React Native template. Don't lower it: current React Native requires iOS 15.1, and a lower platform fails the build — for example with `'hermes/hermes.h' file not found`. 2. In Xcode, select your app target → **Signing & Capabilities** and add: - **Push Notifications** - **Background Modes**, with **Voice over IP**, **Remote notifications**, and **Audio, AirPlay, and Picture in Picture** checked - 3. Add the microphone and camera usage strings to `ios//Info.plist` — iOS needs them before a call can use either: + 3. Add the microphone and camera usage strings to `ios//Info.plist` — a call can't use either without them: ```xml lines NSMicrophoneUsageDescription @@ -141,29 +141,46 @@ npm install @cometchat/push-notifications-react-native Needed for video calls ``` - 4. In `AppDelegate.swift`, create **one** `PKPushRegistry` and forward the APNs token, the VoIP token, and incoming VoIP pushes to the package: + 4. Replace `ios//AppDelegate.swift` with this — React Native's current template plus the push lines. Set `withModuleName` to your app's name: - ```swift lines + ```swift ios//AppDelegate.swift lines + import UIKit + import React + import React_RCTAppDelegate + import ReactAppDependencyProvider import PushKit import react_native_cometchat_push_notifications @main class AppDelegate: UIResponder, UIApplicationDelegate, PKPushRegistryDelegate { + var window: UIWindow? + var reactNativeDelegate: ReactNativeDelegate? + var reactNativeFactory: RCTReactNativeFactory? private var pushRegistry: PKPushRegistry? func application( _ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? = nil ) -> Bool { - // ... your existing React Native setup ... - - // BACKGROUND queue, not .main: on a killed-app cold start the main thread is busy - // starting React Native, and iOS terminates the app if the call isn't reported - // within ~5 seconds of the push. + let delegate = ReactNativeDelegate() + let factory = RCTReactNativeFactory(delegate: delegate) + delegate.dependencyProvider = RCTAppDependencyProvider() + reactNativeDelegate = delegate + reactNativeFactory = factory + + // Create the PushKit registry BEFORE starting React Native, on a BACKGROUND queue. + // When a call wakes a killed app, iOS terminates it unless the call is reported within + // ~5 seconds — the main thread is busy starting React Native and would miss that. pushRegistry = PKPushRegistry(queue: DispatchQueue(label: "com.cometchat.voip.pushkit")) pushRegistry?.delegate = self pushRegistry?.desiredPushTypes = [.voIP] + window = UIWindow(frame: UIScreen.main.bounds) + factory.startReactNative( + withModuleName: "YourAppName", // your app's registered name + in: window, + launchOptions: launchOptions + ) return true } @@ -194,7 +211,7 @@ npm install @cometchat/push-notifications-react-native CometChatPushNotificationsAppDelegate.didUpdateVoIPToken(pushCredentials.token) } - // Incoming VoIP push — the package reports it to CallKit before this returns + // Incoming VoIP push — the package reports the call to CallKit before this returns func pushRegistry( _ registry: PKPushRegistry, didReceiveIncomingPushWith payload: PKPushPayload, @@ -208,231 +225,385 @@ npm install @cometchat/push-notifications-react-native func pushRegistry(_ registry: PKPushRegistry, didInvalidatePushTokenFor type: PKPushType) {} } + + class ReactNativeDelegate: RCTDefaultReactNativeFactoryDelegate { + override func sourceURL(for bridge: RCTBridge) -> URL? { + self.bundleURL() + } + + override func bundleURL() -> URL? { + #if DEBUG + return RCTBundleURLProvider.sharedSettings().jsBundleURL(forBundleRoot: "index") + #else + return Bundle.main.url(forResource: "main", withExtension: "jsbundle") + #endif + } + } ``` - You don't set a `UNUserNotificationCenter` delegate — the package installs its own at launch to handle foreground presentation and notification taps. + You don't set a `UNUserNotificationCenter` delegate — the package installs its own at launch to handle foreground notifications and taps. - **Create exactly one `PKPushRegistry`.** A second one — your own, or from a library such as `react-native-voip-push-notification` — makes iOS deliver every VoIP push twice. Remove other VoIP push libraries. + **Create exactly one `PKPushRegistry`.** A second one — your own or a library's — makes iOS deliver every VoIP push twice. - With an Objective-C `AppDelegate.mm`, make the same `CometChatPushNotificationsAppDelegate` calls from your Objective-C delegate methods. + **Older Swift template** (an `RCTAppDelegate` subclass): add the same registry lines before `return super.application(...)`, which starts React Native, and the same `CometChatPushNotificationsAppDelegate` calls in your delegate methods. **Objective-C `AppDelegate.mm`:** the package's iOS entry points are Swift-only, so move the AppDelegate to Swift first — the [React Native Upgrade Helper](https://react-native-community.github.io/upgrade-helper/) shows the change. ## 3. Initialize the SDK -Register the killed-state background task **at module scope** in `index.js` (before any component renders), then initialize the package **after the user logs in**. +This step adds two files and changes two. Copy the new files as they are — only the route names in `SCREENS` should need changing. -```js lines -// index.js +**`index.js`** — register the package's background task at module scope, before any component renders: + +```js index.js lines import { AppRegistry } from 'react-native'; -import { CometChat } from '@cometchat/chat-sdk-react-native'; import { registerBackgroundCallTask } from '@cometchat/push-notifications-react-native'; import App from './App'; import { name as appName } from './app.json'; -// Android only: runs when the app is FULLY KILLED and the user taps Decline on the -// call notification. A killed app has no JS alive, so this headless task boots just -// enough to reject the call — otherwise the caller only times out. (iOS declines -// natively via CallKit, so this is a no-op there.) +// Android: lets a FULLY KILLED app reject a call declined from its notification. The package +// does the work — this only registers its background task. (No-op on iOS.) +registerBackgroundCallTask(); + +AppRegistry.registerComponent(appName, () => App); +``` + +On Android this lets a **fully killed** app reject a call declined from its notification — the package re-initializes the Chat SDK with the settings `init()` saved and rejects the call. To add your own logic, pass a handler: it runs **after** the package has rejected the call, with the Chat SDK initialized and logged in. + +```js lines registerBackgroundCallTask(async (action, info) => { - if (action === 'decline' && info.sessionId) { - // (re)initialize + login your CometChat session here, then: - await CometChat.rejectCall(info.sessionId, CometChat.CALL_STATUS.REJECTED); - } + // Your logic, e.g. record the declined call. The Chat SDK is initialized and logged in here. + console.log('Declined call from', info.callerUid); }); -AppRegistry.registerComponent(appName, () => App); +// Or reject it yourself instead: +// registerBackgroundCallTask(myHandler, { rejectDeclinedCalls: false }); ``` -```ts lines -// call this once, after CometChat.init(...) and login succeed: +**`src/navigation/navigationRef.ts`** — a notification tap or answered call that **launched** the app arrives before your navigator exists, so every navigation waits for it: + +```ts src/navigation/navigationRef.ts lines +import { createNavigationContainerRef } from '@react-navigation/native'; + +/** Pass this to your . */ +export const navigationRef = createNavigationContainerRef(); + +/** + * Resolves once the NavigationContainer is mounted. A notification tap or answered call + * that LAUNCHED the app arrives before the navigator exists, and navigating then is + * silently dropped. The ref queues listeners added before it mounts. + */ +export function whenNavigationReady(): Promise { + if (navigationRef.isReady()) return Promise.resolve(); + return new Promise(resolve => { + const unsubscribe = navigationRef.addListener('ready', () => { + unsubscribe(); + resolve(); + }); + }); +} + +/** Navigate by route name once the navigator is ready. */ +export async function navigate(name: string, params?: object): Promise { + await whenNavigationReady(); + (navigationRef.navigate as (name: string, params?: object) => void)(name, params); +} +``` + +**`src/push/pushNotifications.ts`** — everything push does for the logged-in user: + +```ts src/push/pushNotifications.ts lines +import { useEffect, useState } from 'react'; +import { CometChat } from '@cometchat/chat-sdk-react-native'; +import { CometChatCalls } from '@cometchat/calls-sdk-react-native'; +import { CometChatUIEventHandler, MessageEvents } from '@cometchat/chat-uikit-react-native'; import { - CometChatPushNotifications, CometChatPNHelper, + CometChatPushNotifications, + type PNCallEndEvent, + type PNCallInfo, + type PNNotificationTapInfo, } from '@cometchat/push-notifications-react-native'; -// Returns a cleanup that removes the handlers — wire it to a `useEffect` (step 5). +import { AppCredentials } from '../AppCredentials'; +import { navigate, navigationRef } from '../navigation/navigationRef'; + +/** Your navigator's route names — these are the CometChat UI Kit sample app's. */ +const SCREENS = { + messages: 'Messages', + thread: 'ThreadView', + ongoingCall: 'OngoingCallScreen', + home: 'BottomTabNavigator', +} as const; + +const LOGIN_LISTENER_ID = 'push-notifications-login'; + +/** + * Starts push for the logged-in user. Call it from React with `usePushOnLogin()` (below) + * rather than directly: it returns a cleanup that must run on logout, or every handler + * fires twice after the next login. + */ export function setupPushOnLogin(): () => void { - // Subscribe BEFORE init() so the tap or answered call that LAUNCHED the app - // is delivered to you rather than dropped. + // Subscribe BEFORE init(): the tap or answered call that LAUNCHED the app is delivered + // as soon as init() runs. const unsubscribes = [ - CometChatPushNotifications.onNotificationTap(handleTap), - CometChatPushNotifications.onCallAccepted(info => - navigate('OngoingCall', { sessionId: info.sessionId, callType: info.callType })), - CometChatPushNotifications.onCallEnded(handleCallEnded), - CometChatPushNotifications.onMessageReceived(data => - console.log('data push:', data)), + CometChatPushNotifications.onNotificationTap(openFromNotification), + CometChatPushNotifications.onCallAccepted(openCallScreen), + CometChatPushNotifications.onCallEnded(endCall), ]; const start = async () => { - // Await each permission request before the next: Android allows only ONE pending - // request per activity, and a second one cancels the dialog still on screen. - // This rejects when the OS could not be asked at all — which is not the user - // declining, and not a reason to skip init(): the token still has to register. + // Await each permission request before the next — Android allows only one pending + // request per activity. A rejection means the OS could not be asked (not that the user + // declined), and must not stop init(): the push token still has to register. await CometChatPNHelper.requestNotificationPermission().catch(() => false); - await CometChatPNHelper.requestCallPermissions(); // mic + camera (Android) before a call connects + await CometChatPNHelper.requestCallPermissions(); // mic + camera, needed before a call connects await CometChatPushNotifications.init({ - fcmProviderId: AppCredentials.fcmProviderId, // Android + fcmProviderId: AppCredentials.fcmProviderId, // Android apnsProviderId: AppCredentials.apnsProviderId, // iOS (APNs device + VoIP) - // Android status-bar icon (a monochrome drawable); without it the launcher icon - // is used, which Android renders as a solid square. - notificationSmallIcon: 'ic_notification', - // Show one chat notification while the app is open too. Set false if your - // in-app UI already surfaces new messages. Calls are unaffected: a foreground - // call never shows CallKit / the system ring, only your in-app call UI. - showInForeground: true, + notificationSmallIcon: 'ic_notification', // Android status-bar icon (step 2) + showInForeground: true, // one notification while the app is open, too }); }; - start().catch(e => console.log('push setup failed', e)); + start().catch(error => console.log('Push setup failed:', error)); - return () => unsubscribes.forEach(unsub => unsub()); + return () => unsubscribes.forEach(unsubscribe => unsubscribe()); } -``` -`init` wires the native events to the Chat SDK, **auto-registers** the device tokens, and drains any cold-start tap/call the app was launched from. It is safe to call again on re-login. +/** + * Runs push while a user is logged in — after a fresh login AND after a session restored + * on launch — and cleans up on logout. Use it once, in a component rendered after + * CometChat has been initialized. + */ +export function usePushOnLogin(): void { + const [loggedIn, setLoggedIn] = useState(false); + + useEffect(() => { + // A restored session never fires loginSuccess, so check once on mount. + CometChat.getLoggedinUser() + .then(user => setLoggedIn(!!user)) + .catch(() => setLoggedIn(false)); + + CometChat.addLoginListener( + LOGIN_LISTENER_ID, + new CometChat.LoginListener({ + loginSuccess: () => setLoggedIn(true), + logoutSuccess: () => setLoggedIn(false), + }), + ); + return () => CometChat.removeLoginListener(LOGIN_LISTENER_ID); + }, []); + + useEffect(() => { + if (!loggedIn) return; + return setupPushOnLogin(); + }, [loggedIn]); +} -`init` also accepts: `voip` (default `true`), `androidChannelId`, and `androidChannelName`. `showInForeground` defaults to `false` and affects **chat notifications only**. +/** Open the thread for a thread reply, otherwise the conversation. */ +async function openFromNotification(info: PNNotificationTapInfo): Promise { + const isGroup = info.receiverType === 'group'; + try { + const user = !isGroup && info.sender ? await CometChat.getUser(info.sender) : undefined; + const group = isGroup && info.receiver ? await CometChat.getGroup(info.receiver) : undefined; + if (!user && !group) return; + + markConversationRead(isGroup ? info.receiver! : info.sender!, isGroup); + + if (info.parentMessageId) { + try { + const parent = await CometChat.getMessageDetails(info.parentMessageId); + // The thread screen needs the user or group, not just the parent message. + await navigate(SCREENS.thread, { message: parent, user, group, highlightMessageId: info.messageId }); + return; + } catch (error) { + console.log('Could not open the thread, opening the conversation:', error); + } + } + await navigate(SCREENS.messages, { user, group }); + } catch (error) { + console.log('Could not open the conversation from a notification:', error); + } +} -## 4. Request permission and register tokens +/** Mark the conversation read and clear its unread badge in the UI Kit's conversation list. */ +function markConversationRead(conversationWith: string, isGroup: boolean): void { + const type = isGroup ? CometChat.RECEIVER_TYPE.GROUP : CometChat.RECEIVER_TYPE.USER; + CometChat.markConversationAsRead(conversationWith, type) + .then(() => CometChat.getConversation(conversationWith, type)) + .then(conversation => { + const lastMessage = conversation.getLastMessage(); + if (lastMessage) { + CometChatUIEventHandler.emitMessageEvent(MessageEvents.ccMessageRead, { message: lastMessage }); + } + }) + .catch(error => console.log('Could not mark the conversation read:', error)); +} -Permission is requested via `CometChatPNHelper` (call it before pushes/calls arrive): +/** The package has already accepted the call — just show the call screen. */ +function openCallScreen(info: PNCallInfo): void { + navigate(SCREENS.ongoingCall, { sessionId: info.sessionId, callType: info.callType }); +} -```ts lines -await CometChatPNHelper.requestNotificationPermission(); // POST_NOTIFICATIONS (Android 13+) / iOS -await CometChatPNHelper.requestCallPermissions(); // mic + camera (Android 14+ call FGS) -// non-prompting check: -const enabled = await CometChatPNHelper.hasNotificationPermission(); +/** + * A ringing call was cancelled or declined, or the user ended the call from the iOS call + * screen — which the Calls SDK does not see, so tear the call down here. + */ +function endCall(info: PNCallEndEvent): void { + if (info.sessionId) CometChat.endCall(info.sessionId).catch(() => {}); + try { + CometChatCalls.endSession(); + } catch {} + try { + CometChat.clearActiveCall(); + } catch {} + if (navigationRef.isReady() && navigationRef.getCurrentRoute()?.name === SCREENS.ongoingCall) { + navigate(SCREENS.home); + } +} ``` - -**Always `await` one permission request before starting the next.** Android allows only -one pending request per activity: a second request cancels the dialog still on screen and -the OS reports it as denied without the user ever seeing it. On a fresh install that leaves -the app with no notification permission — pushes arrive and are silently dropped. - + +Not using the UI Kit? Delete the `@cometchat/chat-uikit-react-native` import and the `emitMessageEvent` block in `markConversationRead`, and point `SCREENS` and the route params at your own screens. In `logout.ts` (step 5), call `CometChat.logout()` instead of `CometChatUIKit.logout()`. + -`requestNotificationPermission()` resolves `true`/`false` on the user's answer. It **rejects** -when the permission could not be requested at all, which is a different situation from the -user declining and should not stop the rest of your setup — the token still registers: +**`App.tsx`** — call `usePushOnLogin()` once, in a component that renders **after** CometChat is initialized, and pass `navigationRef` to your `NavigationContainer`: -| Rejection | Meaning | -| --- | --- | -| `ERR_NO_ACTIVITY` | No foreground activity, so no dialog can be shown. Retry when the app is foreground. | -| `ERR_PERMISSION_IN_FLIGHT` | A request is already open. Await that one instead. | -| `ERR_ACTIVITY_NOT_PERMISSION_AWARE` | Your host activity does not extend `ReactActivity`. Fix the activity. | +```tsx App.tsx lines +import React, { useEffect, useState } from 'react'; +import { NavigationContainer } from '@react-navigation/native'; +import { CometChat } from '@cometchat/chat-sdk-react-native'; +import { CometChatUIKit, UIKitSettings } from '@cometchat/chat-uikit-react-native'; + +import { AppCredentials } from './AppCredentials'; +import { navigationRef } from './navigation/navigationRef'; +import { usePushOnLogin } from './push/pushNotifications'; + +export default function App() { + const [initialized, setInitialized] = useState(false); + + useEffect(() => { + // Your existing CometChat initialization. + CometChatUIKit.init({ + appId: AppCredentials.appId, + region: AppCredentials.region, + authKey: AppCredentials.authKey, + subscriptionType: CometChat.AppSettings.SUBSCRIPTION_TYPE_ALL_USERS as UIKitSettings['subscriptionType'], + } as UIKitSettings) + .then(() => setInitialized(true)) + .catch(error => console.log('CometChat init failed:', error)); + }, []); + + // Push must start only after CometChat is initialized. + if (!initialized) return null; + return ; +} -**Token registration is automatic** — `init()` registers the FCM token (Android) and the APNs device + VoIP tokens (iOS) with CometChat, and re-registers on refresh. You rarely need to do it by hand, but you can: +function Root() { + usePushOnLogin(); // push follows login and logout from here on -```ts lines -await CometChatPushNotifications.registerToken('fcm', token); // or 'apns' / 'voip' + return ( + + {/* your existing navigator */} + + ); +} ``` -## 5. Unregister the token on logout - -Two separate things have to happen on logout: the **token** must be unregistered so the device -stops receiving that user's notifications, and your **handlers** must be unsubscribed. +`usePushOnLogin()` starts push after a fresh login **and** when a session is restored on launch, and removes the handlers when the user logs out — so a later login never registers them twice. `init()` registers the device tokens (and re-registers them when they refresh) and delivers the tap or answered call the app was launched from. -Let React own the handlers. `setupPushOnLogin()` returns its cleanup, so returning it from a -`useEffect` keyed on the logged-in state removes the handlers on logout and on unmount: +`init()` also accepts `voip` (default `true`), `androidChannelId`, and `androidChannelName`. `showInForeground` defaults to `false` and affects chat notifications only — a foreground call always rings in your in-app UI. -```tsx lines -// App.tsx -useEffect(() => { - if (!isLoggedIn) return; - return setupPushOnLogin(); // the cleanup runs on logout / unmount -}, [isLoggedIn]); -``` +## 4. Request permission and register tokens -Unregister the token in your logout handler, **before** `logout()`: +`setupPushOnLogin()` already requests both permissions, in order, before `init()`. To check or request them elsewhere: ```ts lines -// in your logout button / handler -try { - await CometChatPushNotifications.unregister(); -} catch (error) { - console.error('Failed to unregister push token:', error); - return; // logging out anyway would leave the device receiving this user's push -} -await CometChat.logout(); +const granted = await CometChatPNHelper.requestNotificationPermission().catch(() => false); // Android 13+ / iOS +await CometChatPNHelper.requestCallPermissions(); // mic + camera +const enabled = await CometChatPNHelper.hasNotificationPermission(); // checks without prompting ``` -`unregister()` must run **before** `logout()`. It calls the Chat SDK's `unregisterPushToken()`, -which needs the session's auth token — after logout it fails, and the device keeps receiving -notifications for the user who just logged out. +**Always `await` one permission request before starting the next.** Android allows only one pending request per activity: a second request cancels the dialog still on screen, and the OS reports it as denied without the user seeing it. On a fresh install that leaves the app with no notification permission — pushes arrive and are dropped. - -Unlike the other platforms, React Native needs the unsubscribe step too: the package keeps your callbacks in a set that survives `unregister()`, so a logout → login cycle would register a second copy of each handler and fire it twice (e.g. navigating to a tapped message twice). - +`requestNotificationPermission()` resolves `true`/`false` on the user's answer. It **rejects** when the permission could not be requested at all — a different situation from the user declining, and not a reason to skip `init()`: -## 6. Notification taps and call events - -Subscribe once (in `setupPushOnLogin` above). Each subscribe returns an unsubscribe function. - -**Notification tap** — open the conversation, or the **thread** when the push is a thread reply. +| Rejection | Meaning | +| --- | --- | +| `ERR_NO_ACTIVITY` | No foreground activity, so no dialog can be shown. Retry when the app is foreground. | +| `ERR_PERMISSION_IN_FLIGHT` | A request is already open. Await that one instead. | +| `ERR_ACTIVITY_NOT_PERMISSION_AWARE` | Your host activity does not extend `ReactActivity`. Fix the activity. | -When a tap **launches a killed app**, it is delivered as soon as `init()` subscribes — which can be before your `NavigationContainer` has mounted. Wait for the navigator first, or the navigation is silently dropped and the app stays on its first screen: +**Token registration is automatic.** You rarely need it, but you can register a token yourself: ```ts lines -import { navigationRef } from './NavigationService'; // createNavigationContainerRef() +await CometChatPushNotifications.registerToken('fcm', token); // or 'apns' / 'voip' +``` -// React Navigation's documented readiness check. The ref queues listeners added -// before the container mounts, so this also works on a cold start. -function whenNavigationReady() { - if (navigationRef.isReady()) return Promise.resolve(); - return new Promise(resolve => { - const unsubscribe = navigationRef.addListener('ready', () => { - unsubscribe(); - resolve(); - }); - }); -} +## 5. Unregister the token on logout -async function handleTap(info) { - // info: { receiverType, sender, receiver, conversationId, messageId, parentMessageId, senderName } - await whenNavigationReady(); - if (info.parentMessageId) { - const parent = await CometChat.getMessageDetails(info.parentMessageId); - navigate('ThreadView', { message: parent, highlightMessageId: info.messageId }); - return; +Add `src/push/logout.ts` and call it from your logout button instead of logging out directly: + +```ts src/push/logout.ts lines +import { CometChatUIKit } from '@cometchat/chat-uikit-react-native'; +import { CometChatPushNotifications } from '@cometchat/push-notifications-react-native'; + +/** Log out and stop this device receiving the user's notifications. Resolves false on failure. */ +export async function logout(): Promise { + // Unregister BEFORE logout: it needs the session's auth token, so after logout it fails + // and the device keeps receiving notifications for the user who just logged out. + try { + await CometChatPushNotifications.unregister(); + } catch (error) { + console.log('Failed to unregister the push token:', error); + return false; + } + try { + await CometChatUIKit.logout(); + return true; + } catch (error) { + console.log('Logout failed:', error); + return false; } - navigateToConversation({ - receiverType: info.receiverType, - sender: info.sender, - conversationId: info.conversationId, - }); } ``` -**Call accepted** — the package has *already* accepted the call via the Chat SDK; just open your call screen: - -```ts lines -CometChatPushNotifications.onCallAccepted(info => { - navigate('OngoingCall', { sessionId: info.sessionId, callType: info.callType }); -}); +```tsx lines +const onLogoutPress = async () => { + if (loggingOut) return; // ignore a second tap while logging out + setLoggingOut(true); + const loggedOut = await logout(); + setLoggingOut(false); + if (loggedOut) navigation.navigate('Login'); // your login screen +}; ``` -**Call ended** — a ringing call was cancelled/declined/ended, **or** the user ended the call from the iOS CallKit UI. The Calls SDK's own listener doesn't see a CallKit-initiated end, so tear the call down here: + +`unregister()` must run **before** logout. It needs the session's auth token — after logout it fails, and the device keeps receiving notifications for the user who just logged out. + -```ts lines -import { CometChatCalls } from '@cometchat/calls-sdk-react-native'; +When logout completes, `usePushOnLogin()` removes the push handlers for you. -function handleCallEnded(info) { - if (info.sessionId) CometChat.endCall(info.sessionId).catch(() => {}); - try { CometChatCalls.endSession(); } catch {} - CometChat.clearActiveCall?.(); - // leave the ongoing-call screen if you're on it -} -``` +## 6. Notification taps and call events + +`src/push/pushNotifications.ts` from step 3 handles all of these. What each one does, and why: + +| Event | What happens | +| --- | --- | +| **Notification tap** | Waits for the navigator, marks the conversation read (clearing its unread badge), then opens the **thread** for a thread reply — with the parent message *and* the user or group, which the thread screen needs — or the conversation. If the thread can't be opened, it falls back to the conversation. | +| **Call accepted** | The package has **already** accepted the call with the Chat SDK; the app only opens its call screen. | +| **Call ended** | A ringing call was cancelled or declined, or the user ended the call from the iOS call screen. The Calls SDK doesn't see a call ended there, so the handler ends it on the server, ends the media session, and leaves the call screen. | +| **Launched by a tap or an answered call** | Delivered as soon as `init()` runs — before the navigator mounts — which is why the handlers are subscribed before `init()` and every navigation waits for `whenNavigationReady()`. | -**Cold-start VoIP handling (iOS):** when the app is killed and a VoIP push arrives, the package presents CallKit natively via PushKit before React Native is ready. When the user answers, the app cold-starts, `init()` replays the accepted call, `onCallAccepted` fires, and the package has already called `CometChat.acceptCall` — set up your call session/screen there. (This path is why the `PKPushRegistry` must be created on a background queue — see step 2.) +**Killed-app VoIP (iOS):** when a VoIP push wakes a killed app, the package reports the call to CallKit immediately, before React Native is ready. When the user answers, the app starts, `init()` delivers the answered call, and `onCallAccepted` opens your call screen — the call is already accepted. This is why the `PKPushRegistry` is created on a background queue, before React Native starts (step 2). ## 7. Android: OEM permissions for lock-screen calls @@ -478,37 +649,42 @@ CometChat's Enhanced Push Notification payload includes an `unreadMessageCount` ## 9. 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: exactly **one** system notification (with `showInForeground: true`), or none with `false`. - - Background: a notification appears; tapping opens the right conversation via `onNotificationTap` (and the thread, for a thread reply). -3. Force-quit the app, send another message, tap the notification, and confirm it cold-starts 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. - - **Accept** joins the call (audio works both ways) and the screen tears down when the call ends. - - **Decline** rejects the call promptly on the caller side — including from a killed state. - - **Caller cancels** while it's ringing → the callee ring dismisses. -5. Uninstall and reinstall, then log in: on Android 13+ the notification prompt must **wait** for your answer before the microphone/camera prompt appears, and pushes must arrive on that first install. -6. Log out and send a message from another user — the device must receive nothing. -7. On an OEM device (MIUI/Oppo/Vivo), grant the section-7 permissions and re-check locked/killed calls. +Use physical devices. The iOS Simulator can't receive APNs or VoIP pushes, and emulators often delay FCM. Test call timing on a **release** build: a debug build loads its JavaScript from Metro, which delays the first JavaScript that runs in a killed app. + +1. **Fresh install:** install, log in, and confirm the notification prompt **waits** for your answer before the microphone/camera prompt appears. Then send a message from another user — it must arrive. +2. **Chat notifications:** + - App open: exactly **one** notification (`showInForeground: true`). + - App in the background: a notification appears; tapping it opens the conversation. + - App killed: tapping the notification starts the app **in** the conversation. + - A thread reply opens the **thread**; a group message opens the group. +3. **Calls, app killed, device locked:** + - Android shows the **full-screen ringing screen** with Accept/Decline; iOS shows CallKit. + - **Accept** connects the call with audio both ways. + - **Decline** shows the call as rejected on the caller's side. + - The caller **cancelling** stops the ring. +4. **Calls, app in the background:** accept and decline both work, and ending the call from the iOS call screen closes your call screen. +5. **Logout:** log out, send a message from another user — nothing arrives. Log in as another user — only that user's notifications arrive, and a tap opens its conversation once. +6. **OEM devices** (MIUI, Oppo, Vivo): grant the section 7 permissions and re-check locked and killed calls. ## 10. Troubleshooting | Symptom | Platform | Quick checks | | --- | --- | --- | -| No notifications received | Android | Confirm `google-services.json` is in `android/app/`, the package name matches Firebase, the `google-services` plugin is applied, and `POST_NOTIFICATIONS` is granted (Android 13+). | -| Killed app doesn't ring for a VoIP push | iOS | Ensure the `PKPushRegistry` is created in `didFinishLaunchingWithOptions` on a **background queue**, `didReceiveIncomingPushWith` forwards to `CometChatPushNotificationsAppDelegate.didReceiveIncomingVoIPPush`, and nothing else creates a `PKPushRegistry` (step 2). | -| Accepted call connects but has no audio | iOS | Confirm the `audio` background mode is enabled (Background Modes → Audio, step 2) and `react-native-webrtc` (via the Calls SDK) is linked in the app — the package coordinates CallKit's audio session with WebRTC automatically. | -| Full-screen call UI not showing on lock screen | Android | OEM gate — grant Autostart / "Display pop-up while running in background" / "Show on lock screen" and disable battery optimization (section 7). | -| Declining a killed-state call doesn't reject the caller | Both | Android: ensure `registerBackgroundCallTask` is registered at module scope in `index.js`. iOS: the package handles it via CallKit — verify the `AppDelegate` forwards `didReceiveIncomingVoIPPush` (step 2). | -| Two notifications for one chat message | Android | Another FCM receiver is still installed — usually `@react-native-firebase/messaging`, whose broadcast receiver handles every push on its own. Remove the legacy push libraries so the package's service is the only handler. | -| Notifications work on an existing install but not on a fresh one | Android | The permission dialog was cancelled before the user could answer it. `await` `requestNotificationPermission()` before requesting any other permission — Android allows one pending request per activity (step 4). Check with `adb shell dumpsys package \| grep POST_NOTIFICATIONS`: `granted=false` with no `USER_SET` flag means the dialog was never answered. | -| Notifications still arrive after logout | Both | `unregister()` must run **before** `logout()` — it needs the session's auth token (step 5). Check that its rejection is not being swallowed. | -| No notification while the app is open | Both | Expected with `showInForeground: false` (the default). Set it to `true` to show one. It affects chat notifications only; a foreground call always uses your in-app UI. | -| Tapping a notification when the app was killed opens the app but not the conversation | Both | Wait for the navigator before navigating (`whenNavigationReady()`, step 6) — the tap is delivered before `NavigationContainer` mounts. | -| Duplicate navigation after re-login | Both | Return `setupPushOnLogin()`'s cleanup from a `useEffect` keyed on the logged-in state so the handlers are removed on logout (step 5). | -| No VoIP pushes | iOS | Ensure Push Notifications + Background Modes (Voice over IP) are enabled, `aps-environment` is correct (`production` for release), the bundle ID matches the CometChat APNs VoIP provider, and the VoIP cert is uploaded to the dashboard. | -| Token registration errors | Both | Verify the provider IDs match the dashboard exactly and that `init()` runs **after** login. | +| No notifications received | Android | `google-services.json` is in `android/app/`, its package name matches the app, the `google-services` plugin is applied, and `POST_NOTIFICATIONS` is granted (Android 13+). | +| Notifications work on an existing install but not on a fresh one | Android | A permission request wasn't awaited, so the notification dialog was cancelled unseen (step 4). `adb shell dumpsys package \| grep POST_NOTIFICATIONS`: `granted=false` with no `USER_SET` flag means the dialog was never answered. | +| Two notifications for one message | Android | Another push library is still installed — usually `@react-native-firebase/messaging`, whose receiver handles every push too. Remove the other push libraries (step 2). | +| Incoming call is a plain notification, not the full-screen ringing screen | Android | Check the merged manifest still has `MANAGE_OWN_CALLS` and `FOREGROUND_SERVICE_PHONE_CALL` (nothing strips them with `tools:node="remove"`). On MIUI, Oppo and Vivo, grant the section 7 permissions. For Google Play builds, complete the **full-screen intent** declaration in Play Console — without it, Play revokes `USE_FULL_SCREEN_INTENT` on Android 14+. | +| Declining a call in a killed app doesn't reject it | Android | `registerBackgroundCallTask()` is called at module scope in `index.js`, and the app has been opened and logged in once since installing (so `init()` saved the Chat SDK settings). In a debug build the reject can take ~20 s — test on a release build. | +| Killed app doesn't ring for a VoIP push | iOS | The `PKPushRegistry` is created in `didFinishLaunchingWithOptions`, on a background queue, **before** React Native starts, and nothing else creates one (step 2). | +| iOS build fails with `'hermes/hermes.h' file not found` | iOS | The Podfile platform was lowered below React Native's minimum. Restore `platform :ios, min_ios_version_supported` and run `pod install`. | +| Accepted call connects but has no audio | iOS | The **Audio** background mode is enabled and the Calls SDK (with `react-native-webrtc`) is installed. | +| No VoIP pushes | iOS | Push Notifications + Background Modes (Voice over IP) are enabled, `aps-environment` matches the build (`production` for release), and the bundle ID matches the CometChat APNs provider. | +| No notification while the app is open | Both | Expected with `showInForeground: false` (the default). Set it to `true`. Calls always ring in your in-app UI while the app is open. | +| Tapping a notification opens the app but not the conversation | Both | `navigationRef` is passed to your `NavigationContainer`, and navigation goes through `navigate()` from `navigationRef.ts`, which waits for the navigator. The route names in `SCREENS` match your navigator. | +| Thread reply opens an empty thread screen | Both | The thread screen is given the user or group as well as the parent message (step 3 does this). | +| Handlers fire twice after logging out and in | Both | Use `usePushOnLogin()` rather than calling `setupPushOnLogin()` directly — its cleanup must run on logout. | +| Notifications still arrive after logout | Both | `unregister()` runs **before** logout and its failure isn't ignored (step 5). | +| Token registration errors | Both | The provider IDs match the dashboard exactly, and `usePushOnLogin()` is rendered after CometChat is initialized. | ## Resources From 516ba2474c15c9daeab68a8021432f41bc4c3c6a Mon Sep 17 00:00:00 2001 From: Suraj Chauhan Date: Thu, 17 Sep 2026 16:52:43 +0530 Subject: [PATCH 12/20] docs(react-native): document ringInForeground The package now rings with CallKit / the Android ringing screen even while the app is open (ringInForeground, default true). The guide's code sets it to false, because the UI Kit shows its own incoming-call screen; the config reference, the init notes and the troubleshooting row explain both settings. Co-Authored-By: Claude Opus 5 --- notifications/react-native-push-notifications.mdx | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/notifications/react-native-push-notifications.mdx b/notifications/react-native-push-notifications.mdx index 3613d00d8..bb42a4846 100644 --- a/notifications/react-native-push-notifications.mdx +++ b/notifications/react-native-push-notifications.mdx @@ -17,7 +17,7 @@ description: "Add CometChat push notifications and VoIP calls to a React Native | iOS AppDelegate | Swift only. Conform to `PKPushRegistryDelegate`. In `didFinishLaunchingWithOptions`, create **one** `PKPushRegistry` on a **background** queue with `desiredPushTypes = [.voIP]` **before** starting React Native. Forward to `CometChatPushNotificationsAppDelegate`: `.didRegisterAPNsToken(_:)`, `.didReceiveRemoteNotification(_:)`, `.didUpdateVoIPToken(_:)`, `.didReceiveIncomingVoIPPush(_:)` then `completion()`. Don't set a `UNUserNotificationCenter` delegate — the package installs one. | | index.js | `registerBackgroundCallTask();` at module scope — the package rejects a call declined while the Android app is killed. Optional `registerBackgroundCallTask(handler, { rejectDeclinedCalls })`: the handler runs **after** the package's reject; `rejectDeclinedCalls: false` hands the reject to the handler. No-op on iOS. | | Initialize | Call `usePushOnLogin()` once in a component rendered **after** `CometChat.init` resolves. It runs `setupPushOnLogin()` after a fresh login **and** a restored session, and its cleanup on logout. `setupPushOnLogin()` subscribes the handlers **before** `init()`, requests permissions in order, then `await CometChatPushNotifications.init(config)`. | -| Config (`PNConfiguration`) | `fcmProviderId?`, `apnsProviderId?`, `notificationSmallIcon?`, `androidChannelId?` (default `"cometchat_messages"`), `androidChannelName?` (default `"Messages"`), `voip?` (default `true`), `showInForeground?` (default `false`; chat notifications only) | +| Config (`PNConfiguration`) | `fcmProviderId?`, `apnsProviderId?`, `notificationSmallIcon?`, `androidChannelId?` (default `"cometchat_messages"`), `androidChannelName?` (default `"Messages"`), `voip?` (default `true`), `showInForeground?` (default `false`; chat notifications only), `ringInForeground?` (default `true`: calls ring with CallKit / the Android ringing screen even while the app is open; set `false` when the app shows its own incoming-call screen, as the UI Kit does) | | Permissions | `CometChatPNHelper.requestNotificationPermission(): Promise` (resolves on the user's answer; **rejects** with `ERR_NO_ACTIVITY` / `ERR_PERMISSION_IN_FLIGHT` / `ERR_ACTIVITY_NOT_PERMISSION_AWARE` when it could not ask — catch it, don't skip `init()`) · `.hasNotificationPermission()` · `.requestCallPermissions()` (mic + camera). Await each before the next. | | Token registration | Automatic in `init()`, including refreshes. Manual: `CometChatPushNotifications.registerToken(platform, token)`, `PNTokenPlatform` = `"fcm"` \| `"apns"` \| `"voip"` | | Events (each returns `Unsubscribe`) | `onNotificationTap(cb)` · `onCallAccepted(cb)` (call already accepted) · `onCallEnded(cb)` · `onMessageReceived(cb)` | @@ -368,6 +368,7 @@ export function setupPushOnLogin(): () => void { apnsProviderId: AppCredentials.apnsProviderId, // iOS (APNs device + VoIP) notificationSmallIcon: 'ic_notification', // Android status-bar icon (step 2) showInForeground: true, // one notification while the app is open, too + ringInForeground: false, // the UI Kit shows its own incoming-call screen while open }); }; start().catch(error => console.log('Push setup failed:', error)); @@ -517,7 +518,7 @@ function Root() { `usePushOnLogin()` starts push after a fresh login **and** when a session is restored on launch, and removes the handlers when the user logs out — so a later login never registers them twice. `init()` registers the device tokens (and re-registers them when they refresh) and delivers the tap or answered call the app was launched from. -`init()` also accepts `voip` (default `true`), `androidChannelId`, and `androidChannelName`. `showInForeground` defaults to `false` and affects chat notifications only — a foreground call always rings in your in-app UI. +`init()` also accepts `voip` (default `true`), `androidChannelId`, and `androidChannelName`. `showInForeground` defaults to `false` and affects chat notifications only. `ringInForeground` decides how a call rings while the app is open: `true` (the default) rings with CallKit on iOS and the ringing screen on Android; `false`, used here, leaves it to the UI Kit's own incoming-call screen so the call doesn't ring twice. ## 4. Request permission and register tokens @@ -679,7 +680,7 @@ Use physical devices. The iOS Simulator can't receive APNs or VoIP pushes, and e | iOS build fails with `'hermes/hermes.h' file not found` | iOS | The Podfile platform was lowered below React Native's minimum. Restore `platform :ios, min_ios_version_supported` and run `pod install`. | | Accepted call connects but has no audio | iOS | The **Audio** background mode is enabled and the Calls SDK (with `react-native-webrtc`) is installed. | | No VoIP pushes | iOS | Push Notifications + Background Modes (Voice over IP) are enabled, `aps-environment` matches the build (`production` for release), and the bundle ID matches the CometChat APNs provider. | -| No notification while the app is open | Both | Expected with `showInForeground: false` (the default). Set it to `true`. Calls always ring in your in-app UI while the app is open. | +| No notification while the app is open | Both | Expected with `showInForeground: false` (the default). Set it to `true`. For calls, `ringInForeground` decides whether the system call UI or your in-app screen rings. | | Tapping a notification opens the app but not the conversation | Both | `navigationRef` is passed to your `NavigationContainer`, and navigation goes through `navigate()` from `navigationRef.ts`, which waits for the navigator. The route names in `SCREENS` match your navigator. | | Thread reply opens an empty thread screen | Both | The thread screen is given the user or group as well as the parent message (step 3 does this). | | Handlers fire twice after logging out and in | Both | Use `usePushOnLogin()` rather than calling `setupPushOnLogin()` directly — its cleanup must run on logout. | From fb217de2e636bf4090f746ac34eec17c50f55274 Mon Sep 17 00:00:00 2001 From: Suraj Chauhan Date: Thu, 17 Sep 2026 17:58:58 +0530 Subject: [PATCH 13/20] =?UTF-8?q?docs(react-native):=20the=20package=20own?= =?UTF-8?q?s=20PushKit=20=E2=80=94=20registerForVoIPPushes()?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The iOS AppDelegate no longer creates a PKPushRegistry or forwards PushKit callbacks. It calls CometChatPushNotificationsAppDelegate.registerForVoIPPushes() before React Native starts and forwards the APNs token. The quick reference, the AppDelegate example, the PushKit warning (with the forwarding option for apps whose registry belongs to another library), the killed-app note and the troubleshooting row are updated. Co-Authored-By: Claude Opus 5 --- .../react-native-push-notifications.mdx | 53 +++++-------------- 1 file changed, 13 insertions(+), 40 deletions(-) diff --git a/notifications/react-native-push-notifications.mdx b/notifications/react-native-push-notifications.mdx index bb42a4846..74b57996f 100644 --- a/notifications/react-native-push-notifications.mdx +++ b/notifications/react-native-push-notifications.mdx @@ -14,7 +14,7 @@ description: "Add CometChat push notifications and VoIP calls to a React Native | Files you add | `src/AppCredentials.ts` (provider IDs) · `src/navigation/navigationRef.ts` (`navigationRef`, `whenNavigationReady`, `navigate`) · `src/push/pushNotifications.ts` (`setupPushOnLogin`, `usePushOnLogin`) · `src/push/logout.ts` | | Android setup | Apply the `com.google.gms.google-services` plugin (with `google-services.json` in `android/app/`); `minSdkVersion 24`+; add a monochrome `ic_notification` drawable. **No manifest changes** — the package declares its services, activities and permissions. Do **not** add `firebase-messaging` (the package brings it). Remove other push libraries (`@react-native-firebase/messaging`, `notifee`, `react-native-callkeep`, `react-native-voip-push-notification`). | | iOS setup | Keep the Podfile's `platform :ios, min_ios_version_supported` (never lower it); `pod install`. Enable **Push Notifications** + **Background Modes** (*Voice over IP*, *Remote notifications*, *Audio*). Add `NSMicrophoneUsageDescription` + `NSCameraUsageDescription`. | -| iOS AppDelegate | Swift only. Conform to `PKPushRegistryDelegate`. In `didFinishLaunchingWithOptions`, create **one** `PKPushRegistry` on a **background** queue with `desiredPushTypes = [.voIP]` **before** starting React Native. Forward to `CometChatPushNotificationsAppDelegate`: `.didRegisterAPNsToken(_:)`, `.didReceiveRemoteNotification(_:)`, `.didUpdateVoIPToken(_:)`, `.didReceiveIncomingVoIPPush(_:)` then `completion()`. Don't set a `UNUserNotificationCenter` delegate — the package installs one. | +| iOS AppDelegate | Swift only. In `didFinishLaunchingWithOptions`, call `CometChatPushNotificationsAppDelegate.registerForVoIPPushes()` **before** starting React Native — the package owns the PushKit registry and reports calls to CallKit. Forward the APNs token with `.didRegisterAPNsToken(_:)`, and optionally background pushes with `.didReceiveRemoteNotification(_:)`. Don't create a `PKPushRegistry` or set a `UNUserNotificationCenter` delegate — the package owns both. | | index.js | `registerBackgroundCallTask();` at module scope — the package rejects a call declined while the Android app is killed. Optional `registerBackgroundCallTask(handler, { rejectDeclinedCalls })`: the handler runs **after** the package's reject; `rejectDeclinedCalls: false` hands the reject to the handler. No-op on iOS. | | Initialize | Call `usePushOnLogin()` once in a component rendered **after** `CometChat.init` resolves. It runs `setupPushOnLogin()` after a fresh login **and** a restored session, and its cleanup on logout. `setupPushOnLogin()` subscribes the handlers **before** `init()`, requests permissions in order, then `await CometChatPushNotifications.init(config)`. | | Config (`PNConfiguration`) | `fcmProviderId?`, `apnsProviderId?`, `notificationSmallIcon?`, `androidChannelId?` (default `"cometchat_messages"`), `androidChannelName?` (default `"Messages"`), `voip?` (default `true`), `showInForeground?` (default `false`; chat notifications only), `ringInForeground?` (default `true`: calls ring with CallKit / the Android ringing screen even while the app is open; set `false` when the app shows its own incoming-call screen, as the UI Kit does) | @@ -37,7 +37,7 @@ description: "Add CometChat push notifications and VoIP calls to a React Native ## What this guide covers - Adding the `@cometchat/push-notifications-react-native` package and initializing it. -- Platform wiring: Firebase/`google-services.json` on Android, capabilities + `AppDelegate` PushKit forwarding on iOS. +- Platform wiring: Firebase/`google-services.json` on Android, capabilities and a few `AppDelegate` lines on iOS. - Requesting permission and registering tokens (FCM on Android, APNs + VoIP on iOS) after login. - Receiving pushes and letting the package render chat notifications and full-screen / CallKit calls. - Handling notification taps (including thread deep-links), incoming-call navigation, and Android OEM permissions. @@ -50,7 +50,7 @@ The `@cometchat/push-notifications-react-native` package replaces the previous a ## How it works - **Android (FCM):** Firebase issues the registration token and delivers the CometChat payload as a data message. The package ships its **own** `FirebaseMessagingService`, so it receives the message and shows the notification or full-screen call itself — **you write no FCM handling code**. -- **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 package. Your `AppDelegate` forwards the tokens and incoming VoIP pushes to the package (step 2). +- **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 package. Your `AppDelegate` starts the package's PushKit registry and forwards the APNs token (step 2). - **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 package's role:** it retrieves the tokens, registers them with CometChat, parses payloads, drives the call UI, and calls the Chat SDK to accept/reject/end. It requires [`@cometchat/chat-sdk-react-native`](https://www.npmjs.com/package/@cometchat/chat-sdk-react-native) as a peer dependency — the one Chat SDK your app already uses, so there is no second SDK to version-align. @@ -148,15 +148,13 @@ cd ios && pod install && cd .. import React import React_RCTAppDelegate import ReactAppDependencyProvider - import PushKit import react_native_cometchat_push_notifications @main - class AppDelegate: UIResponder, UIApplicationDelegate, PKPushRegistryDelegate { + class AppDelegate: UIResponder, UIApplicationDelegate { var window: UIWindow? var reactNativeDelegate: ReactNativeDelegate? var reactNativeFactory: RCTReactNativeFactory? - private var pushRegistry: PKPushRegistry? func application( _ application: UIApplication, @@ -168,12 +166,10 @@ cd ios && pod install && cd .. reactNativeDelegate = delegate reactNativeFactory = factory - // Create the PushKit registry BEFORE starting React Native, on a BACKGROUND queue. - // When a call wakes a killed app, iOS terminates it unless the call is reported within - // ~5 seconds — the main thread is busy starting React Native and would miss that. - pushRegistry = PKPushRegistry(queue: DispatchQueue(label: "com.cometchat.voip.pushkit")) - pushRegistry?.delegate = self - pushRegistry?.desiredPushTypes = [.voIP] + // VoIP calls: the package creates and owns the PushKit registry. Call it BEFORE + // starting React Native — when a call wakes a killed app, iOS terminates the app + // unless the call reaches CallKit within ~5 seconds. + CometChatPushNotificationsAppDelegate.registerForVoIPPushes() window = UIWindow(frame: UIScreen.main.bounds) factory.startReactNative( @@ -192,6 +188,7 @@ cd ios && pod install && cd .. CometChatPushNotificationsAppDelegate.didRegisterAPNsToken(deviceToken) } + // Optional — background data pushes reach onMessageReceived func application( _ application: UIApplication, didReceiveRemoteNotification userInfo: [AnyHashable: Any], @@ -200,30 +197,6 @@ cd ios && pod install && cd .. CometChatPushNotificationsAppDelegate.didReceiveRemoteNotification(userInfo) completionHandler(.noData) } - - // VoIP token — calls - func pushRegistry( - _ registry: PKPushRegistry, - didUpdate pushCredentials: PKPushCredentials, - for type: PKPushType - ) { - guard type == .voIP else { return } - CometChatPushNotificationsAppDelegate.didUpdateVoIPToken(pushCredentials.token) - } - - // Incoming VoIP push — the package reports the call to CallKit before this returns - func pushRegistry( - _ registry: PKPushRegistry, - didReceiveIncomingPushWith payload: PKPushPayload, - for type: PKPushType, - completion: @escaping () -> Void - ) { - guard type == .voIP else { return completion() } - CometChatPushNotificationsAppDelegate.didReceiveIncomingVoIPPush(payload.dictionaryPayload) - completion() - } - - func pushRegistry(_ registry: PKPushRegistry, didInvalidatePushTokenFor type: PKPushType) {} } class ReactNativeDelegate: RCTDefaultReactNativeFactoryDelegate { @@ -244,11 +217,11 @@ cd ios && pod install && cd .. You don't set a `UNUserNotificationCenter` delegate — the package installs its own at launch to handle foreground notifications and taps. - **Create exactly one `PKPushRegistry`.** A second one — your own or a library's — makes iOS deliver every VoIP push twice. + **Don't create a `PKPushRegistry` of your own.** The package owns it, and a second registry — yours or a library's — makes iOS deliver every VoIP push twice. If another library must own PushKit, skip `registerForVoIPPushes()` and forward that registry's `didUpdate` and `didReceiveIncomingPushWith` callbacks to `CometChatPushNotificationsAppDelegate.didUpdateVoIPToken(_:)` and `.didReceiveIncomingVoIPPush(_:)`, calling `completion()` after it. - **Older Swift template** (an `RCTAppDelegate` subclass): add the same registry lines before `return super.application(...)`, which starts React Native, and the same `CometChatPushNotificationsAppDelegate` calls in your delegate methods. **Objective-C `AppDelegate.mm`:** the package's iOS entry points are Swift-only, so move the AppDelegate to Swift first — the [React Native Upgrade Helper](https://react-native-community.github.io/upgrade-helper/) shows the change. + **Older Swift template** (an `RCTAppDelegate` subclass): call `registerForVoIPPushes()` before `return super.application(...)`, which starts React Native, and add the same token method. **Objective-C `AppDelegate.mm`:** the package's iOS entry points are Swift-only, so move the AppDelegate to Swift first — the [React Native Upgrade Helper](https://react-native-community.github.io/upgrade-helper/) shows the change. @@ -604,7 +577,7 @@ When logout completes, `usePushOnLogin()` removes the push handlers for you. | **Launched by a tap or an answered call** | Delivered as soon as `init()` runs — before the navigator mounts — which is why the handlers are subscribed before `init()` and every navigation waits for `whenNavigationReady()`. | -**Killed-app VoIP (iOS):** when a VoIP push wakes a killed app, the package reports the call to CallKit immediately, before React Native is ready. When the user answers, the app starts, `init()` delivers the answered call, and `onCallAccepted` opens your call screen — the call is already accepted. This is why the `PKPushRegistry` is created on a background queue, before React Native starts (step 2). +**Killed-app VoIP (iOS):** when a VoIP push wakes a killed app, the package reports the call to CallKit immediately, before React Native is ready. When the user answers, the app starts, `init()` delivers the answered call, and `onCallAccepted` opens your call screen — the call is already accepted. This is why `registerForVoIPPushes()` runs before React Native starts (step 2). ## 7. Android: OEM permissions for lock-screen calls @@ -676,7 +649,7 @@ Use physical devices. The iOS Simulator can't receive APNs or VoIP pushes, and e | Two notifications for one message | Android | Another push library is still installed — usually `@react-native-firebase/messaging`, whose receiver handles every push too. Remove the other push libraries (step 2). | | Incoming call is a plain notification, not the full-screen ringing screen | Android | Check the merged manifest still has `MANAGE_OWN_CALLS` and `FOREGROUND_SERVICE_PHONE_CALL` (nothing strips them with `tools:node="remove"`). On MIUI, Oppo and Vivo, grant the section 7 permissions. For Google Play builds, complete the **full-screen intent** declaration in Play Console — without it, Play revokes `USE_FULL_SCREEN_INTENT` on Android 14+. | | Declining a call in a killed app doesn't reject it | Android | `registerBackgroundCallTask()` is called at module scope in `index.js`, and the app has been opened and logged in once since installing (so `init()` saved the Chat SDK settings). In a debug build the reject can take ~20 s — test on a release build. | -| Killed app doesn't ring for a VoIP push | iOS | The `PKPushRegistry` is created in `didFinishLaunchingWithOptions`, on a background queue, **before** React Native starts, and nothing else creates one (step 2). | +| Killed app doesn't ring for a VoIP push | iOS | `registerForVoIPPushes()` is called in `didFinishLaunchingWithOptions`, **before** React Native starts, and nothing else in the app creates a `PKPushRegistry` (step 2). | | iOS build fails with `'hermes/hermes.h' file not found` | iOS | The Podfile platform was lowered below React Native's minimum. Restore `platform :ios, min_ios_version_supported` and run `pod install`. | | Accepted call connects but has no audio | iOS | The **Audio** background mode is enabled and the Calls SDK (with `react-native-webrtc`) is installed. | | No VoIP pushes | iOS | Push Notifications + Background Modes (Voice over IP) are enabled, `aps-environment` matches the build (`production` for release), and the bundle ID matches the CometChat APNs provider. | From fb68482558cfabff1a85724144618eaaaaa4b36d Mon Sep 17 00:00:00 2001 From: Suraj Chauhan Date: Thu, 17 Sep 2026 18:30:34 +0530 Subject: [PATCH 14/20] docs(react-native): split the push guide into Android and iOS pages, matching Flutter MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The React Native guide is back to two pages — react-native-push-notifications-android and -ios, the URLs already live — and each follows its Flutter counterpart section for section: dashboard providers, platform credentials, local configuration, bringing in the package, the native layer, token registration and runtime events, badge count, testing and troubleshooting. The content is the push package's: installing it replaces Flutter's copied notification stack, and the native steps shrink to Gradle and a notification icon on Android and a few AppDelegate lines on iOS. Navigation, redirects and the React Native links in notifications.mdx, push-overview, the React Native calls overview and the HTML-stripping page are back to their main versions. Every code block comes from files compiled against a strict React Native + UI Kit project. Co-Authored-By: Claude Opus 5 --- calls/v4/react-native/overview.mdx | 2 +- docs.json | 13 +- notifications.mdx | 3 +- notifications/push-overview.mdx | 6 +- ...eact-native-push-notifications-android.mdx | 665 +++++++++++++++++ .../react-native-push-notifications-ios.mdx | 605 ++++++++++++++++ .../react-native-push-notifications.mdx | 680 ------------------ .../push-notification-html-stripping.mdx | 7 +- 8 files changed, 1288 insertions(+), 693 deletions(-) create mode 100644 notifications/react-native-push-notifications-android.mdx create mode 100644 notifications/react-native-push-notifications-ios.mdx delete mode 100644 notifications/react-native-push-notifications.mdx diff --git a/calls/v4/react-native/overview.mdx b/calls/v4/react-native/overview.mdx index 40ebb45fc..9c6f72264 100644 --- a/calls/v4/react-native/overview.mdx +++ b/calls/v4/react-native/overview.mdx @@ -109,7 +109,7 @@ Use this when you want: - **Calls not connecting:** Verify the Calls SDK is initialized after the Chat SDK and that both use the same App ID and Region - **No audio/video:** Check that camera and microphone permissions are granted on both Android and iOS - - **Push notifications not arriving:** Ensure push notification setup is complete — see the [Push Notifications](/notifications/react-native-push-notifications) guide + - **Push notifications not arriving:** Ensure push notification setup is complete — see the [Push Notifications](/notifications/react-native-push-notifications-android) guide - **iOS build fails:** Run `pod install` in the `ios` directory after adding the Calls SDK dependency - **Android minSdkVersion error:** Set `minSdkVersion` to 24 or higher in your `build.gradle` diff --git a/docs.json b/docs.json index 3de78f26c..035cebab9 100644 --- a/docs.json +++ b/docs.json @@ -6567,7 +6567,8 @@ "notifications/ios-fcm-push-notifications", "notifications/flutter-push-notifications-android", "notifications/flutter-push-notifications-ios", - "notifications/react-native-push-notifications", + "notifications/react-native-push-notifications-android", + "notifications/react-native-push-notifications-ios", "notifications/web-push-notifications" ] }, @@ -7186,7 +7187,7 @@ }, { "source": "/extensions/react-native-push-notifications", - "destination": "/notifications/react-native-push-notifications" + "destination": "/notifications/react-native-push-notifications-android" }, { "source": "/extensions/capacitor-cordova-ionic-push-notifications", @@ -7481,12 +7482,8 @@ "destination": "https://assets.cometchat.io/legacy-docs/notifications/push-notification-extension-legacy.html" }, { - "source": "/notifications/react-native-push-notifications-android", - "destination": "/notifications/react-native-push-notifications" - }, - { - "source": "/notifications/react-native-push-notifications-ios", - "destination": "/notifications/react-native-push-notifications" + "source": "/notifications/react-native-push-notifications", + "destination": "/notifications/react-native-push-notifications-android" }, { "source": "/rest-api/messages/list-reactions-with-a-specific-emojiunicodes", diff --git a/notifications.mdx b/notifications.mdx index e3ad91797..0eb31ff02 100644 --- a/notifications.mdx +++ b/notifications.mdx @@ -66,7 +66,8 @@ canonical: "https://cometchat.com/docs" } href="/notifications/flutter-push-notifications-android" horizontal /> } href="/notifications/flutter-push-notifications-ios" horizontal /> - } href="/notifications/react-native-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/push-overview.mdx b/notifications/push-overview.mdx index f780a7878..8616d813b 100644 --- a/notifications/push-overview.mdx +++ b/notifications/push-overview.mdx @@ -67,7 +67,11 @@ UI Kit implementation UI Kit implementation
-} href="/notifications/react-native-push-notifications"> +} href="/notifications/react-native-push-notifications-android"> +UI Kit implementation + + +} href="/notifications/react-native-push-notifications-ios"> UI Kit implementation diff --git a/notifications/react-native-push-notifications-android.mdx b/notifications/react-native-push-notifications-android.mdx new file mode 100644 index 000000000..c08432df1 --- /dev/null +++ b/notifications/react-native-push-notifications-android.mdx @@ -0,0 +1,665 @@ +--- +title: "React Native Push Notifications (Android)" +description: "CometChat push notifications and VoIP calls in React Native apps on Android using Firebase Cloud Messaging (FCM) and the @cometchat/push-notifications-react-native package." +--- + + + +| Field | Value | +| --- | --- | +| Platform | Android (FCM) | +| Package | `@cometchat/push-notifications-react-native` | +| Key APIs | `CometChatPushNotifications.init()`, `onNotificationTap()`, `onCallAccepted()`, `onCallEnded()`, `unregister()`, `registerBackgroundCallTask()`, `CometChatPNHelper.requestNotificationPermission()` | +| Push Platform | `FCM_REACT_NATIVE_ANDROID`, registered by `init()` with `fcmProviderId` | +| Native setup | `google-services.json` + Google Services plugin, `minSdkVersion 24`, an `ic_notification` drawable. No manifest or Kotlin changes | +| Prerequisites | CometChat initialized and the user logged in before `init()`, an FCM provider ID, a physical device for call tests | + + + + + Reference implementation of React Native UI Kit, FCM and Push Notification Setup. + + +## What this guide covers + +- CometChat dashboard setup (enable push, add FCM provider) with screenshots. +- Firebase + React Native wiring (credentials, the push package, the Google Services plugin). +- Wiring the package's notification and call handlers into your app. +- Native Android setup (Gradle, notification icon) — no manifest entries or Kotlin code to write. +- Token registration, notification/call handling, navigation, testing, and troubleshooting. +- App icon badge count using `unreadMessageCount` from the CometChat push payload. + +## 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 `CometChatPushNotifications.init()` runs after login, the package registers the token for the logged-in user, and CometChat sends pushes to FCM on your behalf. +- **The package's role:** Its own `FirebaseMessagingService` receives each push and shows the chat notification or the full-screen incoming call. Every CometChat action — registering the token, accepting or rejecting a call — runs in JavaScript through the Chat SDK your app already uses. +- **Flow:** Permission (Android 13+ `POST_NOTIFICATIONS`) → Firebase returns the FCM token → after login, `init()` registers it with `AppCredentials.fcmProviderId` → CometChat sends to FCM → FCM delivers to the device → the package shows the notification or call → your `onNotificationTap`, `onCallAccepted` and `onCallEnded` handlers navigate. + +## 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. + + + Firebase - Push Notifications + + +### 2.2 Local configuration file + +Create `src/AppCredentials.ts` with your app credentials and provider IDs. The same file serves the [iOS guide](/notifications/react-native-push-notifications-ios): + +```ts src/AppCredentials.ts lines +export const AppCredentials = { + appId: 'YOUR_APP_ID', + region: 'YOUR_REGION', + authKey: 'YOUR_AUTH_KEY', + + // Android — the FCM provider ID from the CometChat dashboard + fcmProviderId: 'FCM-PROVIDER-ID', + + // iOS — one APNs provider covers both the device token and the VoIP token + apnsProviderId: 'APNS-PROVIDER-ID', +}; +``` + +## 3. Bring the push package into React Native + +### 3.1 Install the package + +```bash +npm install @cometchat/push-notifications-react-native +``` + + +**Remove other push and call libraries first** — `@notifee/react-native`, `react-native-callkeep`, `react-native-voip-push-notification` — along with their code and native setup, or every notification or call arrives twice. If you keep `@react-native-firebase/messaging` for other features, follow step 4.5. + + +### 3.2 Wire the entry points + +**`index.js`** — register the package's background task at module scope. It lets a **fully killed** app reject a call declined from its notification: the package re-initializes the Chat SDK with the settings `init()` saved, and rejects the call. + +```js index.js lines +import { AppRegistry } from 'react-native'; +import { registerBackgroundCallTask } from '@cometchat/push-notifications-react-native'; +import App from './App'; +import { name as appName } from './app.json'; + +// Android: lets a FULLY KILLED app reject a call declined from its notification. The package +// does the work — this only registers its background task. (No-op on iOS.) +registerBackgroundCallTask(); + +AppRegistry.registerComponent(appName, () => App); +``` + +To add your own logic, pass a handler — it runs **after** the package has rejected the call: + +```js lines +registerBackgroundCallTask(async (action, info) => { + // Your logic, e.g. record the declined call. The Chat SDK is initialized and logged in here. + console.log('Declined call from', info.callerUid); +}); + +// Or reject it yourself instead: +// registerBackgroundCallTask(myHandler, { rejectDeclinedCalls: false }); +``` + +**`src/navigation/navigationRef.ts`** — a notification tap or answered call that **launched** the app arrives before your navigator exists, so every navigation waits for it: + +```ts src/navigation/navigationRef.ts lines +import { createNavigationContainerRef } from '@react-navigation/native'; + +/** Pass this to your . */ +export const navigationRef = createNavigationContainerRef(); + +/** + * Resolves once the NavigationContainer is mounted. A notification tap or answered call + * that LAUNCHED the app arrives before the navigator exists, and navigating then is + * silently dropped. The ref queues listeners added before it mounts. + */ +export function whenNavigationReady(): Promise { + if (navigationRef.isReady()) return Promise.resolve(); + return new Promise(resolve => { + const unsubscribe = navigationRef.addListener('ready', () => { + unsubscribe(); + resolve(); + }); + }); +} + +/** Navigate by route name once the navigator is ready. */ +export async function navigate(name: string, params?: object): Promise { + await whenNavigationReady(); + (navigationRef.navigate as (name: string, params?: object) => void)(name, params); +} +``` + +**`src/push/pushNotifications.ts`** — everything push does for the logged-in user: the tap, call-accepted and call-ended handlers, the permission requests, and `init()`: + +```ts src/push/pushNotifications.ts lines +import { useEffect, useState } from 'react'; +import { CometChat } from '@cometchat/chat-sdk-react-native'; +import { CometChatCalls } from '@cometchat/calls-sdk-react-native'; +import { CometChatUIEventHandler, MessageEvents } from '@cometchat/chat-uikit-react-native'; +import { + CometChatPNHelper, + CometChatPushNotifications, + type PNCallEndEvent, + type PNCallInfo, + type PNNotificationTapInfo, +} from '@cometchat/push-notifications-react-native'; + +import { AppCredentials } from '../AppCredentials'; +import { navigate, navigationRef } from '../navigation/navigationRef'; + +/** Your navigator's route names — these are the CometChat UI Kit sample app's. */ +const SCREENS = { + messages: 'Messages', + thread: 'ThreadView', + ongoingCall: 'OngoingCallScreen', + home: 'BottomTabNavigator', +} as const; + +const LOGIN_LISTENER_ID = 'push-notifications-login'; + +/** + * Starts push for the logged-in user. Call it from React with `usePushOnLogin()` (below) + * rather than directly: it returns a cleanup that must run on logout, or every handler + * fires twice after the next login. + */ +export function setupPushOnLogin(): () => void { + // Subscribe BEFORE init(): the tap or answered call that LAUNCHED the app is delivered + // as soon as init() runs. + const unsubscribes = [ + CometChatPushNotifications.onNotificationTap(openFromNotification), + CometChatPushNotifications.onCallAccepted(openCallScreen), + CometChatPushNotifications.onCallEnded(endCall), + ]; + + const start = async () => { + // Await each permission request before the next — Android allows only one pending + // request per activity. A rejection means the OS could not be asked (not that the user + // declined), and must not stop init(): the push token still has to register. + await CometChatPNHelper.requestNotificationPermission().catch(() => false); + await CometChatPNHelper.requestCallPermissions(); // mic + camera, needed before a call connects + + await CometChatPushNotifications.init({ + fcmProviderId: AppCredentials.fcmProviderId, // Android + apnsProviderId: AppCredentials.apnsProviderId, // iOS (APNs device + VoIP) + notificationSmallIcon: 'ic_notification', // Android status-bar icon (step 2) + showInForeground: true, // one notification while the app is open, too + ringInForeground: false, // the UI Kit shows its own incoming-call screen while open + }); + }; + start().catch(error => console.log('Push setup failed:', error)); + + return () => unsubscribes.forEach(unsubscribe => unsubscribe()); +} + +/** + * Runs push while a user is logged in — after a fresh login AND after a session restored + * on launch — and cleans up on logout. Use it once, in a component rendered after + * CometChat has been initialized. + */ +export function usePushOnLogin(): void { + const [loggedIn, setLoggedIn] = useState(false); + + useEffect(() => { + // A restored session never fires loginSuccess, so check once on mount. + CometChat.getLoggedinUser() + .then(user => setLoggedIn(!!user)) + .catch(() => setLoggedIn(false)); + + CometChat.addLoginListener( + LOGIN_LISTENER_ID, + new CometChat.LoginListener({ + loginSuccess: () => setLoggedIn(true), + logoutSuccess: () => setLoggedIn(false), + }), + ); + return () => CometChat.removeLoginListener(LOGIN_LISTENER_ID); + }, []); + + useEffect(() => { + if (!loggedIn) return; + return setupPushOnLogin(); + }, [loggedIn]); +} + +/** Open the thread for a thread reply, otherwise the conversation. */ +async function openFromNotification(info: PNNotificationTapInfo): Promise { + const isGroup = info.receiverType === 'group'; + try { + const user = !isGroup && info.sender ? await CometChat.getUser(info.sender) : undefined; + const group = isGroup && info.receiver ? await CometChat.getGroup(info.receiver) : undefined; + if (!user && !group) return; + + markConversationRead(isGroup ? info.receiver! : info.sender!, isGroup); + + if (info.parentMessageId) { + try { + const parent = await CometChat.getMessageDetails(info.parentMessageId); + // The thread screen needs the user or group, not just the parent message. + await navigate(SCREENS.thread, { message: parent, user, group, highlightMessageId: info.messageId }); + return; + } catch (error) { + console.log('Could not open the thread, opening the conversation:', error); + } + } + await navigate(SCREENS.messages, { user, group }); + } catch (error) { + console.log('Could not open the conversation from a notification:', error); + } +} + +/** Mark the conversation read and clear its unread badge in the UI Kit's conversation list. */ +function markConversationRead(conversationWith: string, isGroup: boolean): void { + const type = isGroup ? CometChat.RECEIVER_TYPE.GROUP : CometChat.RECEIVER_TYPE.USER; + CometChat.markConversationAsRead(conversationWith, type) + .then(() => CometChat.getConversation(conversationWith, type)) + .then(conversation => { + const lastMessage = conversation.getLastMessage(); + if (lastMessage) { + CometChatUIEventHandler.emitMessageEvent(MessageEvents.ccMessageRead, { message: lastMessage }); + } + }) + .catch(error => console.log('Could not mark the conversation read:', error)); +} + +/** The package has already accepted the call — just show the call screen. */ +function openCallScreen(info: PNCallInfo): void { + navigate(SCREENS.ongoingCall, { sessionId: info.sessionId, callType: info.callType }); +} + +/** + * A ringing call was cancelled or declined, or the user ended the call from the iOS call + * screen — which the Calls SDK does not see, so tear the call down here. + */ +function endCall(info: PNCallEndEvent): void { + if (info.sessionId) CometChat.endCall(info.sessionId).catch(() => {}); + try { + CometChatCalls.endSession(); + } catch {} + try { + CometChat.clearActiveCall(); + } catch {} + if (navigationRef.isReady() && navigationRef.getCurrentRoute()?.name === SCREENS.ongoingCall) { + navigate(SCREENS.home); + } +} +``` + + +Not using the UI Kit? Delete the `@cometchat/chat-uikit-react-native` import and the `emitMessageEvent` block in `markConversationRead`, point `SCREENS` and the route params at your own screens, and in `logout.ts` call `CometChat.logout()` instead of `CometChatUIKit.logout()`. + + +**`App.tsx`** — call `usePushOnLogin()` once, in a component that renders **after** CometChat is initialized, and pass `navigationRef` to your `NavigationContainer`: + +```tsx App.tsx lines +import React, { useEffect, useState } from 'react'; +import { NavigationContainer } from '@react-navigation/native'; +import { CometChat } from '@cometchat/chat-sdk-react-native'; +import { CometChatUIKit, UIKitSettings } from '@cometchat/chat-uikit-react-native'; + +import { AppCredentials } from './AppCredentials'; +import { navigationRef } from './navigation/navigationRef'; +import { usePushOnLogin } from './push/pushNotifications'; + +export default function App() { + const [initialized, setInitialized] = useState(false); + + useEffect(() => { + // Your existing CometChat initialization. + CometChatUIKit.init({ + appId: AppCredentials.appId, + region: AppCredentials.region, + authKey: AppCredentials.authKey, + subscriptionType: CometChat.AppSettings.SUBSCRIPTION_TYPE_ALL_USERS as UIKitSettings['subscriptionType'], + } as UIKitSettings) + .then(() => setInitialized(true)) + .catch(error => console.log('CometChat init failed:', error)); + }, []); + + // Push must start only after CometChat is initialized. + if (!initialized) return null; + return ; +} + +function Root() { + usePushOnLogin(); // push follows login and logout from here on + + return ( + + {/* your existing navigator */} + + ); +} +``` + +`usePushOnLogin()` starts push after a fresh login **and** when a session is restored on launch, and removes the handlers when the user logs out — so a later login never registers them twice. + +### 3.3 Align dependencies and configuration + +- **Peer dependencies:** `@cometchat/chat-sdk-react-native` (or the UI Kit) for chat, `@cometchat/calls-sdk-react-native` for calls, and React Navigation for the handlers above. +- **`init()` options:** + - `fcmProviderId` (Android) and `apnsProviderId` (iOS) — from step 1. + - `notificationSmallIcon` — the Android status-bar icon. + - `showInForeground` (default `false`) — show chat notifications while the app is open. + - `ringInForeground` (default `true`) — ring with the system call UI while the app is open. Set `false` when your app shows its own incoming-call screen, as the UI Kit does. + - `voip` (default `true`), `androidChannelId`, `androidChannelName`. + +## 4. Configure the native Android layer + +### 4.1 Gradle + Firebase + +1. Add `google-services.json` to `android/app`. +2. Apply the Google Services plugin. The package already depends on `firebase-messaging`, so don't add it yourself: + +```groovy android/build.gradle lines +buildscript { + dependencies { + classpath("com.google.gms:google-services:4.4.2") + } +} +``` + +```groovy android/app/build.gradle lines +apply plugin: "com.google.gms.google-services" +``` + +Keep `minSdkVersion 24` or higher. + +### 4.2 Manifest permissions and components + +You don't edit `AndroidManifest.xml`. The package's manifest is merged into your app with everything push and calls need: + +- **Components:** its `FirebaseMessagingService`, the incoming-call foreground service, the full-screen `CallRingingActivity` (shows over the lock screen), the call action receiver, and the background task service for killed-app declines. +- **Permissions:** `POST_NOTIFICATIONS`, `USE_FULL_SCREEN_INTENT`, `FOREGROUND_SERVICE`, `FOREGROUND_SERVICE_PHONE_CALL`, `MANAGE_OWN_CALLS`, `WAKE_LOCK`, `VIBRATE`, `RECORD_AUDIO`, `CAMERA` and `BLUETOOTH_CONNECT`. Camera and microphone hardware are declared optional, so Google Play doesn't hide your app from devices without them. + + +**Google Play (Android 14+):** apps with calls complete two declarations in Play Console under **App content** — **Full-screen intent permission** (calling as core functionality) and **Foreground service permissions** (the **Phone call** type). Without the first, Play revokes `USE_FULL_SCREEN_INTENT` and calls ring only as a heads-up notification. + + +**Chat-only apps** remove the call permissions and components (with `xmlns:tools="http://schemas.android.com/tools"` on the `manifest` element), and skip `requestCallPermissions()` and `registerBackgroundCallTask()`: + +```xml android/app/src/main/AndroidManifest.xml lines + + + + + + + + + + + +``` + +`tools:node="remove"` drops an entry whichever library declared it — keep any permission another part of your app still uses. + +### 4.3 Notification icon + +Add a **white-on-transparent** drawable named `ic_notification` — for example `android/app/src/main/res/drawable/ic_notification.png`. Android Studio generates one: right-click `res` → **New → Image Asset**, icon type **Notification Icons**, name `ic_notification`. Without it, the package falls back to your launcher icon, which the status bar draws as a plain white shape. + +There is no Kotlin bridge to write: the ringing screen, the Answer and Decline actions, and the killed-app decline are built into the package. + +### 4.4 OEM permissions for lock-screen calls + +Stock Android shows the full-screen call over the lock screen out of the box. **OEM skins (MIUI/Redmi/POCO, Oppo, Vivo) gate background-launched full-screen activities** behind their own toggles — without them, a locked or killed call shows only a heads-up notification, and the ringing screen appears after unlock. Guide users to grant, on those devices: + +- **Autostart** — Settings → Apps → *your app* → Autostart (or the Security app). +- **Display pop-up windows while running in background** — Settings → Apps → *your app* → Other permissions. +- **Show on lock screen** — the same "Other permissions" screen. +- Disable **battery optimization** for the app. + +These settings can't be granted programmatically; open the app's settings page so the user can toggle them: + +```ts lines +import { Linking, Platform } from 'react-native'; +if (Platform.OS === 'android') Linking.openSettings(); +``` + +### 4.5 Apps that also use `@react-native-firebase/messaging` + +Android delivers FCM messages and token refreshes to only **one** `FirebaseMessagingService`, and React Native Firebase ships its own — so with both installed, one of them silently receives nothing. Replace both with a service of your own that forwards to each: + +1. Remove both library services and register yours: + +```xml android/app/src/main/AndroidManifest.xml lines + + + + + + + + + +``` + +2. Add Firebase Messaging to your app so the service can extend it (use the BOM version your other Firebase libraries use): + +```groovy android/app/build.gradle lines +dependencies { + implementation platform("com.google.firebase:firebase-bom:33.16.0") + implementation "com.google.firebase:firebase-messaging" +} +``` + +3. Add the service next to `MainApplication.kt`: + +```kotlin AppMessagingService.kt lines +package com.yourapp // your app's package + +import com.cometchat.pushnotification.reactnative.CometChatFcmService +import com.google.firebase.messaging.RemoteMessage +import io.invertase.firebase.messaging.ReactNativeFirebaseMessagingService + +class AppMessagingService : ReactNativeFirebaseMessagingService() { + override fun onMessageReceived(message: RemoteMessage) { + // CometChat's pushes are shown by this package; anything else goes on. + if (!CometChatFcmService.handleMessage(this, message)) super.onMessageReceived(message) + } + + override fun onNewToken(token: String) { + CometChatFcmService.handleNewToken(this, token) + super.onNewToken(token) + } +} +``` + +4. React Native Firebase also hands every push to its JavaScript handlers. Skip CometChat's there, in `onMessage` and `setBackgroundMessageHandler`: + +```ts lines +messaging().onMessage(async (remoteMessage) => { + if (CometChatPNHelper.isCometChatNotification(remoteMessage.data)) return; // shown by this package + // your handling +}); +``` + +## 5. Token registration and runtime events + +### 5.1 FCM tokens + +The package fetches the FCM token during `init()` and registers it with your FCM provider for the logged-in user; when Firebase refreshes the token, it registers the new one. If `init()` runs a moment before login finishes, registration retries 5 times, 3 seconds apart. + +`setupPushOnLogin()` requests the permissions, in order, before `init()`. To check or request them elsewhere: + +```ts lines +const granted = await CometChatPNHelper.requestNotificationPermission().catch(() => false); +await CometChatPNHelper.requestCallPermissions(); // mic + camera (Android); iOS asks on first use +const enabled = await CometChatPNHelper.hasNotificationPermission(); // checks without prompting +``` + + +**Always `await` one permission request before starting the next.** Android allows only one pending request per activity: a second request cancels the dialog still on screen, and the OS reports it as denied without the user seeing it. On a fresh install that leaves the app with no notification permission — pushes arrive and are dropped. + + +`requestNotificationPermission()` resolves `true` or `false` on the user's answer. It **rejects** when the permission could not be requested at all — a different situation from the user declining, and not a reason to skip `init()`: + +| Rejection | Meaning | +| --- | --- | +| `ERR_NO_ACTIVITY` | No foreground activity, so no dialog can be shown. Retry when the app is in the foreground. | +| `ERR_PERMISSION_IN_FLIGHT` | A request is already open. Await that one instead. | +| `ERR_ACTIVITY_NOT_PERMISSION_AWARE` | Your host activity does not extend `ReactActivity`. Fix the activity. | + +You rarely need it, but you can register a token yourself: + +```ts lines +await CometChatPushNotifications.registerToken('fcm', token); // or 'apns' / 'voip' +``` + +### 5.2 Local notifications and navigation + +- **App in the background or killed:** the package's service shows the notification with your `ic_notification` icon. +- **App open:** the payload goes to `onMessageReceived`; a system notification also shows when `showInForeground` is `true`. +- **Tap:** `onNotificationTap` fires, and `openFromNotification` marks the conversation read, then opens the thread for a thread reply, otherwise the conversation. A tap that **launched** the app is held until your handler subscribes, and navigation waits for the navigator. + +### 5.3 Call events + +| Event | What happens | +| --- | --- | +| **Call push arrives** | App in the background or killed: the package starts a phone-call foreground service with the full-screen ringing screen, over the lock screen too. App open: it rings the same way unless `ringInForeground` is `false`. The ring stops itself after 45 seconds if nothing ends it. | +| **Accept** | The app opens and the package accepts the call through the Chat SDK; then `onCallAccepted` fires and `openCallScreen` opens your call screen. | +| **Decline** | The package rejects the call through the Chat SDK. In a **fully killed** app the background task registered in `index.js` does it, so the caller sees the call rejected right away. | +| **Caller hangs up** | A cancel push stops the ring and `onCallEnded` fires; `endCall` tears the call down and leaves the call screen. | + +### 5.4 Unregister on logout + +Add `src/push/logout.ts` and call it from your logout button instead of logging out directly: + +```ts src/push/logout.ts lines +import { CometChatUIKit } from '@cometchat/chat-uikit-react-native'; +import { CometChatPushNotifications } from '@cometchat/push-notifications-react-native'; + +/** Log out and stop this device receiving the user's notifications. Resolves false on failure. */ +export async function logout(): Promise { + // Unregister BEFORE logout: it needs the session's auth token, so after logout it fails + // and the device keeps receiving notifications for the user who just logged out. + try { + await CometChatPushNotifications.unregister(); + } catch (error) { + console.log('Failed to unregister the push token:', error); + return false; + } + try { + await CometChatUIKit.logout(); + return true; + } catch (error) { + console.log('Logout failed:', error); + return false; + } +} +``` + +```tsx lines +const onLogoutPress = async () => { + if (loggingOut) return; // ignore a second tap while logging out + setLoggingOut(true); + const loggedOut = await logout(); + setLoggingOut(false); + if (loggedOut) navigation.navigate('Login'); // your login screen +}; +``` + + +`unregister()` must run **before** logout. It needs the session's auth token — after logout it fails, and the device keeps receiving notifications for the user who just logged out. + + +## 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 it to set a launcher badge. + +### 6.1 Enable unread badge count on the CometChat Dashboard + +1. Go to **CometChat Dashboard → Notifications → Settings → Preferences → Push Notification Preferences**. +2. Scroll to the bottom and enable the **Unread Badge Count** toggle. + +This ensures CometChat includes the `unreadMessageCount` field in every push payload sent to your app. + +### 6.2 Expected payload format + +CometChat sends FCM data messages with this structure (relevant fields): + +```jsonc +{ + "data": { + "unreadMessageCount": "5", + "title": "New Message", + "alert": "John: Hello!", + "conversationId": "user_abc123", + "parentId": "176001", // Optional - parent message ID; sent only for threaded notifications + "conversationType": "user" + } +} +``` + +### 6.3 Update the app badge from the push payload + +Android has no OS-level app icon badge API, and the push package doesn't manage launcher badges. While the app is open, `onMessageReceived` receives each payload — hand `unreadMessageCount` to a launcher-badge library. In the background the package shows the notification, and Android's notification dot marks the app icon. + +```ts lines +CometChatPushNotifications.onMessageReceived(data => { + const count = Number(data.unreadMessageCount ?? 0); + // hand `count` to your badge library +}); +``` + +## 7. Testing checklist + +Use physical devices and a **release** build: a debug build loads its JavaScript from Metro, which delays the first JavaScript that runs in a killed app. + +1. **Fresh install:** install, log in, and confirm the notification prompt **waits** for your answer before the microphone/camera prompt appears. Then send a message from another user — it must arrive. +2. **Chat notifications:** + - App open: exactly **one** notification (`showInForeground: true`). + - App in the background: a notification appears; tapping it opens the conversation. + - App killed: tapping the notification starts the app **in** the conversation. + - A thread reply opens the **thread**; a group message opens the group. +3. **Calls, app killed, phone locked:** + - The **full-screen ringing screen** shows with Accept and Decline. + - **Accept** connects the call with audio both ways. + - **Decline** shows the call as rejected on the caller's side. + - The caller **cancelling** stops the ring. +4. **Calls, app in the background:** the ringing screen shows, and accept and decline both work. +5. **Calls, app open:** only your in-app incoming-call screen rings (`ringInForeground: false`). +6. **Logout:** log out, send a message from another user — nothing arrives. Log in as another user — only that user's notifications arrive. +7. **OEM devices** (MIUI, Oppo, Vivo): grant the step 4.4 permissions and re-check locked and killed calls. + +## 8. Troubleshooting tips + +| Symptom | Quick checks | +| --- | --- | +| No notifications received | `google-services.json` is in `android/app`, its package name matches the app, the Google Services plugin is applied, and `POST_NOTIFICATIONS` is granted (Android 13+). | +| Notifications work on an existing install but not on a fresh one | A permission request wasn't awaited, so the notification dialog was cancelled unseen (step 5.1). `adb shell dumpsys package \| grep POST_NOTIFICATIONS`: `granted=false` with no `USER_SET` flag means the dialog was never answered. | +| Two notifications for one message | Another push library is still installed (step 3.1). With `@react-native-firebase/messaging`, follow step 4.5. | +| Incoming call is a plain notification, not the full-screen ringing screen | The merged manifest still has `MANAGE_OWN_CALLS` and `FOREGROUND_SERVICE_PHONE_CALL`. On MIUI, Oppo and Vivo, grant the step 4.4 permissions. From Google Play, complete the full-screen intent declaration (step 4.2). | +| Declining a call in a killed app doesn't reject it | `registerBackgroundCallTask()` is called at module scope in `index.js`, and the app has been opened and logged in once since installing (so `init()` saved the Chat SDK settings). Test on a release build. | +| Token registration errors | The provider IDs match the dashboard exactly, and `usePushOnLogin()` is rendered after CometChat is initialized. | +| No notification while the app is open | Expected with `showInForeground: false` (the default) — set it to `true`. For calls, `ringInForeground` decides whether the system call UI or your in-app screen rings. | +| Tapping a notification opens the app but not the conversation | `navigationRef` is passed to your `NavigationContainer`, navigation goes through `navigate()` from `navigationRef.ts`, and the route names in `SCREENS` match your navigator. | +| Thread reply opens an empty thread screen | The thread screen is given the user or group as well as the parent message, as `openFromNotification` does. | +| Handlers fire twice after logging out and in | Use `usePushOnLogin()` rather than calling `setupPushOnLogin()` directly — its cleanup must run on logout. | +| Notifications still arrive after logout | `unregister()` runs **before** logout and its failure isn't ignored. | diff --git a/notifications/react-native-push-notifications-ios.mdx b/notifications/react-native-push-notifications-ios.mdx new file mode 100644 index 000000000..7374f4cd9 --- /dev/null +++ b/notifications/react-native-push-notifications-ios.mdx @@ -0,0 +1,605 @@ +--- +title: "React Native Push Notifications (iOS)" +description: "CometChat push notifications and VoIP calls in React Native apps on iOS using Apple Push Notification service (APNs), PushKit and CallKit, with the @cometchat/push-notifications-react-native package." +--- + + + +| Field | Value | +| --- | --- | +| Platform | iOS (APNs + PushKit + CallKit) | +| Package | `@cometchat/push-notifications-react-native` | +| Key APIs | `CometChatPushNotifications.init()`, `onNotificationTap()`, `onCallAccepted()`, `onCallEnded()`, `unregister()`, `CometChatPushNotificationsAppDelegate.registerForVoIPPushes()`, `CometChatPushNotificationsAppDelegate.didRegisterAPNsToken(_:)` | +| Push Platforms | `APNS_REACT_NATIVE_DEVICE` and `APNS_REACT_NATIVE_VOIP`, registered by `init()` with one `apnsProviderId` | +| Native setup | Push Notifications + Background Modes (Voice over IP, Remote notifications, Audio), microphone and camera usage strings, and in `AppDelegate`: `registerForVoIPPushes()` before React Native starts plus the APNs token method | +| Prerequisites | CometChat initialized and the user logged in before `init()`, an APNs provider ID, a physical device | + + + + + Reference implementation of React Native UI Kit, APNs and Push Notification Setup. + + +## What this guide covers + +- CometChat dashboard setup (enable push, add an APNs provider) with screenshots. +- Apple setup (APNs key, capabilities, `Info.plist`). +- Wiring the package's notification and call handlers into your app. +- Native iOS setup — a few `AppDelegate` lines; no PushKit or CallKit code to write. +- Token registration (APNs + VoIP), notification/call handling, navigation, testing, and troubleshooting. +- App icon badge count using `unreadMessageCount` from the CometChat push payload. + +## How APNs + CometChat work together + +- **APNs's role:** Issues the device token for chat notifications and, through PushKit, the VoIP token for calls, and delivers both kinds of push. No Firebase is needed on iOS. +- **CometChat's role:** The APNs provider you add in the CometChat dashboard holds your `.p8` key. When `init()` runs after login, the package registers both tokens with that one provider, and CometChat sends chat pushes and VoIP call pushes through APNs. +- **The package's role:** It creates and owns the PushKit registry and reports every VoIP push to CallKit — before React Native starts in a killed app. Every CometChat action runs in JavaScript through the Chat SDK your app already uses. +- **Flow:** Permission prompt → APNs issues the device token and PushKit the VoIP token → after login, `init()` registers both with `AppCredentials.apnsProviderId` → CometChat sends to APNs → iOS shows the notification, or the package reports the call to CallKit → your `onNotificationTap`, `onCallAccepted` and `onCallEnded` handlers navigate. + +## 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**, upload your `.p8` key with its Key ID and Team ID, and copy the Provider ID. One APNs provider covers both chat notifications and VoIP call pushes. + + + Upload APNs credentials + + +Keep the provider ID—you'll use it in `AppCredentials.apnsProviderId`. + +## 2. Prepare Apple 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 on your app's bundle ID. + + +**`.p12` certificates are deprecated.** Apple recommends `.p8` Auth Keys for push notifications: they never expire and work across all your apps. + + +## 3. Local configuration file + +Create `src/AppCredentials.ts` with your app credentials and provider IDs. The same file serves the [Android guide](/notifications/react-native-push-notifications-android): + +```ts src/AppCredentials.ts lines +export const AppCredentials = { + appId: 'YOUR_APP_ID', + region: 'YOUR_REGION', + authKey: 'YOUR_AUTH_KEY', + + // Android — the FCM provider ID from the CometChat dashboard + fcmProviderId: 'FCM-PROVIDER-ID', + + // iOS — one APNs provider covers both the device token and the VoIP token + apnsProviderId: 'APNS-PROVIDER-ID', +}; +``` + +## 4. Bring the push package into React Native + +### 4.1 Install the package + +```bash +npm install @cometchat/push-notifications-react-native +cd ios && pod install && cd .. +``` + +Keep the Podfile's `platform :ios, min_ios_version_supported` from the React Native template. Don't lower it: current React Native requires iOS 15.1, and a lower platform fails the build — for example with `'hermes/hermes.h' file not found`. + + +**Remove other push and call libraries first** — `@react-native-firebase/messaging`, `@notifee/react-native`, `react-native-callkeep`, `react-native-voip-push-notification` — along with their code and native setup. Each registers its own push handler or PushKit registry, and every notification or call then arrives twice. + + +### 4.2 Wire the entry points + +**`index.js`** — the same file as on Android. `registerBackgroundCallTask()` is a no-op on iOS, where CallKit handles a decline in a killed app: + +```js index.js lines +import { AppRegistry } from 'react-native'; +import { registerBackgroundCallTask } from '@cometchat/push-notifications-react-native'; +import App from './App'; +import { name as appName } from './app.json'; + +// Android: lets a FULLY KILLED app reject a call declined from its notification. The package +// does the work — this only registers its background task. (No-op on iOS.) +registerBackgroundCallTask(); + +AppRegistry.registerComponent(appName, () => App); +``` + +**`src/navigation/navigationRef.ts`** — a notification tap or answered call that **launched** the app arrives before your navigator exists, so every navigation waits for it: + +```ts src/navigation/navigationRef.ts lines +import { createNavigationContainerRef } from '@react-navigation/native'; + +/** Pass this to your . */ +export const navigationRef = createNavigationContainerRef(); + +/** + * Resolves once the NavigationContainer is mounted. A notification tap or answered call + * that LAUNCHED the app arrives before the navigator exists, and navigating then is + * silently dropped. The ref queues listeners added before it mounts. + */ +export function whenNavigationReady(): Promise { + if (navigationRef.isReady()) return Promise.resolve(); + return new Promise(resolve => { + const unsubscribe = navigationRef.addListener('ready', () => { + unsubscribe(); + resolve(); + }); + }); +} + +/** Navigate by route name once the navigator is ready. */ +export async function navigate(name: string, params?: object): Promise { + await whenNavigationReady(); + (navigationRef.navigate as (name: string, params?: object) => void)(name, params); +} +``` + +**`src/push/pushNotifications.ts`** — everything push does for the logged-in user: the tap, call-accepted and call-ended handlers, the permission requests, and `init()`: + +```ts src/push/pushNotifications.ts lines +import { useEffect, useState } from 'react'; +import { CometChat } from '@cometchat/chat-sdk-react-native'; +import { CometChatCalls } from '@cometchat/calls-sdk-react-native'; +import { CometChatUIEventHandler, MessageEvents } from '@cometchat/chat-uikit-react-native'; +import { + CometChatPNHelper, + CometChatPushNotifications, + type PNCallEndEvent, + type PNCallInfo, + type PNNotificationTapInfo, +} from '@cometchat/push-notifications-react-native'; + +import { AppCredentials } from '../AppCredentials'; +import { navigate, navigationRef } from '../navigation/navigationRef'; + +/** Your navigator's route names — these are the CometChat UI Kit sample app's. */ +const SCREENS = { + messages: 'Messages', + thread: 'ThreadView', + ongoingCall: 'OngoingCallScreen', + home: 'BottomTabNavigator', +} as const; + +const LOGIN_LISTENER_ID = 'push-notifications-login'; + +/** + * Starts push for the logged-in user. Call it from React with `usePushOnLogin()` (below) + * rather than directly: it returns a cleanup that must run on logout, or every handler + * fires twice after the next login. + */ +export function setupPushOnLogin(): () => void { + // Subscribe BEFORE init(): the tap or answered call that LAUNCHED the app is delivered + // as soon as init() runs. + const unsubscribes = [ + CometChatPushNotifications.onNotificationTap(openFromNotification), + CometChatPushNotifications.onCallAccepted(openCallScreen), + CometChatPushNotifications.onCallEnded(endCall), + ]; + + const start = async () => { + // Await each permission request before the next — Android allows only one pending + // request per activity. A rejection means the OS could not be asked (not that the user + // declined), and must not stop init(): the push token still has to register. + await CometChatPNHelper.requestNotificationPermission().catch(() => false); + await CometChatPNHelper.requestCallPermissions(); // mic + camera, needed before a call connects + + await CometChatPushNotifications.init({ + fcmProviderId: AppCredentials.fcmProviderId, // Android + apnsProviderId: AppCredentials.apnsProviderId, // iOS (APNs device + VoIP) + notificationSmallIcon: 'ic_notification', // Android status-bar icon (step 2) + showInForeground: true, // one notification while the app is open, too + ringInForeground: false, // the UI Kit shows its own incoming-call screen while open + }); + }; + start().catch(error => console.log('Push setup failed:', error)); + + return () => unsubscribes.forEach(unsubscribe => unsubscribe()); +} + +/** + * Runs push while a user is logged in — after a fresh login AND after a session restored + * on launch — and cleans up on logout. Use it once, in a component rendered after + * CometChat has been initialized. + */ +export function usePushOnLogin(): void { + const [loggedIn, setLoggedIn] = useState(false); + + useEffect(() => { + // A restored session never fires loginSuccess, so check once on mount. + CometChat.getLoggedinUser() + .then(user => setLoggedIn(!!user)) + .catch(() => setLoggedIn(false)); + + CometChat.addLoginListener( + LOGIN_LISTENER_ID, + new CometChat.LoginListener({ + loginSuccess: () => setLoggedIn(true), + logoutSuccess: () => setLoggedIn(false), + }), + ); + return () => CometChat.removeLoginListener(LOGIN_LISTENER_ID); + }, []); + + useEffect(() => { + if (!loggedIn) return; + return setupPushOnLogin(); + }, [loggedIn]); +} + +/** Open the thread for a thread reply, otherwise the conversation. */ +async function openFromNotification(info: PNNotificationTapInfo): Promise { + const isGroup = info.receiverType === 'group'; + try { + const user = !isGroup && info.sender ? await CometChat.getUser(info.sender) : undefined; + const group = isGroup && info.receiver ? await CometChat.getGroup(info.receiver) : undefined; + if (!user && !group) return; + + markConversationRead(isGroup ? info.receiver! : info.sender!, isGroup); + + if (info.parentMessageId) { + try { + const parent = await CometChat.getMessageDetails(info.parentMessageId); + // The thread screen needs the user or group, not just the parent message. + await navigate(SCREENS.thread, { message: parent, user, group, highlightMessageId: info.messageId }); + return; + } catch (error) { + console.log('Could not open the thread, opening the conversation:', error); + } + } + await navigate(SCREENS.messages, { user, group }); + } catch (error) { + console.log('Could not open the conversation from a notification:', error); + } +} + +/** Mark the conversation read and clear its unread badge in the UI Kit's conversation list. */ +function markConversationRead(conversationWith: string, isGroup: boolean): void { + const type = isGroup ? CometChat.RECEIVER_TYPE.GROUP : CometChat.RECEIVER_TYPE.USER; + CometChat.markConversationAsRead(conversationWith, type) + .then(() => CometChat.getConversation(conversationWith, type)) + .then(conversation => { + const lastMessage = conversation.getLastMessage(); + if (lastMessage) { + CometChatUIEventHandler.emitMessageEvent(MessageEvents.ccMessageRead, { message: lastMessage }); + } + }) + .catch(error => console.log('Could not mark the conversation read:', error)); +} + +/** The package has already accepted the call — just show the call screen. */ +function openCallScreen(info: PNCallInfo): void { + navigate(SCREENS.ongoingCall, { sessionId: info.sessionId, callType: info.callType }); +} + +/** + * A ringing call was cancelled or declined, or the user ended the call from the iOS call + * screen — which the Calls SDK does not see, so tear the call down here. + */ +function endCall(info: PNCallEndEvent): void { + if (info.sessionId) CometChat.endCall(info.sessionId).catch(() => {}); + try { + CometChatCalls.endSession(); + } catch {} + try { + CometChat.clearActiveCall(); + } catch {} + if (navigationRef.isReady() && navigationRef.getCurrentRoute()?.name === SCREENS.ongoingCall) { + navigate(SCREENS.home); + } +} +``` + + +Not using the UI Kit? Delete the `@cometchat/chat-uikit-react-native` import and the `emitMessageEvent` block in `markConversationRead`, point `SCREENS` and the route params at your own screens, and in `logout.ts` call `CometChat.logout()` instead of `CometChatUIKit.logout()`. + + +**`App.tsx`** — call `usePushOnLogin()` once, in a component that renders **after** CometChat is initialized, and pass `navigationRef` to your `NavigationContainer`: + +```tsx App.tsx lines +import React, { useEffect, useState } from 'react'; +import { NavigationContainer } from '@react-navigation/native'; +import { CometChat } from '@cometchat/chat-sdk-react-native'; +import { CometChatUIKit, UIKitSettings } from '@cometchat/chat-uikit-react-native'; + +import { AppCredentials } from './AppCredentials'; +import { navigationRef } from './navigation/navigationRef'; +import { usePushOnLogin } from './push/pushNotifications'; + +export default function App() { + const [initialized, setInitialized] = useState(false); + + useEffect(() => { + // Your existing CometChat initialization. + CometChatUIKit.init({ + appId: AppCredentials.appId, + region: AppCredentials.region, + authKey: AppCredentials.authKey, + subscriptionType: CometChat.AppSettings.SUBSCRIPTION_TYPE_ALL_USERS as UIKitSettings['subscriptionType'], + } as UIKitSettings) + .then(() => setInitialized(true)) + .catch(error => console.log('CometChat init failed:', error)); + }, []); + + // Push must start only after CometChat is initialized. + if (!initialized) return null; + return ; +} + +function Root() { + usePushOnLogin(); // push follows login and logout from here on + + return ( + + {/* your existing navigator */} + + ); +} +``` + +`usePushOnLogin()` starts push after a fresh login **and** when a session is restored on launch, and removes the handlers when the user logs out — so a later login never registers them twice. + +### 4.3 Align dependencies and configuration + +- **Peer dependencies:** `@cometchat/chat-sdk-react-native` (or the UI Kit) for chat, `@cometchat/calls-sdk-react-native` for calls, and React Navigation for the handlers above. +- **`init()` options:** + - `fcmProviderId` (Android) and `apnsProviderId` (iOS) — from step 1. + - `notificationSmallIcon` — the Android status-bar icon. + - `showInForeground` (default `false`) — show chat notifications while the app is open. + - `ringInForeground` (default `true`) — ring with the system call UI while the app is open. Set `false` when your app shows its own incoming-call screen, as the UI Kit does. + - `voip` (default `true`), `androidChannelId`, `androidChannelName`. + +## 5. Configure the native iOS layer + +### 5.1 Capabilities and Info.plist + +1. Open `ios/.xcworkspace` in Xcode. +2. Under *Signing & Capabilities*, enable **Push Notifications** and **Background Modes** with **Voice over IP**, **Remote notifications**, and **Audio, AirPlay, and Picture in Picture**. +3. Add the microphone and camera usage strings to `Info.plist` — a call can't use either without them: + +```xml ios//Info.plist lines +NSMicrophoneUsageDescription +Needed for voice and video calls +NSCameraUsageDescription +Needed for video calls +``` + + + Enable Push Notifications and Background Modes for APNs + + +### 5.2 `AppDelegate.swift` + +Replace `ios//AppDelegate.swift` with this — React Native's current template plus the push lines. Set `withModuleName` to your app's name: + +```swift ios//AppDelegate.swift lines +import UIKit +import React +import React_RCTAppDelegate +import ReactAppDependencyProvider +import react_native_cometchat_push_notifications + +@main +class AppDelegate: UIResponder, UIApplicationDelegate { + var window: UIWindow? + var reactNativeDelegate: ReactNativeDelegate? + var reactNativeFactory: RCTReactNativeFactory? + + func application( + _ application: UIApplication, + didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? = nil + ) -> Bool { + let delegate = ReactNativeDelegate() + let factory = RCTReactNativeFactory(delegate: delegate) + delegate.dependencyProvider = RCTAppDependencyProvider() + reactNativeDelegate = delegate + reactNativeFactory = factory + + // VoIP calls: the package creates and owns the PushKit registry. Call it BEFORE + // starting React Native — when a call wakes a killed app, iOS terminates the app + // unless the call reaches CallKit within ~5 seconds. + CometChatPushNotificationsAppDelegate.registerForVoIPPushes() + + window = UIWindow(frame: UIScreen.main.bounds) + factory.startReactNative( + withModuleName: "YourAppName", // your app's registered name + in: window, + launchOptions: launchOptions + ) + return true + } + + // APNs device token — chat notifications + func application( + _ application: UIApplication, + didRegisterForRemoteNotificationsWithDeviceToken deviceToken: Data + ) { + CometChatPushNotificationsAppDelegate.didRegisterAPNsToken(deviceToken) + } + + // Optional — background data pushes reach onMessageReceived + func application( + _ application: UIApplication, + didReceiveRemoteNotification userInfo: [AnyHashable: Any], + fetchCompletionHandler completionHandler: @escaping (UIBackgroundFetchResult) -> Void + ) { + CometChatPushNotificationsAppDelegate.didReceiveRemoteNotification(userInfo) + completionHandler(.noData) + } +} + +class ReactNativeDelegate: RCTDefaultReactNativeFactoryDelegate { + override func sourceURL(for bridge: RCTBridge) -> URL? { + self.bundleURL() + } + + override func bundleURL() -> URL? { +#if DEBUG + return RCTBundleURLProvider.sharedSettings().jsBundleURL(forBundleRoot: "index") +#else + return Bundle.main.url(forResource: "main", withExtension: "jsbundle") +#endif + } +} +``` + +You don't write PushKit or CallKit code, and you don't set a `UNUserNotificationCenter` delegate — the package installs its own at launch to handle foreground notifications and taps. + + +**Don't create a `PKPushRegistry` of your own.** The package owns it, and a second registry — yours or a library's — makes iOS deliver every VoIP push twice. If another library must own PushKit, skip `registerForVoIPPushes()` and forward that registry's `didUpdate` and `didReceiveIncomingPushWith` callbacks to `CometChatPushNotificationsAppDelegate.didUpdateVoIPToken(_:)` and `.didReceiveIncomingVoIPPush(_:)`, calling `completion()` after it. + + + +**Older Swift template** (an `RCTAppDelegate` subclass): call `registerForVoIPPushes()` before `return super.application(...)`, which starts React Native, and add the same token method. **Objective-C `AppDelegate.mm`:** the package's iOS entry points are Swift-only, so move the AppDelegate to Swift first — the [React Native Upgrade Helper](https://react-native-community.github.io/upgrade-helper/) shows the change. + + +## 6. Token registration and runtime events + +### 6.1 Standard APNs tokens + +`didRegisterAPNsToken(_:)` hands the device token to the package, and `init()` registers it with your APNs provider for the logged-in user — re-registering it whenever iOS issues a new one. `setupPushOnLogin()` asks for notification permission before `init()`. On iOS `requestCallPermissions()` does nothing: iOS asks for the microphone and camera the first time a call uses them. + +### 6.2 VoIP tokens + +`registerForVoIPPushes()` creates the PushKit registry at launch, and PushKit hands over the VoIP token right away — before React Native runs. The package holds it, and `init()` registers it with the same APNs provider. If `init()` runs a moment before login finishes, registration retries 5 times, 3 seconds apart. + +### 6.3 Local notifications and navigation + +- **App in the background or killed:** iOS shows the APNs notification. +- **App open:** the package's notification delegate shows the banner when `showInForeground` is `true`; otherwise the payload goes to `onMessageReceived`. +- **Tap:** `onNotificationTap` fires, and `openFromNotification` marks the conversation read, then opens the thread for a thread reply, otherwise the conversation. A tap that **launched** the app is held until your handler subscribes, and navigation waits for the navigator. + +### 6.4 Call events + +| Event | What happens | +| --- | --- | +| **VoIP push arrives** | The package reports it to CallKit immediately, in every app state, as iOS requires. App open with `ringInForeground: false`: it reports the call and ends it at once, and your in-app screen rings instead. | +| **Accept** | The package puts the audio session in call mode, accepts the call through the Chat SDK, and hands CallKit's audio to WebRTC; then `onCallAccepted` fires and `openCallScreen` opens your call screen. | +| **Decline** | The package rejects the call through the Chat SDK. In a killed app, iOS has already launched the app for the VoIP push, and the package keeps it running long enough to reject. | +| **Caller hangs up** | The cancel VoIP push ends the CallKit call and `onCallEnded` fires. | +| **Ended from the iOS call screen** | `onCallEnded` fires; `endCall` ends the call on the server, ends the media session, and leaves the call screen. | + + +**Killed-app VoIP:** when a VoIP push wakes a killed app, the package reports the call to CallKit before React Native is ready. When the user answers, the app starts, `init()` delivers the answered call, and `onCallAccepted` opens your call screen — the call is already accepted. This is why `registerForVoIPPushes()` runs before React Native starts (step 5.2). + + +### 6.5 Unregister on logout + +Add `src/push/logout.ts` and call it from your logout button instead of logging out directly: + +```ts src/push/logout.ts lines +import { CometChatUIKit } from '@cometchat/chat-uikit-react-native'; +import { CometChatPushNotifications } from '@cometchat/push-notifications-react-native'; + +/** Log out and stop this device receiving the user's notifications. Resolves false on failure. */ +export async function logout(): Promise { + // Unregister BEFORE logout: it needs the session's auth token, so after logout it fails + // and the device keeps receiving notifications for the user who just logged out. + try { + await CometChatPushNotifications.unregister(); + } catch (error) { + console.log('Failed to unregister the push token:', error); + return false; + } + try { + await CometChatUIKit.logout(); + return true; + } catch (error) { + console.log('Logout failed:', error); + return false; + } +} +``` + +```tsx lines +const onLogoutPress = async () => { + if (loggingOut) return; // ignore a second tap while logging out + setLoggingOut(true); + const loggedOut = await logout(); + setLoggingOut(false); + if (loggedOut) navigation.navigate('Login'); // your login screen +}; +``` + + +`unregister()` must run **before** logout. It needs the session's auth token — after logout it fails, and the device keeps receiving notifications for the user who just logged out. + + +## 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 is handled by the server: CometChat sets `aps.badge` in the push payload, and iOS updates the app icon badge when the notification is delivered — no dependency or client code required. + +### 7.1 Enable unread badge count on the CometChat Dashboard + +1. Go to **CometChat Dashboard → Notifications → Settings → Preferences → Push Notification Preferences**. +2. Scroll to the bottom and enable the **Unread Badge Count** toggle. + +This ensures CometChat includes the `unreadMessageCount` field in every push payload and sets `aps.badge` for APNs. + +### 7.2 Expected payload format + +CometChat sends APNs payloads with this structure (relevant fields): + +```jsonc +{ + "aps": { + "alert": { + "title": "New Message", + "body": "John: Hello!" + }, + "badge": 5, + "sound": "default" + }, + "unreadMessageCount": 5, + "conversationId": "user_abc123", + "parentId": "176001" // Optional - parent message ID; sent only for threaded notifications +} +``` + +The `aps.badge` field is set by CometChat server-side, so iOS updates the badge when the push is delivered. + +## 8. Testing checklist + +Use a physical iPhone — the Simulator can't receive APNs or VoIP pushes — and a **release** build for killed-app calls. + +1. **First launch:** log in and allow notifications. Then send a message from another user — it must arrive. +2. **Chat notifications:** + - App open: exactly **one** banner (`showInForeground: true`). + - App in the background: a notification appears; tapping it opens the conversation. + - App killed: tapping the notification starts the app **in** the conversation. + - A thread reply opens the **thread**; a group message opens the group. +3. **Calls, app killed (locked and unlocked):** + - CallKit shows the call with Accept and Decline. + - **Accept** connects the call with audio both ways. + - **Decline** shows the call as rejected on the caller's side. + - The caller **cancelling** stops the ring. +4. **Calls, app in the background:** CallKit rings, and ending the call from the iOS call screen closes your call screen. +5. **Calls, app open:** only your in-app incoming-call screen rings (`ringInForeground: false`). +6. **Logout:** log out, send a message from another user — nothing arrives. Log in as another user — only that user's notifications arrive. + +## 9. Troubleshooting tips + +| Symptom | Quick checks | +| --- | --- | +| No VoIP pushes | Push Notifications + Background Modes (Voice over IP) are enabled, `aps-environment` matches the build (`production` for release), and the bundle ID matches the CometChat APNs provider. | +| Killed app doesn't ring for a VoIP push | `registerForVoIPPushes()` is called in `didFinishLaunchingWithOptions` **before** React Native starts, and nothing else in the app creates a `PKPushRegistry` (step 5.2). | +| iOS build fails with `'hermes/hermes.h' file not found` | The Podfile platform was lowered below React Native's minimum. Restore `platform :ios, min_ios_version_supported` and run `pod install`. | +| Accepted call connects but has no audio | The **Audio** background mode is enabled, and the Calls SDK (with `react-native-webrtc`) is installed. | +| Token registration errors | The provider IDs match the dashboard exactly, and `usePushOnLogin()` is rendered after CometChat is initialized. | +| No notification while the app is open | Expected with `showInForeground: false` (the default) — set it to `true`. For calls, `ringInForeground` decides whether the system call UI or your in-app screen rings. | +| Tapping a notification opens the app but not the conversation | `navigationRef` is passed to your `NavigationContainer`, navigation goes through `navigate()` from `navigationRef.ts`, and the route names in `SCREENS` match your navigator. | +| Thread reply opens an empty thread screen | The thread screen is given the user or group as well as the parent message, as `openFromNotification` does. | +| Handlers fire twice after logging out and in | Use `usePushOnLogin()` rather than calling `setupPushOnLogin()` directly — its cleanup must run on logout. | +| Notifications still arrive after logout | `unregister()` runs **before** logout and its failure isn't ignored. | diff --git a/notifications/react-native-push-notifications.mdx b/notifications/react-native-push-notifications.mdx deleted file mode 100644 index 74b57996f..000000000 --- a/notifications/react-native-push-notifications.mdx +++ /dev/null @@ -1,680 +0,0 @@ ---- -title: "React Native" -description: "Add CometChat push notifications and VoIP calls to a React Native app (Android + iOS) with the drop-in @cometchat/push-notifications-react-native package." ---- - - - -| Field | Value | -| --- | --- | -| Goal | Add push notifications and VoIP calling (FCM on Android, APNs + PushKit on iOS) to a React Native app | -| Package | `@cometchat/push-notifications-react-native` | -| Peer dependencies | `@cometchat/chat-sdk-react-native` (initialized **and logged in** before `init()`); `@cometchat/calls-sdk-react-native` for calls | -| Import | `import { CometChatPushNotifications, CometChatPNHelper, registerBackgroundCallTask } from "@cometchat/push-notifications-react-native";` | -| Files you add | `src/AppCredentials.ts` (provider IDs) · `src/navigation/navigationRef.ts` (`navigationRef`, `whenNavigationReady`, `navigate`) · `src/push/pushNotifications.ts` (`setupPushOnLogin`, `usePushOnLogin`) · `src/push/logout.ts` | -| Android setup | Apply the `com.google.gms.google-services` plugin (with `google-services.json` in `android/app/`); `minSdkVersion 24`+; add a monochrome `ic_notification` drawable. **No manifest changes** — the package declares its services, activities and permissions. Do **not** add `firebase-messaging` (the package brings it). Remove other push libraries (`@react-native-firebase/messaging`, `notifee`, `react-native-callkeep`, `react-native-voip-push-notification`). | -| iOS setup | Keep the Podfile's `platform :ios, min_ios_version_supported` (never lower it); `pod install`. Enable **Push Notifications** + **Background Modes** (*Voice over IP*, *Remote notifications*, *Audio*). Add `NSMicrophoneUsageDescription` + `NSCameraUsageDescription`. | -| iOS AppDelegate | Swift only. In `didFinishLaunchingWithOptions`, call `CometChatPushNotificationsAppDelegate.registerForVoIPPushes()` **before** starting React Native — the package owns the PushKit registry and reports calls to CallKit. Forward the APNs token with `.didRegisterAPNsToken(_:)`, and optionally background pushes with `.didReceiveRemoteNotification(_:)`. Don't create a `PKPushRegistry` or set a `UNUserNotificationCenter` delegate — the package owns both. | -| index.js | `registerBackgroundCallTask();` at module scope — the package rejects a call declined while the Android app is killed. Optional `registerBackgroundCallTask(handler, { rejectDeclinedCalls })`: the handler runs **after** the package's reject; `rejectDeclinedCalls: false` hands the reject to the handler. No-op on iOS. | -| Initialize | Call `usePushOnLogin()` once in a component rendered **after** `CometChat.init` resolves. It runs `setupPushOnLogin()` after a fresh login **and** a restored session, and its cleanup on logout. `setupPushOnLogin()` subscribes the handlers **before** `init()`, requests permissions in order, then `await CometChatPushNotifications.init(config)`. | -| Config (`PNConfiguration`) | `fcmProviderId?`, `apnsProviderId?`, `notificationSmallIcon?`, `androidChannelId?` (default `"cometchat_messages"`), `androidChannelName?` (default `"Messages"`), `voip?` (default `true`), `showInForeground?` (default `false`; chat notifications only), `ringInForeground?` (default `true`: calls ring with CallKit / the Android ringing screen even while the app is open; set `false` when the app shows its own incoming-call screen, as the UI Kit does) | -| Permissions | `CometChatPNHelper.requestNotificationPermission(): Promise` (resolves on the user's answer; **rejects** with `ERR_NO_ACTIVITY` / `ERR_PERMISSION_IN_FLIGHT` / `ERR_ACTIVITY_NOT_PERMISSION_AWARE` when it could not ask — catch it, don't skip `init()`) · `.hasNotificationPermission()` · `.requestCallPermissions()` (mic + camera). Await each before the next. | -| Token registration | Automatic in `init()`, including refreshes. Manual: `CometChatPushNotifications.registerToken(platform, token)`, `PNTokenPlatform` = `"fcm"` \| `"apns"` \| `"voip"` | -| Events (each returns `Unsubscribe`) | `onNotificationTap(cb)` · `onCallAccepted(cb)` (call already accepted) · `onCallEnded(cb)` · `onMessageReceived(cb)` | -| Notification tap | Wait for the navigator (`whenNavigationReady()`), mark the conversation read, then open `ThreadView` with the parent message **and** the user/group when `parentMessageId` is set (fall back to the conversation), else `Messages` with the user/group. Group taps use `info.receiver` (the GUID). | -| Call ended | End the call on the server (`CometChat.endCall`), end the media session (`CometChatCalls.endSession()`), `CometChat.clearActiveCall()`, and leave the call screen — a call ended from the iOS call screen isn't seen by the Calls SDK. | -| Logout | `await CometChatPushNotifications.unregister()` **before** `CometChatUIKit.logout()` / `CometChat.logout()` (it needs the auth token); if it fails, don't log out. `usePushOnLogin()` removes the handlers when logout completes. | -| `PNNotificationTapInfo` | `receiverType?` · `sender?` · `senderName?` · `receiver?` · `conversationId?` · `messageId?` · `parentMessageId?` (thread replies) | -| `PNCallInfo` | `sessionId` · `callerUid?` · `callerName?` · `callerAvatar?` · `receiverId?` · `receiverType?` · `callType?` · `callAction?` | -| `PNCallEndEvent` | `sessionId` · `reason?` (`"cancelled"` \| `"unanswered"` \| `"rejected"` \| `"busy"` \| `"ended"`) | -| Coexist with existing push | `CometChatPNHelper.isCometChatNotification(data): boolean` | -| Exported types | `PNConfiguration` · `PNCallInfo` · `PNCallEndEvent` · `PNNotificationTapInfo` · `PNTokenPlatform` · `PNBackgroundCallAction` · `PNBackgroundCallOptions` · `PNReceiverType` · `PNCallType` · `PNCallEndReason` · `PNTokenEvent` | -| Android permissions | Declared by the package: `POST_NOTIFICATIONS`, `USE_FULL_SCREEN_INTENT`, `FOREGROUND_SERVICE`, `FOREGROUND_SERVICE_PHONE_CALL`, `MANAGE_OWN_CALLS`, `WAKE_LOCK`, `VIBRATE`, `RECORD_AUDIO`, `CAMERA`, `BLUETOOTH_CONNECT`. `MANAGE_OWN_CALLS` is required for the ringing foreground service on Android 14+; it is auto-granted and registers **no** calling account. | -| Related | [Push Overview](/notifications/push-overview) · [Templates & Sounds](/notifications/templates-and-sounds) · [Badge Count](/notifications/badge-count) | - - - -## What this guide covers - -- Adding the `@cometchat/push-notifications-react-native` package and initializing it. -- Platform wiring: Firebase/`google-services.json` on Android, capabilities and a few `AppDelegate` lines on iOS. -- Requesting permission and registering tokens (FCM on Android, APNs + VoIP on iOS) after login. -- Receiving pushes and letting the package render chat notifications and full-screen / CallKit calls. -- Handling notification taps (including thread deep-links), incoming-call navigation, and Android OEM permissions. -- Testing and troubleshooting. - - -The `@cometchat/push-notifications-react-native` package replaces the previous approach of copying the sample app's `notifications` stack and hand-wiring `@react-native-firebase/messaging`, `notifee`, `react-native-callkeep`, and `react-native-voip-push-notification`. Token registration, foreground presentation, notification taps, and the full incoming-call experience (the Android lock-screen call activity and iOS CallKit) are handled inside the package — the design is **JS-first**: native code only shows the UI and captures tokens, while every CometChat action (register token, accept/reject/end call) runs in JavaScript through the Chat SDK your app already ships. - - -## How it works - -- **Android (FCM):** Firebase issues the registration token and delivers the CometChat payload as a data message. The package ships its **own** `FirebaseMessagingService`, so it receives the message and shows the notification or full-screen call itself — **you write no FCM handling code**. -- **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 package. Your `AppDelegate` starts the package's PushKit registry and forwards the APNs token (step 2). -- **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 package's role:** it retrieves the tokens, registers them with CometChat, parses payloads, drives the call UI, and calls the Chat SDK to accept/reject/end. It requires [`@cometchat/chat-sdk-react-native`](https://www.npmjs.com/package/@cometchat/chat-sdk-react-native) as a peer dependency — the one Chat SDK your app already uses, so there is no second SDK to version-align. - -## Prerequisites - -- The providers, Firebase project, and Apple/APNs credentials from **[Getting Started](/notifications/push-overview)** (this guide assumes those are done). -- React Native **0.65+**, an app already initializing and logging in with `@cometchat/chat-sdk-react-native` or the UI Kit, and `@cometchat/calls-sdk-react-native` for calls. The code below uses the UI Kit's screens and React Navigation — rename the screens to yours. -- **Android:** `google-services.json` in `android/app/`, the `com.google.gms.google-services` plugin, `minSdkVersion 24`+. -- **iOS:** iOS 13.0+. React Native already sets a higher minimum in your Podfile — keep it. -- A physical device — background delivery, full-screen calls, and VoIP pushes are unreliable on emulators/simulators. - - -**Complete the [Getting Started](/notifications/push-overview) guide first** — enable Push Notifications, add your providers (FCM for Android; APNs for iOS — the package registers both the device token and the VoIP token with that one APNs provider), and finish the Firebase/Apple setup. This guide covers only the React Native app wiring. - - -## 1. Store your credentials - -Create `src/AppCredentials.ts` with the values from Getting Started. Only the fields for the platforms you ship are needed: - -```ts src/AppCredentials.ts lines -export const AppCredentials = { - appId: 'YOUR_APP_ID', - region: 'YOUR_REGION', - authKey: 'YOUR_AUTH_KEY', - - // Android — the FCM provider ID from the CometChat dashboard - fcmProviderId: 'FCM-PROVIDER-ID', - - // iOS — one APNs provider covers both the device token and the VoIP token - apnsProviderId: 'APNS-PROVIDER-ID', -}; -``` - -## 2. Add the package and configure the platform - -Install the package (the Chat SDK and Calls SDK are already in your app): - -```bash -npm install @cometchat/push-notifications-react-native -cd ios && pod install && cd .. -``` - - -**Remove any other push library first** — `@react-native-firebase/messaging`, `@notifee/react-native`, `react-native-callkeep`, `react-native-voip-push-notification` — along with their code and native setup. Each registers its own push handler or call registry, and every notification or call then arrives twice. - - - - - With `google-services.json` already in `android/app/` (from [Getting Started](/notifications/push-overview)): - - 1. Apply the Google Services plugin — it reads `google-services.json` at build time. The package already depends on `firebase-messaging`, so don't add it yourself: - - ```groovy lines - // android/build.gradle - buildscript { - dependencies { - classpath("com.google.gms:google-services:4.4.2") - } - } - ``` - - ```groovy lines - // android/app/build.gradle - apply plugin: "com.google.gms.google-services" - ``` - - 2. Keep `minSdkVersion 24` or higher. - - 3. Add the status-bar notification icon: a **white-on-transparent** drawable named `ic_notification` — for example `android/app/src/main/res/drawable/ic_notification.png`. Android Studio generates one: right-click `res` → **New → Image Asset**, icon type **Notification Icons**, name `ic_notification`. Without it, the package falls back to your launcher icon, which the status bar draws as a plain white shape. - - - You do **not** edit `AndroidManifest.xml` and you write **no** FCM code. The package's manifest contributes the `FirebaseMessagingService`, the incoming-call foreground service, the full-screen `CallRingingActivity`, the notification action receivers, and every permission it needs — merged into your app by Gradle. - - - - 1. **Keep** the Podfile's `platform :ios, min_ios_version_supported` from the React Native template. Don't lower it: current React Native requires iOS 15.1, and a lower platform fails the build — for example with `'hermes/hermes.h' file not found`. - - 2. In Xcode, select your app target → **Signing & Capabilities** and add: - - **Push Notifications** - - **Background Modes**, with **Voice over IP**, **Remote notifications**, and **Audio, AirPlay, and Picture in Picture** checked - - 3. Add the microphone and camera usage strings to `ios//Info.plist` — a call can't use either without them: - - ```xml lines - NSMicrophoneUsageDescription - Needed for voice and video calls - NSCameraUsageDescription - Needed for video calls - ``` - - 4. Replace `ios//AppDelegate.swift` with this — React Native's current template plus the push lines. Set `withModuleName` to your app's name: - - ```swift ios//AppDelegate.swift lines - import UIKit - import React - import React_RCTAppDelegate - import ReactAppDependencyProvider - import react_native_cometchat_push_notifications - - @main - class AppDelegate: UIResponder, UIApplicationDelegate { - var window: UIWindow? - var reactNativeDelegate: ReactNativeDelegate? - var reactNativeFactory: RCTReactNativeFactory? - - func application( - _ application: UIApplication, - didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? = nil - ) -> Bool { - let delegate = ReactNativeDelegate() - let factory = RCTReactNativeFactory(delegate: delegate) - delegate.dependencyProvider = RCTAppDependencyProvider() - reactNativeDelegate = delegate - reactNativeFactory = factory - - // VoIP calls: the package creates and owns the PushKit registry. Call it BEFORE - // starting React Native — when a call wakes a killed app, iOS terminates the app - // unless the call reaches CallKit within ~5 seconds. - CometChatPushNotificationsAppDelegate.registerForVoIPPushes() - - window = UIWindow(frame: UIScreen.main.bounds) - factory.startReactNative( - withModuleName: "YourAppName", // your app's registered name - in: window, - launchOptions: launchOptions - ) - return true - } - - // APNs device token — chat notifications - func application( - _ application: UIApplication, - didRegisterForRemoteNotificationsWithDeviceToken deviceToken: Data - ) { - CometChatPushNotificationsAppDelegate.didRegisterAPNsToken(deviceToken) - } - - // Optional — background data pushes reach onMessageReceived - func application( - _ application: UIApplication, - didReceiveRemoteNotification userInfo: [AnyHashable: Any], - fetchCompletionHandler completionHandler: @escaping (UIBackgroundFetchResult) -> Void - ) { - CometChatPushNotificationsAppDelegate.didReceiveRemoteNotification(userInfo) - completionHandler(.noData) - } - } - - class ReactNativeDelegate: RCTDefaultReactNativeFactoryDelegate { - override func sourceURL(for bridge: RCTBridge) -> URL? { - self.bundleURL() - } - - override func bundleURL() -> URL? { - #if DEBUG - return RCTBundleURLProvider.sharedSettings().jsBundleURL(forBundleRoot: "index") - #else - return Bundle.main.url(forResource: "main", withExtension: "jsbundle") - #endif - } - } - ``` - - You don't set a `UNUserNotificationCenter` delegate — the package installs its own at launch to handle foreground notifications and taps. - - - **Don't create a `PKPushRegistry` of your own.** The package owns it, and a second registry — yours or a library's — makes iOS deliver every VoIP push twice. If another library must own PushKit, skip `registerForVoIPPushes()` and forward that registry's `didUpdate` and `didReceiveIncomingPushWith` callbacks to `CometChatPushNotificationsAppDelegate.didUpdateVoIPToken(_:)` and `.didReceiveIncomingVoIPPush(_:)`, calling `completion()` after it. - - - - **Older Swift template** (an `RCTAppDelegate` subclass): call `registerForVoIPPushes()` before `return super.application(...)`, which starts React Native, and add the same token method. **Objective-C `AppDelegate.mm`:** the package's iOS entry points are Swift-only, so move the AppDelegate to Swift first — the [React Native Upgrade Helper](https://react-native-community.github.io/upgrade-helper/) shows the change. - - - - -## 3. Initialize the SDK - -This step adds two files and changes two. Copy the new files as they are — only the route names in `SCREENS` should need changing. - -**`index.js`** — register the package's background task at module scope, before any component renders: - -```js index.js lines -import { AppRegistry } from 'react-native'; -import { registerBackgroundCallTask } from '@cometchat/push-notifications-react-native'; -import App from './App'; -import { name as appName } from './app.json'; - -// Android: lets a FULLY KILLED app reject a call declined from its notification. The package -// does the work — this only registers its background task. (No-op on iOS.) -registerBackgroundCallTask(); - -AppRegistry.registerComponent(appName, () => App); -``` - -On Android this lets a **fully killed** app reject a call declined from its notification — the package re-initializes the Chat SDK with the settings `init()` saved and rejects the call. To add your own logic, pass a handler: it runs **after** the package has rejected the call, with the Chat SDK initialized and logged in. - -```js lines -registerBackgroundCallTask(async (action, info) => { - // Your logic, e.g. record the declined call. The Chat SDK is initialized and logged in here. - console.log('Declined call from', info.callerUid); -}); - -// Or reject it yourself instead: -// registerBackgroundCallTask(myHandler, { rejectDeclinedCalls: false }); -``` - -**`src/navigation/navigationRef.ts`** — a notification tap or answered call that **launched** the app arrives before your navigator exists, so every navigation waits for it: - -```ts src/navigation/navigationRef.ts lines -import { createNavigationContainerRef } from '@react-navigation/native'; - -/** Pass this to your . */ -export const navigationRef = createNavigationContainerRef(); - -/** - * Resolves once the NavigationContainer is mounted. A notification tap or answered call - * that LAUNCHED the app arrives before the navigator exists, and navigating then is - * silently dropped. The ref queues listeners added before it mounts. - */ -export function whenNavigationReady(): Promise { - if (navigationRef.isReady()) return Promise.resolve(); - return new Promise(resolve => { - const unsubscribe = navigationRef.addListener('ready', () => { - unsubscribe(); - resolve(); - }); - }); -} - -/** Navigate by route name once the navigator is ready. */ -export async function navigate(name: string, params?: object): Promise { - await whenNavigationReady(); - (navigationRef.navigate as (name: string, params?: object) => void)(name, params); -} -``` - -**`src/push/pushNotifications.ts`** — everything push does for the logged-in user: - -```ts src/push/pushNotifications.ts lines -import { useEffect, useState } from 'react'; -import { CometChat } from '@cometchat/chat-sdk-react-native'; -import { CometChatCalls } from '@cometchat/calls-sdk-react-native'; -import { CometChatUIEventHandler, MessageEvents } from '@cometchat/chat-uikit-react-native'; -import { - CometChatPNHelper, - CometChatPushNotifications, - type PNCallEndEvent, - type PNCallInfo, - type PNNotificationTapInfo, -} from '@cometchat/push-notifications-react-native'; - -import { AppCredentials } from '../AppCredentials'; -import { navigate, navigationRef } from '../navigation/navigationRef'; - -/** Your navigator's route names — these are the CometChat UI Kit sample app's. */ -const SCREENS = { - messages: 'Messages', - thread: 'ThreadView', - ongoingCall: 'OngoingCallScreen', - home: 'BottomTabNavigator', -} as const; - -const LOGIN_LISTENER_ID = 'push-notifications-login'; - -/** - * Starts push for the logged-in user. Call it from React with `usePushOnLogin()` (below) - * rather than directly: it returns a cleanup that must run on logout, or every handler - * fires twice after the next login. - */ -export function setupPushOnLogin(): () => void { - // Subscribe BEFORE init(): the tap or answered call that LAUNCHED the app is delivered - // as soon as init() runs. - const unsubscribes = [ - CometChatPushNotifications.onNotificationTap(openFromNotification), - CometChatPushNotifications.onCallAccepted(openCallScreen), - CometChatPushNotifications.onCallEnded(endCall), - ]; - - const start = async () => { - // Await each permission request before the next — Android allows only one pending - // request per activity. A rejection means the OS could not be asked (not that the user - // declined), and must not stop init(): the push token still has to register. - await CometChatPNHelper.requestNotificationPermission().catch(() => false); - await CometChatPNHelper.requestCallPermissions(); // mic + camera, needed before a call connects - - await CometChatPushNotifications.init({ - fcmProviderId: AppCredentials.fcmProviderId, // Android - apnsProviderId: AppCredentials.apnsProviderId, // iOS (APNs device + VoIP) - notificationSmallIcon: 'ic_notification', // Android status-bar icon (step 2) - showInForeground: true, // one notification while the app is open, too - ringInForeground: false, // the UI Kit shows its own incoming-call screen while open - }); - }; - start().catch(error => console.log('Push setup failed:', error)); - - return () => unsubscribes.forEach(unsubscribe => unsubscribe()); -} - -/** - * Runs push while a user is logged in — after a fresh login AND after a session restored - * on launch — and cleans up on logout. Use it once, in a component rendered after - * CometChat has been initialized. - */ -export function usePushOnLogin(): void { - const [loggedIn, setLoggedIn] = useState(false); - - useEffect(() => { - // A restored session never fires loginSuccess, so check once on mount. - CometChat.getLoggedinUser() - .then(user => setLoggedIn(!!user)) - .catch(() => setLoggedIn(false)); - - CometChat.addLoginListener( - LOGIN_LISTENER_ID, - new CometChat.LoginListener({ - loginSuccess: () => setLoggedIn(true), - logoutSuccess: () => setLoggedIn(false), - }), - ); - return () => CometChat.removeLoginListener(LOGIN_LISTENER_ID); - }, []); - - useEffect(() => { - if (!loggedIn) return; - return setupPushOnLogin(); - }, [loggedIn]); -} - -/** Open the thread for a thread reply, otherwise the conversation. */ -async function openFromNotification(info: PNNotificationTapInfo): Promise { - const isGroup = info.receiverType === 'group'; - try { - const user = !isGroup && info.sender ? await CometChat.getUser(info.sender) : undefined; - const group = isGroup && info.receiver ? await CometChat.getGroup(info.receiver) : undefined; - if (!user && !group) return; - - markConversationRead(isGroup ? info.receiver! : info.sender!, isGroup); - - if (info.parentMessageId) { - try { - const parent = await CometChat.getMessageDetails(info.parentMessageId); - // The thread screen needs the user or group, not just the parent message. - await navigate(SCREENS.thread, { message: parent, user, group, highlightMessageId: info.messageId }); - return; - } catch (error) { - console.log('Could not open the thread, opening the conversation:', error); - } - } - await navigate(SCREENS.messages, { user, group }); - } catch (error) { - console.log('Could not open the conversation from a notification:', error); - } -} - -/** Mark the conversation read and clear its unread badge in the UI Kit's conversation list. */ -function markConversationRead(conversationWith: string, isGroup: boolean): void { - const type = isGroup ? CometChat.RECEIVER_TYPE.GROUP : CometChat.RECEIVER_TYPE.USER; - CometChat.markConversationAsRead(conversationWith, type) - .then(() => CometChat.getConversation(conversationWith, type)) - .then(conversation => { - const lastMessage = conversation.getLastMessage(); - if (lastMessage) { - CometChatUIEventHandler.emitMessageEvent(MessageEvents.ccMessageRead, { message: lastMessage }); - } - }) - .catch(error => console.log('Could not mark the conversation read:', error)); -} - -/** The package has already accepted the call — just show the call screen. */ -function openCallScreen(info: PNCallInfo): void { - navigate(SCREENS.ongoingCall, { sessionId: info.sessionId, callType: info.callType }); -} - -/** - * A ringing call was cancelled or declined, or the user ended the call from the iOS call - * screen — which the Calls SDK does not see, so tear the call down here. - */ -function endCall(info: PNCallEndEvent): void { - if (info.sessionId) CometChat.endCall(info.sessionId).catch(() => {}); - try { - CometChatCalls.endSession(); - } catch {} - try { - CometChat.clearActiveCall(); - } catch {} - if (navigationRef.isReady() && navigationRef.getCurrentRoute()?.name === SCREENS.ongoingCall) { - navigate(SCREENS.home); - } -} -``` - - -Not using the UI Kit? Delete the `@cometchat/chat-uikit-react-native` import and the `emitMessageEvent` block in `markConversationRead`, and point `SCREENS` and the route params at your own screens. In `logout.ts` (step 5), call `CometChat.logout()` instead of `CometChatUIKit.logout()`. - - -**`App.tsx`** — call `usePushOnLogin()` once, in a component that renders **after** CometChat is initialized, and pass `navigationRef` to your `NavigationContainer`: - -```tsx App.tsx lines -import React, { useEffect, useState } from 'react'; -import { NavigationContainer } from '@react-navigation/native'; -import { CometChat } from '@cometchat/chat-sdk-react-native'; -import { CometChatUIKit, UIKitSettings } from '@cometchat/chat-uikit-react-native'; - -import { AppCredentials } from './AppCredentials'; -import { navigationRef } from './navigation/navigationRef'; -import { usePushOnLogin } from './push/pushNotifications'; - -export default function App() { - const [initialized, setInitialized] = useState(false); - - useEffect(() => { - // Your existing CometChat initialization. - CometChatUIKit.init({ - appId: AppCredentials.appId, - region: AppCredentials.region, - authKey: AppCredentials.authKey, - subscriptionType: CometChat.AppSettings.SUBSCRIPTION_TYPE_ALL_USERS as UIKitSettings['subscriptionType'], - } as UIKitSettings) - .then(() => setInitialized(true)) - .catch(error => console.log('CometChat init failed:', error)); - }, []); - - // Push must start only after CometChat is initialized. - if (!initialized) return null; - return ; -} - -function Root() { - usePushOnLogin(); // push follows login and logout from here on - - return ( - - {/* your existing navigator */} - - ); -} -``` - -`usePushOnLogin()` starts push after a fresh login **and** when a session is restored on launch, and removes the handlers when the user logs out — so a later login never registers them twice. `init()` registers the device tokens (and re-registers them when they refresh) and delivers the tap or answered call the app was launched from. - -`init()` also accepts `voip` (default `true`), `androidChannelId`, and `androidChannelName`. `showInForeground` defaults to `false` and affects chat notifications only. `ringInForeground` decides how a call rings while the app is open: `true` (the default) rings with CallKit on iOS and the ringing screen on Android; `false`, used here, leaves it to the UI Kit's own incoming-call screen so the call doesn't ring twice. - -## 4. Request permission and register tokens - -`setupPushOnLogin()` already requests both permissions, in order, before `init()`. To check or request them elsewhere: - -```ts lines -const granted = await CometChatPNHelper.requestNotificationPermission().catch(() => false); // Android 13+ / iOS -await CometChatPNHelper.requestCallPermissions(); // mic + camera -const enabled = await CometChatPNHelper.hasNotificationPermission(); // checks without prompting -``` - - -**Always `await` one permission request before starting the next.** Android allows only one pending request per activity: a second request cancels the dialog still on screen, and the OS reports it as denied without the user seeing it. On a fresh install that leaves the app with no notification permission — pushes arrive and are dropped. - - -`requestNotificationPermission()` resolves `true`/`false` on the user's answer. It **rejects** when the permission could not be requested at all — a different situation from the user declining, and not a reason to skip `init()`: - -| Rejection | Meaning | -| --- | --- | -| `ERR_NO_ACTIVITY` | No foreground activity, so no dialog can be shown. Retry when the app is foreground. | -| `ERR_PERMISSION_IN_FLIGHT` | A request is already open. Await that one instead. | -| `ERR_ACTIVITY_NOT_PERMISSION_AWARE` | Your host activity does not extend `ReactActivity`. Fix the activity. | - -**Token registration is automatic.** You rarely need it, but you can register a token yourself: - -```ts lines -await CometChatPushNotifications.registerToken('fcm', token); // or 'apns' / 'voip' -``` - -## 5. Unregister the token on logout - -Add `src/push/logout.ts` and call it from your logout button instead of logging out directly: - -```ts src/push/logout.ts lines -import { CometChatUIKit } from '@cometchat/chat-uikit-react-native'; -import { CometChatPushNotifications } from '@cometchat/push-notifications-react-native'; - -/** Log out and stop this device receiving the user's notifications. Resolves false on failure. */ -export async function logout(): Promise { - // Unregister BEFORE logout: it needs the session's auth token, so after logout it fails - // and the device keeps receiving notifications for the user who just logged out. - try { - await CometChatPushNotifications.unregister(); - } catch (error) { - console.log('Failed to unregister the push token:', error); - return false; - } - try { - await CometChatUIKit.logout(); - return true; - } catch (error) { - console.log('Logout failed:', error); - return false; - } -} -``` - -```tsx lines -const onLogoutPress = async () => { - if (loggingOut) return; // ignore a second tap while logging out - setLoggingOut(true); - const loggedOut = await logout(); - setLoggingOut(false); - if (loggedOut) navigation.navigate('Login'); // your login screen -}; -``` - - -`unregister()` must run **before** logout. It needs the session's auth token — after logout it fails, and the device keeps receiving notifications for the user who just logged out. - - -When logout completes, `usePushOnLogin()` removes the push handlers for you. - -## 6. Notification taps and call events - -`src/push/pushNotifications.ts` from step 3 handles all of these. What each one does, and why: - -| Event | What happens | -| --- | --- | -| **Notification tap** | Waits for the navigator, marks the conversation read (clearing its unread badge), then opens the **thread** for a thread reply — with the parent message *and* the user or group, which the thread screen needs — or the conversation. If the thread can't be opened, it falls back to the conversation. | -| **Call accepted** | The package has **already** accepted the call with the Chat SDK; the app only opens its call screen. | -| **Call ended** | A ringing call was cancelled or declined, or the user ended the call from the iOS call screen. The Calls SDK doesn't see a call ended there, so the handler ends it on the server, ends the media session, and leaves the call screen. | -| **Launched by a tap or an answered call** | Delivered as soon as `init()` runs — before the navigator mounts — which is why the handlers are subscribed before `init()` and every navigation waits for `whenNavigationReady()`. | - - -**Killed-app VoIP (iOS):** when a VoIP push wakes a killed app, the package reports the call to CallKit immediately, before React Native is ready. When the user answers, the app starts, `init()` delivers the answered call, and `onCallAccepted` opens your call screen — the call is already accepted. This is why `registerForVoIPPushes()` runs before React Native starts (step 2). - - -## 7. Android: OEM permissions for lock-screen calls - -The package declares the standard permissions and uses the correct `setShowWhenLocked` / `setTurnScreenOn` flags, so full-screen calls over the lock screen work out of the box on stock Android (including Android 14+). **OEM skins (MIUI/Redmi/POCO, Oppo, Vivo) additionally gate background-launched full-screen activities** behind their own toggles — without them, a locked/killed call shows only a heads-up notification (with ringtone), and the full-screen screen appears only after unlock. - -Guide users to grant, on those devices: - -- **Autostart** — Settings → Apps → *your app* → Autostart (or the Security app). -- **Display pop-up windows while running in background** — Settings → Apps → *your app* → Other permissions. -- **Show on lock screen** — same "Other permissions" screen. -- Disable **battery optimization** for the app. - -These OEM settings cannot be granted programmatically (the OS blocks it); open the app's settings page so the user can toggle them: - -```ts lines -import { Linking, Platform } from 'react-native'; -if (Platform.OS === 'android') Linking.openSettings(); -``` - -## 8. Badge count - -CometChat's Enhanced Push Notification payload includes an `unreadMessageCount` field (the total unread across all conversations). Enable it once on the dashboard: - -1. Go to **CometChat Dashboard → Notification Engine → Settings → Preferences → Push Notification Preferences**. -2. Enable the **Unread Badge Count** toggle. - - - - Android has no OS-level app-icon badge API. If you want a launcher badge, read `unreadMessageCount` from the payload in `onMessageReceived` and apply it with your own badge library — the push package does not manage launcher badges. - - ```ts lines - CometChatPushNotifications.onMessageReceived(data => { - const count = Number(data.unreadMessageCount ?? 0); - // hand `count` to your badge library - }); - ``` - - - With 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. - - - -## 9. Testing checklist - -Use physical devices. The iOS Simulator can't receive APNs or VoIP pushes, and emulators often delay FCM. Test call timing on a **release** build: a debug build loads its JavaScript from Metro, which delays the first JavaScript that runs in a killed app. - -1. **Fresh install:** install, log in, and confirm the notification prompt **waits** for your answer before the microphone/camera prompt appears. Then send a message from another user — it must arrive. -2. **Chat notifications:** - - App open: exactly **one** notification (`showInForeground: true`). - - App in the background: a notification appears; tapping it opens the conversation. - - App killed: tapping the notification starts the app **in** the conversation. - - A thread reply opens the **thread**; a group message opens the group. -3. **Calls, app killed, device locked:** - - Android shows the **full-screen ringing screen** with Accept/Decline; iOS shows CallKit. - - **Accept** connects the call with audio both ways. - - **Decline** shows the call as rejected on the caller's side. - - The caller **cancelling** stops the ring. -4. **Calls, app in the background:** accept and decline both work, and ending the call from the iOS call screen closes your call screen. -5. **Logout:** log out, send a message from another user — nothing arrives. Log in as another user — only that user's notifications arrive, and a tap opens its conversation once. -6. **OEM devices** (MIUI, Oppo, Vivo): grant the section 7 permissions and re-check locked and killed calls. - -## 10. Troubleshooting - -| Symptom | Platform | Quick checks | -| --- | --- | --- | -| No notifications received | Android | `google-services.json` is in `android/app/`, its package name matches the app, the `google-services` plugin is applied, and `POST_NOTIFICATIONS` is granted (Android 13+). | -| Notifications work on an existing install but not on a fresh one | Android | A permission request wasn't awaited, so the notification dialog was cancelled unseen (step 4). `adb shell dumpsys package \| grep POST_NOTIFICATIONS`: `granted=false` with no `USER_SET` flag means the dialog was never answered. | -| Two notifications for one message | Android | Another push library is still installed — usually `@react-native-firebase/messaging`, whose receiver handles every push too. Remove the other push libraries (step 2). | -| Incoming call is a plain notification, not the full-screen ringing screen | Android | Check the merged manifest still has `MANAGE_OWN_CALLS` and `FOREGROUND_SERVICE_PHONE_CALL` (nothing strips them with `tools:node="remove"`). On MIUI, Oppo and Vivo, grant the section 7 permissions. For Google Play builds, complete the **full-screen intent** declaration in Play Console — without it, Play revokes `USE_FULL_SCREEN_INTENT` on Android 14+. | -| Declining a call in a killed app doesn't reject it | Android | `registerBackgroundCallTask()` is called at module scope in `index.js`, and the app has been opened and logged in once since installing (so `init()` saved the Chat SDK settings). In a debug build the reject can take ~20 s — test on a release build. | -| Killed app doesn't ring for a VoIP push | iOS | `registerForVoIPPushes()` is called in `didFinishLaunchingWithOptions`, **before** React Native starts, and nothing else in the app creates a `PKPushRegistry` (step 2). | -| iOS build fails with `'hermes/hermes.h' file not found` | iOS | The Podfile platform was lowered below React Native's minimum. Restore `platform :ios, min_ios_version_supported` and run `pod install`. | -| Accepted call connects but has no audio | iOS | The **Audio** background mode is enabled and the Calls SDK (with `react-native-webrtc`) is installed. | -| No VoIP pushes | iOS | Push Notifications + Background Modes (Voice over IP) are enabled, `aps-environment` matches the build (`production` for release), and the bundle ID matches the CometChat APNs provider. | -| No notification while the app is open | Both | Expected with `showInForeground: false` (the default). Set it to `true`. For calls, `ringInForeground` decides whether the system call UI or your in-app screen rings. | -| Tapping a notification opens the app but not the conversation | Both | `navigationRef` is passed to your `NavigationContainer`, and navigation goes through `navigate()` from `navigationRef.ts`, which waits for the navigator. The route names in `SCREENS` match your navigator. | -| Thread reply opens an empty thread screen | Both | The thread screen is given the user or group as well as the parent message (step 3 does this). | -| Handlers fire twice after logging out and in | Both | Use `usePushOnLogin()` rather than calling `setupPushOnLogin()` directly — its cleanup must run on logout. | -| Notifications still arrive after logout | Both | `unregister()` runs **before** logout and its failure isn't ignored (step 5). | -| Token registration errors | Both | The provider IDs match the dashboard exactly, and `usePushOnLogin()` is rendered after CometChat is initialized. | - -## Resources - - - - The drop-in push & VoIP package on npm. - - - The peer Chat SDK the package registers tokens and drives calls through. - - diff --git a/sdk/react-native/push-notification-html-stripping.mdx b/sdk/react-native/push-notification-html-stripping.mdx index 77d88a68b..d08f3dde9 100644 --- a/sdk/react-native/push-notification-html-stripping.mdx +++ b/sdk/react-native/push-notification-html-stripping.mdx @@ -249,8 +249,11 @@ If your project uses React Native Firebase (e.g., for FCM on Android), you may a ## Next Steps - -Set up FCM (Android) and APNs/VoIP (iOS) push notifications + +Set up FCM push notifications for Android + + +Set up APNs push notifications for iOS Learn how to send different types of messages From 6ebe8f66c3eedf78dcc69d1760ab8b8fe5228742 Mon Sep 17 00:00:00 2001 From: Suraj Chauhan Date: Thu, 17 Sep 2026 18:56:06 +0530 Subject: [PATCH 15/20] =?UTF-8?q?docs(react-native):=20address=20review=20?= =?UTF-8?q?=E2=80=94=20shared-code=20note,=20icon=20comment,=20payload=20t?= =?UTF-8?q?ypes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Both pages say the JavaScript files are shared by Android and iOS, and which lines apply only on Android (registerBackgroundCallTask, notificationSmallIcon, waiting between permission requests). - The init() comment no longer points notificationSmallIcon at "step 2", which is the credentials step on both pages. - The iOS payload example shows unreadMessageCount as a string, like CometChat's native iOS APNs guide, and both pages note that the package hands payload values to JavaScript as strings. Co-Authored-By: Claude Opus 5 --- .../react-native-push-notifications-android.mdx | 8 +++++++- notifications/react-native-push-notifications-ios.mdx | 10 +++++++--- 2 files changed, 14 insertions(+), 4 deletions(-) diff --git a/notifications/react-native-push-notifications-android.mdx b/notifications/react-native-push-notifications-android.mdx index c08432df1..c39210f61 100644 --- a/notifications/react-native-push-notifications-android.mdx +++ b/notifications/react-native-push-notifications-android.mdx @@ -99,6 +99,10 @@ npm install @cometchat/push-notifications-react-native ### 3.2 Wire the entry points + +The JavaScript below is the same for Android and iOS — one set of files serves both guides. Lines for one platform do nothing on the other: `registerBackgroundCallTask()` and `notificationSmallIcon` only apply on Android, and waiting for each permission answer before the next request matters only on Android. + + **`index.js`** — register the package's background task at module scope. It lets a **fully killed** app reject a call declined from its notification: the package re-initializes the Chat SDK with the settings `init()` saved, and rejects the call. ```js index.js lines @@ -208,7 +212,7 @@ export function setupPushOnLogin(): () => void { await CometChatPushNotifications.init({ fcmProviderId: AppCredentials.fcmProviderId, // Android apnsProviderId: AppCredentials.apnsProviderId, // iOS (APNs device + VoIP) - notificationSmallIcon: 'ic_notification', // Android status-bar icon (step 2) + notificationSmallIcon: 'ic_notification', // Android status-bar icon showInForeground: true, // one notification while the app is open, too ringInForeground: false, // the UI Kit shows its own incoming-call screen while open }); @@ -617,6 +621,8 @@ CometChat sends FCM data messages with this structure (relevant fields): } ``` +`unreadMessageCount` is a string; the package hands every payload value to JavaScript as a string, so convert it with `Number()` before use. + ### 6.3 Update the app badge from the push payload Android has no OS-level app icon badge API, and the push package doesn't manage launcher badges. While the app is open, `onMessageReceived` receives each payload — hand `unreadMessageCount` to a launcher-badge library. In the background the package shows the notification, and Android's notification dot marks the app icon. diff --git a/notifications/react-native-push-notifications-ios.mdx b/notifications/react-native-push-notifications-ios.mdx index 7374f4cd9..5bebb86fb 100644 --- a/notifications/react-native-push-notifications-ios.mdx +++ b/notifications/react-native-push-notifications-ios.mdx @@ -102,6 +102,10 @@ Keep the Podfile's `platform :ios, min_ios_version_supported` from the React Nat ### 4.2 Wire the entry points + +The JavaScript below is the same for Android and iOS — one set of files serves both guides. Lines for one platform do nothing on the other: `registerBackgroundCallTask()` and `notificationSmallIcon` only apply on Android, and waiting for each permission answer before the next request matters only on Android. + + **`index.js`** — the same file as on Android. `registerBackgroundCallTask()` is a no-op on iOS, where CallKit handles a decline in a killed app: ```js index.js lines @@ -199,7 +203,7 @@ export function setupPushOnLogin(): () => void { await CometChatPushNotifications.init({ fcmProviderId: AppCredentials.fcmProviderId, // Android apnsProviderId: AppCredentials.apnsProviderId, // iOS (APNs device + VoIP) - notificationSmallIcon: 'ic_notification', // Android status-bar icon (step 2) + notificationSmallIcon: 'ic_notification', // Android status-bar icon showInForeground: true, // one notification while the app is open, too ringInForeground: false, // the UI Kit shows its own incoming-call screen while open }); @@ -562,13 +566,13 @@ CometChat sends APNs payloads with this structure (relevant fields): "badge": 5, "sound": "default" }, - "unreadMessageCount": 5, + "unreadMessageCount": "5", "conversationId": "user_abc123", "parentId": "176001" // Optional - parent message ID; sent only for threaded notifications } ``` -The `aps.badge` field is set by CometChat server-side, so iOS updates the badge when the push is delivered. +The `aps.badge` field is set by CometChat server-side, so iOS updates the badge when the push is delivered. In JavaScript (`onMessageReceived`), the package hands `unreadMessageCount` over as a string, as on Android. ## 8. Testing checklist From 69292a2cbe6c54a260ce6da3194483a040f24cf4 Mon Sep 17 00:00:00 2001 From: Suraj Chauhan Date: Thu, 17 Sep 2026 21:16:40 +0530 Subject: [PATCH 16/20] docs(react-native): keep today's push guides at legacy URLs Customers on the current setup (Firebase Messaging + Notifee + CallKeep + react-native-voip-push-notification) keep their docs when the new guides go live: - react-native-push-notifications-android-legacy and -ios-legacy carry today's live pages unchanged, apart from a banner pointing to the new guide and cross-links that stay within the legacy pair. They are not added to the navigation. - The new Android and iOS pages link to their legacy guide. Co-Authored-By: Claude Opus 5 --- ...tive-push-notifications-android-legacy.mdx | 964 ++++++++++++++++++ ...eact-native-push-notifications-android.mdx | 4 + ...t-native-push-notifications-ios-legacy.mdx | 851 ++++++++++++++++ .../react-native-push-notifications-ios.mdx | 4 + 4 files changed, 1823 insertions(+) create mode 100644 notifications/react-native-push-notifications-android-legacy.mdx create mode 100644 notifications/react-native-push-notifications-ios-legacy.mdx diff --git a/notifications/react-native-push-notifications-android-legacy.mdx b/notifications/react-native-push-notifications-android-legacy.mdx new file mode 100644 index 000000000..c4002948c --- /dev/null +++ b/notifications/react-native-push-notifications-android-legacy.mdx @@ -0,0 +1,964 @@ +--- +title: "React Native Push Notification (Android) — Legacy" +description: "Bring the SampleAppWithPushNotifications experience—FCM + VoIP calls—into any React Native project using CometChat UI Kit." +--- + + +**Legacy guide.** This page covers the previous setup, which copies the sample app's notification stack (Firebase Messaging, Notifee, CallKeep). For new integrations, use the `@cometchat/push-notifications-react-native` package — see [React Native Push Notifications (Android)](/notifications/react-native-push-notifications-android). + + + + +| Field | Value | +| --- | --- | +| Platform | Android (FCM) | +| Key Classes | `CometChatNotifications`, `VoipNotificationHandler`, `PendingCallManager` | +| Key Methods | `registerPushToken()`, `unregisterPushToken()`, `messaging().getToken()` | +| Push Platform | `CometChatNotifications.PushPlatforms.FCM_REACT_NATIVE_ANDROID` | +| Prerequisites | CometChat SDK initialized, user logged in, FCM configured, `google-services.json` in `android/app` | + + + + + Reference implementation of React Native UI Kit, FCM and Push Notification Setup. + + +## What this guide covers + +- CometChat Dashboard setup (enable push, add FCM providers). +- Platform credentials (Firebase). +- 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. +- Token registration, navigation from pushes, testing, and troubleshooting. + +## What you need first + +- CometChat app credentials (App ID, Region, Auth Key) and Push Notifications enabled with an **FCM provider (React Native Android)**. +- Firebase project with an Android app (`google-services.json` in `android/app`) and Cloud Messaging enabled. +- React Native 0.81+, Node 18+, physical Android devices for reliable push/call testing. + +## How FCM + CometChat work together + +- **FCM (Android) is the transport:** Firebase issues the Android FCM token and delivers payloads to devices. +- **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) + +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 + +- Update `src/utils/AppConstants.tsx` with `appId`, `authKey`, `region`, and `fcmProviderId`. +- Keep `app.json` name consistent with your bundle ID / applicationId. + +```ts lines +const APP_ID = ""; +const AUTH_KEY = ""; +const REGION = ""; +const DEMO_UID = "cometchat-uid-1"; +``` + +### 3.1 Dependencies snapshot (from Sample App) + +Install these dependencies in your React Native app: + +```npm lines +npm install \ + @react-native-firebase/app@23.4.0 \ + @react-native-firebase/messaging@23.4.0 \ + @notifee/react-native@9.1.8 \ + @cometchat/chat-sdk-react-native@4.0.18 \ + @cometchat/calls-sdk-react-native@4.4.0 \ + @cometchat/chat-uikit-react-native@5.2.6 \ + @react-native-async-storage/async-storage@2.2.0 \ + react-native-callkeep@github:cometchat/react-native-callkeep \ + react-native-voip-push-notification@3.3.3 +``` + +Match these or newer compatible versions in your app. + +## 4. Android App Setup + +### 4.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. + +First, add the google-services plugin as a dependency inside of your `/android/build.gradle` file: + +```android lines +buildscript { + dependencies { + // ... other dependencies + classpath("com.google.gms:google-services:4.4.4") + } +} +``` + +Lastly, execute the plugin by adding the following to your `/android/app/build.gradle` file: + +```android lines +apply plugin: 'com.android.application' +apply plugin: 'com.google.gms.google-services' +``` + +### 4.2 Configure required permissions in `AndroidManifest.xml` as shown. + +```xml lines + + + + + + + + + + + + + + + + +``` + +and ask for runtime permissions where needed (e.g. `POST_NOTIFICATIONS` on Android 13+). + +```tsx lines +import { PermissionsAndroid, Platform } from "react-native"; + + const requestAndroidPermissions = async () => { + if (Platform.OS !== 'android') return; + + try { + // Ask for push‑notification permission + const authStatus = await messaging().requestPermission(); + const enabled = + authStatus === messaging.AuthorizationStatus.AUTHORIZED || + authStatus === messaging.AuthorizationStatus.PROVISIONAL; + + if (!enabled) { + console.warn('Notification permission denied (FCM).'); + } + } catch (error) { + console.warn('FCM permission request error:', error); + } + + try { + await PermissionsAndroid.requestMultiple([ + PermissionsAndroid.PERMISSIONS.WRITE_EXTERNAL_STORAGE, + PermissionsAndroid.PERMISSIONS.READ_EXTERNAL_STORAGE, + PermissionsAndroid.PERMISSIONS.CAMERA, + PermissionsAndroid.PERMISSIONS.RECORD_AUDIO, + PermissionsAndroid.PERMISSIONS.POST_NOTIFICATIONS, + ]); + } catch (err) { + console.warn('Android permissions error:', err); + } +} +``` + +### 4.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: + +```ts lines +requestAndroidPermissions(); + +const FCM_TOKEN = await messaging().getToken(); +console.log("FCM Token:", FCM_TOKEN); + +// For React Native Android +CometChatNotifications.registerPushToken( + FCM_TOKEN, + CometChatNotifications.PushPlatforms.FCM_REACT_NATIVE_ANDROID, + "YOUR_FCM_PROVIDER_ID" // from CometChat Dashboard + ) + .then(() => { + console.log("Token registration successful"); + }) + .catch((err) => { + console.log("Token registration failed:", err); + }); +``` + +### 4.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 + +These steps are Android-only—copy/paste and fill your IDs. + +### 5.1 Add CallKeep services to `android/app/src/main/AndroidManifest.xml` +Inside the `` tag add: + +```xml lines + + + + + + + +``` + +### 5.2 Background handler for call pushes (`index.js`) +Data-only FCM calls show the native dialer even when the app is killed. + +```js lines +import messaging from "@react-native-firebase/messaging"; +import { Platform } from "react-native"; +import { CometChat } from "@cometchat/chat-sdk-react-native"; +import { voipHandler } from "./VoipNotificationHandler"; +import { displayLocalNotification } from "./LocalNotificationHandler"; + +if (Platform.OS === "android") { + messaging().setBackgroundMessageHandler(async remoteMessage => { + const data = remoteMessage.data || {}; + if (data.type === "call") { + await voipHandler.initialize(); + switch (data.callAction) { + case "initiated": + voipHandler.msg = data; + await voipHandler.displayCallAndroid(); + break; + case "ended": + case "unanswered": + case "busy": + case "rejected": + case "cancelled": + CometChat.clearActiveCall(); + if (voipHandler?.callerId) { + voipHandler.removeCallDialerWithUUID(voipHandler.callerId); + } + await voipHandler.endCall({ callUUID: voipHandler.callerId }); + break; + case "ongoing": + voipHandler.displayNotification({ + title: data?.receiverName || "", + body: "ongoing call", + }); + break; + default: + break; + } + return; + } + await displayLocalNotification(remoteMessage); + }); +} +``` + +### 5.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 +import { Platform } from "react-native"; +import notifee, { AndroidImportance } from "@notifee/react-native"; +import RNCallKeep, { IOptions } from "react-native-callkeep"; +import { CometChat } from "@cometchat/chat-sdk-react-native"; +import { setPendingAnsweredCall } from "./PendingCallManager"; + +const options: IOptions = { + android: { + alertTitle: "VoIP permissions", + alertDescription: "Allow phone account access to show incoming calls", + cancelButton: "Cancel", + okButton: "OK", + imageName: "ic_notification", + additionalPermissions: [], + foregroundService: { + channelId: "com.cometchat.sampleapp.reactnative.android", + channelName: "Sampleapp Channel", + notificationTitle: "Sampleapp is running in the background", + }, + }, + ios: { appName: "Sampleapp" }, +}; + +function uuid() { + return "xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx".replace(/[xy]/g, (c) => { + const r = Math.floor(Math.random() * 16); + const v = c === "x" ? r : (r & 0x3) | 0x8; + return v.toString(16); + }); +} + +class VoipNotificationHandler { + channelId = ""; + isRinging = false; + isAnswered = false; + pendingAcceptance = false; + callerId = ""; + msg: any = {}; + initialized = false; + private setupPromise: Promise | null = null; + private listenersAttached = false; + + async initialize() { + if (this.initialized && this.setupPromise) { + await this.setupPromise; + return; + } + if (!this.setupPromise) { + this.setupPromise = (async () => { + if (Platform.OS === "android") { + await this.createNotificationChannel(); + } + await this.getPermissions(); + this.setupEventListeners(); + this.initialized = true; + })().catch((err) => { + this.setupPromise = null; + throw err; + }); + } + await this.setupPromise; + } + + async getPermissions() { + await RNCallKeep.setup(options); + RNCallKeep.setAvailable(true); + RNCallKeep.setReachable(); + try { + await RNCallKeep.checkPhoneAccountEnabled(); + } catch {} + } + + async createNotificationChannel() { + this.channelId = await notifee.createChannel({ + id: "message", + name: "Messages", + lights: true, + vibration: true, + importance: AndroidImportance.HIGH, + }); + } + + async displayNotification({ + title, + body, + data, + }: { + title: string; + body: string; + data?: any; + }) { + if (Platform.OS === "android" && !this.channelId) + await this.createNotificationChannel(); + await notifee.displayNotification({ + title, + body, + data, + android: this.channelId + ? { channelId: this.channelId, smallIcon: "ic_launcher" } + : undefined, + }); + } + + async displayCallAndroid() { + if (this.isAnswered || this.pendingAcceptance) return; + await this.initialize(); + this.isRinging = true; + this.callerId = uuid(); + const callerName = this.msg?.senderName || "Incoming Call"; + await RNCallKeep.displayIncomingCall( + this.callerId, + callerName, + callerName, + "generic", + ); + } + + onAnswerCall = async ({ callUUID }: { callUUID: string }) => { + if (this.isAnswered) return; + this.isRinging = false; + this.isAnswered = true; + const sessionID = this.msg?.sessionId; + if (!sessionID) return; + + setTimeout(async () => { + const loggedInUser = await CometChat.getLoggedinUser().catch(() => null); + if (!loggedInUser) { + this.pendingAcceptance = true; + await setPendingAnsweredCall({ + sessionId: sessionID, + raw: this.msg, + storedAt: Date.now(), + }); + try { + RNCallKeep.backToForeground(); + } catch (err) { + // Activity may not exist yet if app was killed - the pending call will be handled when app opens + console.log( + "[VoIP] backToForeground failed, pending call saved:", + err, + ); + } + return; + } + try { + await CometChat.acceptCall(sessionID); + } catch (error: any) { + if (error?.code !== "ERR_CALL_USER_ALREADY_JOINED") throw error; + } + RNCallKeep.endAllCalls(); + this.pendingAcceptance = false; + }, 600); + }; + + endCall = async ({ callUUID }: { callUUID: string }) => { + if (this.msg?.type === "call") { + const sessionID = this.msg.sessionId; + if (this.isAnswered && sessionID) { + this.isAnswered = false; + CometChat.endCall(sessionID); + } else if (sessionID) { + const loggedInUser = await CometChat.getLoggedinUser().catch( + () => null, + ); + if (loggedInUser) { + setTimeout(() => { + CometChat.rejectCall(sessionID, CometChat.CALL_STATUS.REJECTED); + }, 300); + } + } + } + const id = callUUID || this.callerId; + if (id) RNCallKeep.endCall(id); + RNCallKeep.endAllCalls(); + this.isRinging = false; + this.isAnswered = false; + this.pendingAcceptance = false; + this.callerId = ""; + this.msg = {}; + }; + + removeCallDialerWithUUID = (callerId: string) => { + const id = callerId || this.callerId; + if (id) RNCallKeep.reportEndCallWithUUID(id, 6); + }; + + setupEventListeners() { + if (this.listenersAttached) return; + RNCallKeep.addEventListener("answerCall", this.onAnswerCall); + RNCallKeep.addEventListener("endCall", this.endCall); + RNCallKeep.addEventListener("didDisplayIncomingCall", ({ callUUID }) => { + if (callUUID) this.callerId = callUUID; + this.isRinging = true; + }); + this.listenersAttached = true; + } +} + +export const voipHandler = new VoipNotificationHandler(); +``` + +### 5.4 Add `PendingCallManager.ts` +Stores an answered call during cold-start so you can accept it once login/navigation is ready. + +```ts lines +import AsyncStorage from "@react-native-async-storage/async-storage"; + +export interface PendingAnsweredCallPayload { + sessionId: string; + raw: any; + storedAt: number; +} + +let inMemoryPending: PendingAnsweredCallPayload | null = null; +const STORAGE_KEY = "pendingAnsweredCall"; + +export async function setPendingAnsweredCall(payload: PendingAnsweredCallPayload) { + inMemoryPending = payload; + try { await AsyncStorage.setItem(STORAGE_KEY, JSON.stringify(payload)); } catch {} +} + +export async function consumePendingAnsweredCall(): Promise { + if (inMemoryPending) { + const tmp = inMemoryPending; + inMemoryPending = null; + try { await AsyncStorage.removeItem(STORAGE_KEY); } catch {} + return tmp; + } + try { + const raw = await AsyncStorage.getItem(STORAGE_KEY); + if (raw) { + await AsyncStorage.removeItem(STORAGE_KEY); + const parsed: PendingAnsweredCallPayload = JSON.parse(raw); + inMemoryPending = null; + return parsed; + } + } catch {} + return null; +} + +export function isPendingStale(p: PendingAnsweredCallPayload, maxAgeMs = 2 * 60 * 1000) { + return Date.now() - p.storedAt > maxAgeMs; +} +``` + +### 5.5 Wire `App.tsx` to init VoIP + consume pending accepts +Add this after CometChat init/login: + +```ts lines +import { Platform } from "react-native"; +import messaging from "@react-native-firebase/messaging"; +import { CometChat, CometChatNotifications } from "@cometchat/chat-sdk-react-native"; +import { voipHandler } from "./VoipNotificationHandler"; +import { consumePendingAnsweredCall, isPendingStale } from "./PendingCallManager"; + +if (Platform.OS === "android") { + const fcmToken = await messaging().getToken(); + await CometChatNotifications.registerPushToken( + fcmToken, + CometChatNotifications.PushPlatforms.FCM_REACT_NATIVE_ANDROID, + "YOUR_FCM_PROVIDER_ID" + ); +} + +useEffect(() => { + if (Platform.OS === "android" && loggedIn) { + const t = setTimeout(() => voipHandler.initialize(), 3000); + return () => clearTimeout(t); + } +}, [loggedIn]); + +// Handle pending calls in a useEffect +useEffect(() => { + const handlePendingCall = async () => { + const pending = await consumePendingAnsweredCall(); + if (pending && !isPendingStale(pending)) { + try { + await CometChat.acceptCall(pending.sessionId); + } catch (err) { + console.log(err); + } + } + }; + handlePendingCall(); +}, []); +``` + +### 5.6 Call push payload (FCM data) +Send a data-only FCM message like: + +```json +{ + "to": "", + "priority": "high", + "data": { + "type": "call", + "callAction": "initiated", + "sessionId": "", + "senderName": "Alice", + "receiverName": "Bob" + } +} +``` + +### 5.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: + +```ts lines +import { Platform } from "react-native"; +import notifee, { AndroidImportance } from "@notifee/react-native"; + +const CHANNEL_ID = "default"; + +async function ensureChannel(): Promise { + if (Platform.OS !== "android") return undefined; + return notifee.createChannel({ + id: CHANNEL_ID, + name: "Default", + lights: true, + vibration: true, + importance: AndroidImportance.HIGH, + }); +} + +export async function displayLocalNotification(remoteMessage: any) { + try { + const { notification = {}, data = {} } = remoteMessage || {}; + const title = notification?.title || data?.title || "Notification"; + const body = notification?.body || data?.body || ""; + + if (Platform.OS === "ios") { + await notifee.requestPermission(); + } + + const channelId = await ensureChannel(); + + await notifee.displayNotification({ + title, + body, + data, + android: channelId + ? { + channelId, + pressAction: { id: "default" }, + importance: AndroidImportance.HIGH, + smallIcon: "ic_launcher", + } + : undefined, + }); + } catch (error) { + console.error("[LocalNotificationHandler] Failed to display notification", error); + } +} +``` +- 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 + +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 + +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 + + + + Go to **CometChat Dashboard → Notifications Engine → Settings → Preferences → Push Notification Preferences**. + + + Scroll down and enable the **Unread Badge Count** toggle. + + + +Once enabled, CometChat automatically includes the `unreadMessageCount` field in every push payload sent to your app. + +### 7.2 Expected Payload Format + +CometChat sends push notifications with the following structure: + +```jsonc +{ + "data": { + "unreadMessageCount": "5", + "title": "New Message", + "body": "John: Hello!", + "conversationId": "user_abc123", + "parentId": "176001", // Optional - parent message ID; sent only for threaded notifications + "receiverType": "user", + "type": "chat" + } +} +``` + + +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 + +Update your FCM background message handler in `index.js` to extract and set the badge count: + +```javascript +import messaging from "@react-native-firebase/messaging"; +import notifee from "@notifee/react-native"; + +messaging().setBackgroundMessageHandler(async (remoteMessage) => { + const data = remoteMessage.data || {}; + + // Extract and set badge count from push payload + const unreadCount = data?.unreadMessageCount; + if (unreadCount !== undefined && unreadCount !== null) { + const count = parseInt(unreadCount, 10); + if (!isNaN(count) && count >= 0) { + try { + await notifee.setBadgeCount(count); + console.log("Badge count updated (Android):", count); + } catch (error) { + console.error("Error setting badge:", error); + } + } + } + + // Display local notification + await displayLocalNotification(remoteMessage); +}); +``` + +### 7.4 Handle Badge Count in Foreground Messages + +In your `App.tsx`, set up a listener for foreground FCM messages: + +```typescript +import messaging from "@react-native-firebase/messaging"; +import notifee from "@notifee/react-native"; + +useEffect(() => { + if (Platform.OS === "android") { + const unsubscribe = messaging().onMessage(async (remoteMessage) => { + // Extract and set badge count from push payload + const unreadCount = remoteMessage.data?.unreadMessageCount; + if (unreadCount !== undefined && unreadCount !== null) { + const count = parseInt(unreadCount as string, 10); + if (!isNaN(count) && count >= 0) { + try { + await notifee.setBadgeCount(count); + console.log("Badge count updated (Android):", count); + } catch (error) { + console.error("Error setting badge:", error); + } + } + } + + // Display local notification + await displayLocalNotification(remoteMessage); + }); + + return () => unsubscribe(); + } +}, []); +``` + +### 7.5 Display Local Notification with Badge Count + +Update your notification display function to include the badge count: + +```typescript +import notifee, { AndroidImportance } from "@notifee/react-native"; + +export async function displayLocalNotification(remoteMessage: any) { + const { title, body, senderAvatar } = remoteMessage.data || {}; + + // Create notification channel + const channelId = await notifee.createChannel({ + id: "chat-messages", + name: "Chat Messages", + vibration: true, + importance: AndroidImportance.HIGH, + }); + + // Parse badge count from payload + const unreadCount = remoteMessage.data?.unreadMessageCount; + const badgeCount = unreadCount ? parseInt(unreadCount, 10) : undefined; + + // Optionally enhance title with unread count + const displayTitle = + badgeCount && badgeCount > 1 + ? `${title || "New Message"} (${badgeCount} unread)` + : title || "New Message"; + + // Update badge count + if (badgeCount && badgeCount > 0) { + await notifee.setBadgeCount(badgeCount); + } + + // Display notification with fixed ID to prevent badge accumulation + // on devices that sum badge counts from multiple notifications + await notifee.displayNotification({ + id: "chat-notification", + title: displayTitle, + body: body || "You received a new message.", + android: { + channelId, + autoCancel: true, + smallIcon: "ic_notification", + largeIcon: + senderAvatar || + "https://cdn-icons-png.flaticon.com/512/149/149071.png", + importance: AndroidImportance.HIGH, + badgeCount: badgeCount, + pressAction: { + id: "default", + }, + }, + data: { + receiverType: remoteMessage.data?.receiverType, + sender: remoteMessage.data?.sender, + conversationId: remoteMessage.data?.conversationId, + }, + }); +} +``` + +### 7.6 Clear Badge When App Becomes Active + +Clear all notifications and reset the badge when the app returns to the foreground: + +```typescript +import { AppState, AppStateStatus, Platform } from "react-native"; +import notifee from "@notifee/react-native"; + +useEffect(() => { + const handleAppStateChange = async (nextState: AppStateStatus) => { + if (nextState === "active" && Platform.OS === "android") { + // Clear all notifications (also resets badge count) + await notifee.cancelAllNotifications(); + console.log("Notifications cleared (Android)"); + } + }; + + const subscription = AppState.addEventListener("change", handleAppStateChange); + return () => subscription.remove(); +}, []); +``` + +### 7.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: + +```typescript +import notifee from "@notifee/react-native"; +import { CometChat, CometChatNotifications } from "@cometchat/chat-sdk-react-native"; + +const handleLogout = async () => { + // Unregister push token first + await CometChatNotifications.unregisterPushToken(); + + // Clear badge before logout + await notifee.setBadgeCount(0); + await notifee.cancelAllNotifications(); + + // Logout from CometChat + await CometChat.logout(); + console.log("User logged out, badge cleared"); +}; +``` + +### 7.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: + +```typescript +import notifee from "@notifee/react-native"; +import { CometChat } from "@cometchat/chat-sdk-react-native"; + +// During app initialization, after CometChat.init() +const initializeApp = async () => { + // Initialize CometChat first + await CometChatUIKit.init(uiKitSettings); + + // Check if user is logged in + const loggedInUser = await CometChat.getLoggedinUser(); + + if (!loggedInUser) { + // No user logged in - clear any stale badge + await notifee.setBadgeCount(0); + await notifee.cancelAllNotifications(); + console.log("No logged-in user, badge cleared"); + } +}; +``` + +### 7.9 Clear Badge in Login Listener (Safety Net) + +Register a login listener to clear the badge on logout as a backup mechanism: + +```typescript +import notifee from "@notifee/react-native"; +import { CometChat } from "@cometchat/chat-sdk-react-native"; + +useEffect(() => { + const listenerID = "BADGE_LOGOUT_LISTENER"; + + CometChat.addLoginListener( + listenerID, + new CometChat.LoginListener({ + logoutOnSuccess: async () => { + // Safety net: clear badge when logout succeeds + await notifee.setBadgeCount(0); + await notifee.cancelAllNotifications(); + console.log("Logout listener: badge cleared"); + }, + }) + ); + + return () => { + CometChat.removeLoginListener(listenerID); + }; +}, []); +``` + +### 7.10 Key Implementation Notes + +| Consideration | Details | +| --- | --- | +| **Backend-driven badge count** | The `unreadMessageCount` value comes directly from CometChat's backend via the push payload, ensuring consistency across all devices. | +| **Fixed notification ID** | Using a fixed notification ID (`'chat-notification'`) prevents certain devices from accumulating badge counts across multiple notifications. The badge always reflects the exact `unreadMessageCount` from the backend. | +| **Clear on app active** | Always clear the badge when the app becomes active. New notifications will update the badge with the fresh `unreadMessageCount` from the backend. | +| **Clear on logout** | Always clear the badge when a user logs out to prevent stale counts for the next user. | +| **Clear on fresh install** | Clear the badge during app initialization when no user is logged in to handle reinstall scenarios. | +| **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 + +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: + - **Foreground:** Notifee banner appears unless that chat is already open. + - **Background/terminated:** Tap opens the correct conversation; Notifee background handler runs. +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 + +| Symptom | Quick Checks | +| --- | --- | +| No pushes | Confirm `google-services.json` location, package IDs match Firebase, Push extension enabled with correct provider IDs, permissions granted. | +| Token registration fails | Ensure registration runs **after login**, provider IDs are set, and `registerDeviceForRemoteMessages()` is called. | +{/* | Notification taps do nothing | Keep Notifee foreground/background handlers and ensure the navigation ref is ready before routing. | */} +{/* | Call UI not showing | Verify CallKeep setup, telecom permissions, and that `VoipNotificationHandler.initialize()` runs post-login. | */} +{/* | Inline reply needed | Extend Notifee action buttons; CometChat expects you to send the message manually after reading `remoteMessage.data`. | */} + +--- + +## Next Steps + + + +Set up APNs push notifications for iOS + + +Strip HTML tags and customize notification content + + +Learn how to send different types of messages + + +Handle incoming messages in real time + + diff --git a/notifications/react-native-push-notifications-android.mdx b/notifications/react-native-push-notifications-android.mdx index c39210f61..055dac326 100644 --- a/notifications/react-native-push-notifications-android.mdx +++ b/notifications/react-native-push-notifications-android.mdx @@ -24,6 +24,10 @@ description: "CometChat push notifications and VoIP calls in React Native apps o Reference implementation of React Native UI Kit, FCM and Push Notification Setup. + +Still on the previous setup that copies the sample app's notification stack? The [legacy Android guide](/notifications/react-native-push-notifications-android-legacy) still covers it. + + ## What this guide covers - CometChat dashboard setup (enable push, add FCM provider) with screenshots. diff --git a/notifications/react-native-push-notifications-ios-legacy.mdx b/notifications/react-native-push-notifications-ios-legacy.mdx new file mode 100644 index 000000000..27e705cab --- /dev/null +++ b/notifications/react-native-push-notifications-ios-legacy.mdx @@ -0,0 +1,851 @@ +--- +title: "React Native Push Notifications (iOS) — Legacy" +description: "Bring the SampleAppWithPushNotifications experience—APNs + VoIP—into any React Native project using CometChat UI Kit." +--- + + +**Legacy guide.** This page covers the previous setup, which copies the sample app's notification stack (APNs, CallKeep, react-native-voip-push-notification). For new integrations, use the `@cometchat/push-notifications-react-native` package — see [React Native Push Notifications (iOS)](/notifications/react-native-push-notifications-ios). + + + + +| Field | Value | +| --- | --- | +| Platform | iOS (APNs + PushKit/CallKit) | +| Key Classes | `CometChatNotifications`, `VoipNotificationHandler`, `PendingCallManager` | +| Key Methods | `registerPushToken()`, `unregisterPushToken()`, `PushNotificationIOS.requestPermissions()` | +| Push Platforms | `APNS_REACT_NATIVE_DEVICE`, `APNS_REACT_NATIVE_VOIP` | +| Prerequisites | CometChat SDK initialized, user logged in, APNs `.p8` key uploaded, physical iOS device | + + + + + Reference implementation of React Native UI Kit and APNs Push Notification setup. + + +## What this guide covers + +- CometChat Dashboard setup (enable push, add APNs provider). +- Platform credentials (Apple entitlements). +- 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. + +## What you need first + +- CometChat app credentials (App ID, Region, Auth Key) and Push Notifications enabled with an **APNs provider (React Native iOS)**; add an **APNs VoIP provider** if you plan to receive call invites via PushKit. +- Apple push setup: APNs `.p8` key/cert in CometChat, iOS project with Push Notifications + Background Modes (Remote notifications) permissions. +- React Native 0.81+, Node 18+, physical iOS device for reliable push/call testing. + +## How APNs + CometChat work together + +- **APNs (iOS) is the transport:** Apple issues the APNs token and delivers payloads to devices. +- **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) + +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 + +- Update `src/utils/AppConstants.tsx` with `appId`, `authKey`, `region`, and `apnProviderId`. +- Keep `app.json` name consistent with your bundle ID / applicationId. + +```ts lines +const APP_ID = ""; +const AUTH_KEY = ""; +const REGION = ""; +const DEMO_UID = "cometchat-uid-1"; +``` + +### 3.1 Dependencies snapshot (from Sample App) + +Install these dependencies in your React Native app: + +```npm lines +npm install \ + @cometchat/chat-sdk-react-native@4.0.18 \ + @cometchat/calls-sdk-react-native@4.4.0 \ + @cometchat/chat-uikit-react-native@5.2.6 \ + @notifee/react-native@9.1.8 \ + @react-native-async-storage/async-storage@2.2.0 \ + @react-native-community/push-notification-ios@1.12.0 \ + react-native-push-notification@8.1.1 \ + react-native-callkeep@4.3.16 \ + react-native-voip-push-notification@3.3.3 +``` + +Match these or newer compatible versions in your app. + +## 4. iOS App setup + +### 4.1 Project Setup + +Enable **Push Notifications** and **Background Modes** (Remote notifications) in Xcode. + + + Enable Push Notifications + + +### 4.2 Install dependencies + pods + +After running the npm install above, install pods from the `ios` directory: +```bash lines +cd ios +pod install +``` + +### 4.3 AppDelegate.swift modifications: + +Add imports at the top: +```swift lines +import UserNotifications +import RNCPushNotificationIOS +``` + +Add `UNUserNotificationCenterDelegate` to the `AppDelegate` class declaration: +```swift +class AppDelegate: UIResponder, UIApplicationDelegate, UNUserNotificationCenterDelegate +``` + +Add the following inside the `didFinishLaunchingWithOptions` method: +```swift lines +UNUserNotificationCenter.current().delegate = self + +UNUserNotificationCenter.current().requestAuthorization( + options: [.alert, .badge, .sound] +) { + granted, + error in + if granted { + DispatchQueue.main.async { + application.registerForRemoteNotifications() + } + } else { + print("Push Notification permission not granted: \(String(describing: error))") + } +} +``` + +Add the following methods to handle push notification events: +```swift lines +func application(_ application: UIApplication, didRegisterForRemoteNotificationsWithDeviceToken deviceToken: Data) { + print("APNs device token received: \(deviceToken)") + RNCPushNotificationIOS.didRegisterForRemoteNotifications(withDeviceToken: deviceToken) +} + +func application(_ application: UIApplication, didFailToRegisterForRemoteNotificationsWithError error: Error) { + print("APNs registration failed: \(error)") + RNCPushNotificationIOS.didFailToRegisterForRemoteNotificationsWithError(error) +} + +func application(_ application: UIApplication, didReceiveRemoteNotification userInfo: [AnyHashable: Any], fetchCompletionHandler completionHandler: @escaping (UIBackgroundFetchResult) -> Void) { + RNCPushNotificationIOS.didReceiveRemoteNotification(userInfo, fetchCompletionHandler: completionHandler) +} + +func userNotificationCenter(_ center: UNUserNotificationCenter, willPresent notification: UNNotification, withCompletionHandler completionHandler: @escaping (UNNotificationPresentationOptions) -> Void) { + completionHandler([.banner, .sound, .badge]) +} + +func userNotificationCenter(_ center: UNUserNotificationCenter, didReceive response: UNNotificationResponse, withCompletionHandler completionHandler: @escaping () -> Void) { + RNCPushNotificationIOS.didReceive(response) + completionHandler() +} +``` + +Add the following to `Podfile` to avoid framework linkage issues: +```ruby +use_frameworks! :linkage => :static +``` + +You might have to remove below code if already present in your Podfile: +```ruby lines +linkage = ENV['USE_FRAMEWORKS'] +if linkage != nil + Pod::UI.puts "Configuring Pod with #{linkage}ally linked Frameworks".green + use_frameworks! :linkage => linkage.to_sym +end +``` + +Then lets install pods and open the workspace: +```bash lines +cd ios +pod install +open YourProjectName.xcworkspace +``` + +### 4.4 App.tsx modifications: + +Import CometChatNotifications and PushNotificationIOS: + +```tsx +import { CometChat, CometChatNotifications } from "@cometchat/chat-sdk-react-native"; +import PushNotificationIOS from "@react-native-community/push-notification-ios"; +``` + +Get device token and store it in a ref: +Also, define your APNs provider ID from the CometChat Dashboard. +And request permissions on mount: + +```tsx lines +const APNS_PROVIDER_ID = 'YOUR_APNS_PROVIDER_ID'; // from CometChat Dashboard +const apnsTokenRef = useRef < string | null > (null); + +useEffect(() => { + if (Platform.OS !== 'ios') return; + + const onRegister = (deviceToken: string) => { + console.log(' APNs device token captured:', deviceToken); + apnsTokenRef.current = deviceToken; + }; + + PushNotificationIOS.addEventListener('register', onRegister); + + PushNotificationIOS.addEventListener('registrationError', error => { + console.error(' APNs registration error:', error); + }); + + // Trigger permission + native registration + PushNotificationIOS.requestPermissions().then(p => + console.log('Push permissions:', p), + ); + + return () => { + PushNotificationIOS.removeEventListener('register'); + PushNotificationIOS.removeEventListener('registrationError'); + }; +}, []); +``` + +After user login, register the APNs token: +```tsx lines +// Register token ONLY if we already have it +if (apnsTokenRef.current) { + await CometChatNotifications.registerPushToken( + apnsTokenRef.current, + CometChatNotifications.PushPlatforms.APNS_REACT_NATIVE_DEVICE, + APNS_PROVIDER_ID + ); + console.log(' APNs token registered with CometChat'); +} +``` + +Prior to logout, unregister the APNs token: +```tsx +await CometChatNotifications.unregisterPushToken(); +``` + +## 5. VoIP call notifications (iOS) + +These steps are iOS-only—copy/paste and fill your IDs. + +### 5.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) +Update your `AppDelegate` to register for VoIP pushes ASAP and forward events to JS/CallKeep: + +```swift lines +import PushKit +import RNVoipPushNotification +import RNCallKeep +// ... +@main +class AppDelegate: UIResponder, UIApplicationDelegate, UNUserNotificationCenterDelegate, PKPushRegistryDelegate { + // ... + func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? = nil) -> Bool { + // existing UNUserNotificationCenter code ... + RNVoipPushNotificationManager.voipRegistration() // triggers PushKit token + return true + } + + // APNs device token handlers stay unchanged + + // PushKit token -> JS + func pushRegistry(_ registry: PKPushRegistry, didUpdate pushCredentials: PKPushCredentials, for type: PKPushType) { + RNVoipPushNotificationManager.didUpdate(pushCredentials, forType: type.rawValue) + } + + // Incoming VoIP push -> CallKit + JS + func pushRegistry(_ registry: PKPushRegistry, didReceiveIncomingPushWith payload: PKPushPayload, for type: PKPushType, completion: @escaping () -> Void) { + let dict = payload.dictionaryPayload + let uuid = (dict["uuid"] as? String) ?? UUID().uuidString + RNVoipPushNotificationManager.addCompletionHandler(uuid, completionHandler: completion) + RNVoipPushNotificationManager.didReceiveIncomingPush(with: payload, forType: type.rawValue) + RNCallKeep.reportNewIncomingCall(uuid, handle: (dict["handle"] as? String) ?? "Unknown", handleType: "generic", hasVideo: false, localizedCallerName: (dict["callerName"] as? String) ?? "Incoming Call", supportsHolding: true, supportsDTMF: true, supportsGrouping: true, supportsUngrouping: true, fromPushKit: true, payload: nil) + } +} +``` + +### 5.3 Drop in `VoipNotificationHandler.ts` +Handles CallKeep UI, defers acceptance until login, and listens for PushKit events. + +```ts lines +import { Platform } from "react-native"; +import notifee, { AndroidImportance } from "@notifee/react-native"; +import RNCallKeep, { IOptions } from "react-native-callkeep"; +import { CometChat } from "@cometchat/chat-sdk-react-native"; +import VoipPushNotification from "react-native-voip-push-notification"; +import { setPendingAnsweredCall } from "./PendingCallManager"; + +const options: IOptions = { + ios: { appName: "YourAppName" }, + android: { alertTitle: "VOIP required", alertDescription: "Allow phone account access", cancelButton: "Cancel", okButton: "OK", imageName: "ic_notification" }, +}; + +type IncomingPayload = { sessionId?: string; senderName?: string; callerName?: string; name?: string; type?: string; [k: string]: any; }; + +class VoipNotificationHandler { + channelId = ""; + isRinging = false; + isAnswered = false; + pendingAcceptance = false; + callerId = ""; + msg: IncomingPayload | null = null; + initialized = false; + private setupPromise: Promise | null = null; + private listenersAttached = false; + private lastSessionId: string | null = null; + private lastRingAt = 0; + + async initialize() { + if (this.initialized && this.setupPromise) { await this.setupPromise; return; } + if (!this.setupPromise) { + this.setupPromise = (async () => { + if (Platform.OS === "android") { await this.createNotificationChannel(); } + await this.setupCallKeep(); + this.setupEventListeners(); + this.initialized = true; + })().catch(err => { this.setupPromise = null; throw err; }); + } + await this.setupPromise; + } + + private async setupCallKeep() { + await RNCallKeep.setup(options); + RNCallKeep.setAvailable(true); + if (Platform.OS === "android") { RNCallKeep.setReachable(); } + } + + private async createNotificationChannel() { + this.channelId = await notifee.createChannel({ id: "message", name: "Messages", lights: true, vibration: true, importance: AndroidImportance.HIGH }); + } + + async displayIncomingCall(payload: IncomingPayload) { + this.msg = payload || {}; + const sessionId = this.msg?.sessionId; + const now = Date.now(); + if (sessionId && this.lastSessionId === sessionId && now - this.lastRingAt < 5000) return; + if (this.isAnswered || this.pendingAcceptance) return; + await this.initialize(); + + const callerName = this.msg?.senderName || this.msg?.callerName || this.msg?.name || "Incoming Call"; + this.callerId = this.callerId || Math.random().toString(); + this.isRinging = true; + + await RNCallKeep.displayIncomingCall(this.callerId, callerName, callerName, "generic", true); + this.lastSessionId = sessionId || null; + this.lastRingAt = now; + } + + onAnswerCall = async ({ callUUID }: { callUUID: string }) => { + if (this.isAnswered) return; + this.isRinging = false; this.isAnswered = true; + const sessionID = this.msg?.sessionId; if (!sessionID) return; + RNCallKeep.backToForeground(); + setTimeout(async () => { + const loggedInUser = await CometChat.getLoggedinUser().catch(() => null); + if (!loggedInUser) { this.pendingAcceptance = true; await setPendingAnsweredCall({ sessionId: sessionID, raw: this.msg, storedAt: Date.now() }); return; } + try { await CometChat.acceptCall(sessionID); } catch (error: any) { if (error?.code !== "ERR_CALL_USER_ALREADY_JOINED") throw error; } + RNCallKeep.endAllCalls(); this.pendingAcceptance = false; + }, 350); + }; + + endCall = async ({ callUUID }: { callUUID: string }) => { + const sessionID = this.msg?.sessionId; + if (sessionID) { + const loggedInUser = await CometChat.getLoggedinUser().catch(() => null); + if (this.isAnswered) { await CometChat.endCall(sessionID).catch(() => {}); } + else if (loggedInUser) { await CometChat.rejectCall(sessionID, CometChat.CALL_STATUS.REJECTED).catch(() => {}); } + } + const id = callUUID || this.callerId; + if (id) RNCallKeep.endCall(id); + RNCallKeep.endAllCalls(); + this.isRinging = false; this.isAnswered = false; this.pendingAcceptance = false; this.callerId = ""; this.msg = null; this.lastSessionId = null; this.lastRingAt = 0; + }; + + setupEventListeners() { + if (this.listenersAttached) return; + if (Platform.OS === "ios") { + VoipPushNotification.addEventListener("notification", (notification: any) => this.displayIncomingCall(notification)); + VoipPushNotification.addEventListener("didLoadWithEvents", (events: any[]) => { + (events || []).forEach(event => { + if (event?.name === VoipPushNotification.RNVoipPushRemoteNotificationReceivedEvent) { + this.displayIncomingCall(event.data); + } + }); + }); + } + RNCallKeep.addEventListener("answerCall", this.onAnswerCall); + RNCallKeep.addEventListener("endCall", this.endCall); + RNCallKeep.addEventListener("didDisplayIncomingCall", ({ callUUID }) => { if (callUUID) this.callerId = callUUID; this.isRinging = true; }); + this.listenersAttached = true; + } +} + +export const voipHandler = new VoipNotificationHandler(); +``` + +### 5.4 Add `PendingCallManager.ts` +Stores an answered call during cold start so you can accept it after login/navigation is ready. + +```ts lines +import AsyncStorage from "@react-native-async-storage/async-storage"; + +export interface PendingAnsweredCallPayload { sessionId: string; raw: any; storedAt: number; } +let inMemoryPending: PendingAnsweredCallPayload | null = null; +const STORAGE_KEY = "pendingAnsweredCall"; + +export async function setPendingAnsweredCall(payload: PendingAnsweredCallPayload) { + inMemoryPending = payload; try { await AsyncStorage.setItem(STORAGE_KEY, JSON.stringify(payload)); } catch {} +} + +export async function consumePendingAnsweredCall(): Promise { + if (inMemoryPending) { const tmp = inMemoryPending; inMemoryPending = null; try { await AsyncStorage.removeItem(STORAGE_KEY); } catch {} return tmp; } + try { const raw = await AsyncStorage.getItem(STORAGE_KEY); if (raw) { await AsyncStorage.removeItem(STORAGE_KEY); return JSON.parse(raw); } } catch {} + return null; +} + +export function isPendingStale(p: PendingAnsweredCallPayload, maxAgeMs = 2 * 60 * 1000) { + return Date.now() - p.storedAt > maxAgeMs; +} +``` + +### 5.5 Wire `App.tsx` for APNs + VoIP token registration and handler init + +```tsx lines +import PushNotificationIOS from "@react-native-community/push-notification-ios"; +import VoipPushNotification from "react-native-voip-push-notification"; +import { voipHandler } from "./VoipNotificationHandler"; +import { consumePendingAnsweredCall, isPendingStale } from "./PendingCallManager"; + +const APNS_PROVIDER_ID = "YOUR_APNS_PROVIDER_ID"; + +// Capture APNs device token +useEffect(() => { + if (Platform.OS !== "ios") return; + const onRegister = (deviceToken: string) => { apnsTokenRef.current = deviceToken; }; + PushNotificationIOS.addEventListener("register", onRegister); + PushNotificationIOS.requestPermissions(); + return () => PushNotificationIOS.removeEventListener("register"); +}, []); + +// Capture VoIP token +useEffect(() => { + if (Platform.OS !== "ios") return; + const onVoipRegister = (token: string) => { + CometChatNotifications.registerPushToken( + token, + CometChatNotifications.PushPlatforms.APNS_REACT_NATIVE_VOIP, + APNS_PROVIDER_ID + ).catch(err => console.log("[VoIP] register failed", err)); + }; + VoipPushNotification.addEventListener("register", onVoipRegister); + // token request is triggered in AppDelegate via RNVoipPushNotificationManager.voipRegistration() + return () => VoipPushNotification.removeEventListener("register"); +}, []); + +// After login: register APNs token + init VoIP handler + consume pending accepts +useEffect(() => { + const run = async () => { + if (!loggedIn || Platform.OS !== "ios") return; + const pending = await consumePendingAnsweredCall(); + if (pending && !isPendingStale(pending)) { await CometChat.acceptCall(pending.sessionId).catch(console.log); } + const token = apnsTokenRef.current; + if (token) { + await CometChatNotifications.registerPushToken( + token, + CometChatNotifications.PushPlatforms.APNS_REACT_NATIVE_DEVICE, + APNS_PROVIDER_ID + ); + } + await voipHandler.initialize(); + }; + run(); +}, [loggedIn]); +``` + +### 5.6 VoIP push payload (APNs / PushKit) +Send a VoIP push with `push_type=voip` via APNs using a payload shaped like: + +```json +{ + "aps": { "alert": { "title": "Alice", "body": "Incoming call" }, "content-available": 1 }, + "sessionId": "", + "callerName": "Alice", + "handle": "alice", + "type": "call", + "uuid": "" +} +``` + +## 6. 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 + +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 + + + + Go to **CometChat Dashboard → Notifications Engine → Settings → Preferences → Push Notification Preferences**. + + + Scroll down and enable the **Unread Badge Count** toggle. + + + +Once enabled, CometChat automatically includes the `unreadMessageCount` field in every push payload sent to your app. + +### 7.2 Expected Payload Format + +CometChat sends APNs payloads with the following structure: + +```jsonc +{ + "aps": { + "alert": { + "title": "New Message", + "body": "John: Hello!" + }, + "badge": 5, + "sound": "default" + }, + "unreadMessageCount": "5", + "conversationId": "user_abc123", + "parentId": "176001" // Optional - parent message ID; sent only for threaded notifications +} +``` + + +The `aps.badge` field is set server-side by CometChat. iOS automatically updates the app icon badge when the push notification is delivered. + + +### 7.3 Handle Badge Count from Notifications + +Update your iOS notification handler to set the badge count programmatically: + +```typescript +import PushNotificationIOS from "@react-native-community/push-notification-ios"; + +export async function onRemoteNotificationIOS(notification: any) { + // Extract badge count from push payload + const data = notification.getData(); + const unreadCount = data?.unreadMessageCount; + + if (unreadCount !== undefined && unreadCount !== null) { + const count = parseInt(unreadCount, 10); + if (!isNaN(count) && count >= 0) { + PushNotificationIOS.setApplicationIconBadgeNumber(count); + console.log("Badge count updated (iOS):", count); + } + } + + // Handle notification tap + const isClicked = data?.userInteraction === 1; + if (isClicked && data?.type === "chat") { + // Navigate to conversation... + } + + // Required: Notify iOS that processing is complete + notification.finish(PushNotificationIOS.FetchResult.NoData); +} +``` + +### 7.4 Register Notification Listener + +In your `App.tsx`, set up the notification listener: + +```typescript +import PushNotificationIOS from "@react-native-community/push-notification-ios"; + +useEffect(() => { + if (Platform.OS === "ios") { + const onNotification = async (notification: any) => { + try { + await onRemoteNotificationIOS(notification); + } catch (error) { + console.log("Error in onRemoteNotificationIOS:", error); + } + }; + + PushNotificationIOS.addEventListener("notification", onNotification); + + return () => { + PushNotificationIOS.removeEventListener("notification"); + }; + } +}, []); +``` + +### 7.5 Clear Badge When App Becomes Active + +Clear the badge count when the app launches or returns to the foreground: + +```typescript +import { AppState, AppStateStatus, Platform } from "react-native"; +import PushNotificationIOS from "@react-native-community/push-notification-ios"; + +useEffect(() => { + const handleAppStateChange = async (nextState: AppStateStatus) => { + if (nextState === "active" && Platform.OS === "ios") { + PushNotificationIOS.setApplicationIconBadgeNumber(0); + console.log("Badge cleared (iOS)"); + } + }; + + const subscription = AppState.addEventListener("change", handleAppStateChange); + return () => subscription.remove(); +}, []); +``` + +### 7.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: + +```typescript +import PushNotificationIOS from "@react-native-community/push-notification-ios"; +import { CometChat, CometChatNotifications } from "@cometchat/chat-sdk-react-native"; + +const handleLogout = async () => { + // Unregister push token first + await CometChatNotifications.unregisterPushToken(); + + // Clear badge before logout + PushNotificationIOS.setApplicationIconBadgeNumber(0); + + // Logout from CometChat + await CometChat.logout(); + console.log("User logged out, badge cleared"); +}; +``` + +### 7.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: + +```typescript +import PushNotificationIOS from "@react-native-community/push-notification-ios"; +import { CometChat } from "@cometchat/chat-sdk-react-native"; + +// During app initialization, after CometChat.init() +const initializeApp = async () => { + // Initialize CometChat first + await CometChatUIKit.init(uiKitSettings); + + // Check if user is logged in + const loggedInUser = await CometChat.getLoggedinUser(); + + if (!loggedInUser) { + // No user logged in - clear any stale badge + PushNotificationIOS.setApplicationIconBadgeNumber(0); + console.log("No logged-in user, badge cleared"); + } +}; +``` + +### 7.8 Clear Badge in Login Listener (Safety Net) + +Register a login listener to clear the badge on logout as a backup mechanism: + +```typescript +import PushNotificationIOS from "@react-native-community/push-notification-ios"; +import { CometChat } from "@cometchat/chat-sdk-react-native"; + +useEffect(() => { + const listenerID = "BADGE_LOGOUT_LISTENER"; + + CometChat.addLoginListener( + listenerID, + new CometChat.LoginListener({ + logoutOnSuccess: () => { + // Safety net: clear badge when logout succeeds + PushNotificationIOS.setApplicationIconBadgeNumber(0); + console.log("Logout listener: badge cleared"); + }, + }) + ); + + return () => { + CometChat.removeLoginListener(listenerID); + }; +}, []); +``` + +### 7.9 Key Implementation Notes + +| Consideration | Details | +| --- | --- | +| **Backend-driven badge count** | The `unreadMessageCount` value comes directly from CometChat's backend via the push payload, ensuring consistency across all devices. | +| **iOS server-side badge** | For iOS using APNs, the `aps.badge` field is set server-side by CometChat, so the badge updates automatically even without client-side code. However, you still need to clear it when the app opens. | +| **Clear on app active** | Always clear the badge when the app becomes active. New notifications will update the badge with the fresh `unreadMessageCount` from the backend. | +| **Clear on logout** | Always clear the badge when a user logs out to prevent stale counts for the next user. | +| **Clear on fresh install** | On iOS, the badge count may persist after app reinstall in certain scenarios. Clear the badge during app initialization when no user is logged in. | +| **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 + +If you're building a cross-platform app, use this combined handler for both iOS and Android: + +```typescript +import { AppState, AppStateStatus, Platform } from "react-native"; +import PushNotificationIOS from "@react-native-community/push-notification-ios"; +import notifee from "@notifee/react-native"; + +useEffect(() => { + const handleAppStateChange = async (nextState: AppStateStatus) => { + if (nextState === "active") { + // Clear badge for iOS + if (Platform.OS === "ios") { + PushNotificationIOS.setApplicationIconBadgeNumber(0); + console.log("Badge cleared (iOS)"); + } + // Clear all notifications for Android (also resets badge) + else if (Platform.OS === "android") { + await notifee.cancelAllNotifications(); + console.log("Notifications cleared (Android)"); + } + } + }; + + const subscription = AppState.addEventListener("change", handleAppStateChange); + return () => subscription.remove(); +}, []); +``` + +## 8. Testing Checklist + +1. Install on a physical iOS device, log in, and verify APNs token registration succeeds. +2. Send a message from another user: + - **Foreground:** Banner appears unless that chat is already open. + - **Background/terminated:** Tap opens the correct conversation; handler runs. +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 + +| Symptom | Quick Checks | +| --- | --- | +| No pushes | Confirm APNs key uploaded, bundle ID matches, Push extension enabled with correct provider IDs, permissions granted. | +| Token registration fails | Ensure registration runs **after login**, provider IDs are set, and `registerForRemoteNotifications()` is called. | +{/* | Notification taps do nothing | Keep foreground/background handlers and ensure navigation ref is ready before routing. | */} +{/* | Call UI not showing | Verify PushKit VoIP capability, CallKeep entitlements/permissions, and that `voipHandler.initialize()` runs after login. | */} +{/* | Inline reply needed | Extend Notifee action buttons; CometChat expects you to send the message manually after reading `remoteMessage.data`. | */} + +--- + +## Next Steps + + + +Set up FCM push notifications for Android + + +Strip HTML tags and customize notification content + + +Learn how to send different types of messages + + +Handle incoming messages in real time + + diff --git a/notifications/react-native-push-notifications-ios.mdx b/notifications/react-native-push-notifications-ios.mdx index 5bebb86fb..2dbb111d6 100644 --- a/notifications/react-native-push-notifications-ios.mdx +++ b/notifications/react-native-push-notifications-ios.mdx @@ -24,6 +24,10 @@ description: "CometChat push notifications and VoIP calls in React Native apps o Reference implementation of React Native UI Kit, APNs and Push Notification Setup.
+ +Still on the previous setup that copies the sample app's notification stack? The [legacy iOS guide](/notifications/react-native-push-notifications-ios-legacy) still covers it. + + ## What this guide covers - CometChat dashboard setup (enable push, add an APNs provider) with screenshots. From 8c1480c56f849f8a2fd68fe250bb86005f22829d Mon Sep 17 00:00:00 2001 From: Suraj Chauhan Date: Thu, 17 Sep 2026 21:19:14 +0530 Subject: [PATCH 17/20] docs(react-native): drop the legacy page copies, matching the Flutter guides MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The docs keep no legacy push guides in the repo: dfce1bbb1 deleted them all (including the earlier react-native-push-notifications-legacy and flutter-push-notifications pages) in favour of redirects to the legacy archive on assets.cometchat.io, and the Flutter guides have since been rewritten in place. The React Native guides now do the same — the two pages are updated in place, and /notifications/react-native-push-notifications-legacy already redirects to the archive. Removes react-native-push-notifications-android-legacy.mdx and -ios-legacy.mdx, and the notes linking to them. Co-Authored-By: Claude Opus 5 --- ...tive-push-notifications-android-legacy.mdx | 964 ------------------ ...eact-native-push-notifications-android.mdx | 4 - ...t-native-push-notifications-ios-legacy.mdx | 851 ---------------- .../react-native-push-notifications-ios.mdx | 4 - 4 files changed, 1823 deletions(-) delete mode 100644 notifications/react-native-push-notifications-android-legacy.mdx delete mode 100644 notifications/react-native-push-notifications-ios-legacy.mdx diff --git a/notifications/react-native-push-notifications-android-legacy.mdx b/notifications/react-native-push-notifications-android-legacy.mdx deleted file mode 100644 index c4002948c..000000000 --- a/notifications/react-native-push-notifications-android-legacy.mdx +++ /dev/null @@ -1,964 +0,0 @@ ---- -title: "React Native Push Notification (Android) — Legacy" -description: "Bring the SampleAppWithPushNotifications experience—FCM + VoIP calls—into any React Native project using CometChat UI Kit." ---- - - -**Legacy guide.** This page covers the previous setup, which copies the sample app's notification stack (Firebase Messaging, Notifee, CallKeep). For new integrations, use the `@cometchat/push-notifications-react-native` package — see [React Native Push Notifications (Android)](/notifications/react-native-push-notifications-android). - - - - -| Field | Value | -| --- | --- | -| Platform | Android (FCM) | -| Key Classes | `CometChatNotifications`, `VoipNotificationHandler`, `PendingCallManager` | -| Key Methods | `registerPushToken()`, `unregisterPushToken()`, `messaging().getToken()` | -| Push Platform | `CometChatNotifications.PushPlatforms.FCM_REACT_NATIVE_ANDROID` | -| Prerequisites | CometChat SDK initialized, user logged in, FCM configured, `google-services.json` in `android/app` | - - - - - Reference implementation of React Native UI Kit, FCM and Push Notification Setup. - - -## What this guide covers - -- CometChat Dashboard setup (enable push, add FCM providers). -- Platform credentials (Firebase). -- 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. -- Token registration, navigation from pushes, testing, and troubleshooting. - -## What you need first - -- CometChat app credentials (App ID, Region, Auth Key) and Push Notifications enabled with an **FCM provider (React Native Android)**. -- Firebase project with an Android app (`google-services.json` in `android/app`) and Cloud Messaging enabled. -- React Native 0.81+, Node 18+, physical Android devices for reliable push/call testing. - -## How FCM + CometChat work together - -- **FCM (Android) is the transport:** Firebase issues the Android FCM token and delivers payloads to devices. -- **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) - -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 - -- Update `src/utils/AppConstants.tsx` with `appId`, `authKey`, `region`, and `fcmProviderId`. -- Keep `app.json` name consistent with your bundle ID / applicationId. - -```ts lines -const APP_ID = ""; -const AUTH_KEY = ""; -const REGION = ""; -const DEMO_UID = "cometchat-uid-1"; -``` - -### 3.1 Dependencies snapshot (from Sample App) - -Install these dependencies in your React Native app: - -```npm lines -npm install \ - @react-native-firebase/app@23.4.0 \ - @react-native-firebase/messaging@23.4.0 \ - @notifee/react-native@9.1.8 \ - @cometchat/chat-sdk-react-native@4.0.18 \ - @cometchat/calls-sdk-react-native@4.4.0 \ - @cometchat/chat-uikit-react-native@5.2.6 \ - @react-native-async-storage/async-storage@2.2.0 \ - react-native-callkeep@github:cometchat/react-native-callkeep \ - react-native-voip-push-notification@3.3.3 -``` - -Match these or newer compatible versions in your app. - -## 4. Android App Setup - -### 4.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. - -First, add the google-services plugin as a dependency inside of your `/android/build.gradle` file: - -```android lines -buildscript { - dependencies { - // ... other dependencies - classpath("com.google.gms:google-services:4.4.4") - } -} -``` - -Lastly, execute the plugin by adding the following to your `/android/app/build.gradle` file: - -```android lines -apply plugin: 'com.android.application' -apply plugin: 'com.google.gms.google-services' -``` - -### 4.2 Configure required permissions in `AndroidManifest.xml` as shown. - -```xml lines - - - - - - - - - - - - - - - - -``` - -and ask for runtime permissions where needed (e.g. `POST_NOTIFICATIONS` on Android 13+). - -```tsx lines -import { PermissionsAndroid, Platform } from "react-native"; - - const requestAndroidPermissions = async () => { - if (Platform.OS !== 'android') return; - - try { - // Ask for push‑notification permission - const authStatus = await messaging().requestPermission(); - const enabled = - authStatus === messaging.AuthorizationStatus.AUTHORIZED || - authStatus === messaging.AuthorizationStatus.PROVISIONAL; - - if (!enabled) { - console.warn('Notification permission denied (FCM).'); - } - } catch (error) { - console.warn('FCM permission request error:', error); - } - - try { - await PermissionsAndroid.requestMultiple([ - PermissionsAndroid.PERMISSIONS.WRITE_EXTERNAL_STORAGE, - PermissionsAndroid.PERMISSIONS.READ_EXTERNAL_STORAGE, - PermissionsAndroid.PERMISSIONS.CAMERA, - PermissionsAndroid.PERMISSIONS.RECORD_AUDIO, - PermissionsAndroid.PERMISSIONS.POST_NOTIFICATIONS, - ]); - } catch (err) { - console.warn('Android permissions error:', err); - } -} -``` - -### 4.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: - -```ts lines -requestAndroidPermissions(); - -const FCM_TOKEN = await messaging().getToken(); -console.log("FCM Token:", FCM_TOKEN); - -// For React Native Android -CometChatNotifications.registerPushToken( - FCM_TOKEN, - CometChatNotifications.PushPlatforms.FCM_REACT_NATIVE_ANDROID, - "YOUR_FCM_PROVIDER_ID" // from CometChat Dashboard - ) - .then(() => { - console.log("Token registration successful"); - }) - .catch((err) => { - console.log("Token registration failed:", err); - }); -``` - -### 4.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 - -These steps are Android-only—copy/paste and fill your IDs. - -### 5.1 Add CallKeep services to `android/app/src/main/AndroidManifest.xml` -Inside the `` tag add: - -```xml lines - - - - - - - -``` - -### 5.2 Background handler for call pushes (`index.js`) -Data-only FCM calls show the native dialer even when the app is killed. - -```js lines -import messaging from "@react-native-firebase/messaging"; -import { Platform } from "react-native"; -import { CometChat } from "@cometchat/chat-sdk-react-native"; -import { voipHandler } from "./VoipNotificationHandler"; -import { displayLocalNotification } from "./LocalNotificationHandler"; - -if (Platform.OS === "android") { - messaging().setBackgroundMessageHandler(async remoteMessage => { - const data = remoteMessage.data || {}; - if (data.type === "call") { - await voipHandler.initialize(); - switch (data.callAction) { - case "initiated": - voipHandler.msg = data; - await voipHandler.displayCallAndroid(); - break; - case "ended": - case "unanswered": - case "busy": - case "rejected": - case "cancelled": - CometChat.clearActiveCall(); - if (voipHandler?.callerId) { - voipHandler.removeCallDialerWithUUID(voipHandler.callerId); - } - await voipHandler.endCall({ callUUID: voipHandler.callerId }); - break; - case "ongoing": - voipHandler.displayNotification({ - title: data?.receiverName || "", - body: "ongoing call", - }); - break; - default: - break; - } - return; - } - await displayLocalNotification(remoteMessage); - }); -} -``` - -### 5.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 -import { Platform } from "react-native"; -import notifee, { AndroidImportance } from "@notifee/react-native"; -import RNCallKeep, { IOptions } from "react-native-callkeep"; -import { CometChat } from "@cometchat/chat-sdk-react-native"; -import { setPendingAnsweredCall } from "./PendingCallManager"; - -const options: IOptions = { - android: { - alertTitle: "VoIP permissions", - alertDescription: "Allow phone account access to show incoming calls", - cancelButton: "Cancel", - okButton: "OK", - imageName: "ic_notification", - additionalPermissions: [], - foregroundService: { - channelId: "com.cometchat.sampleapp.reactnative.android", - channelName: "Sampleapp Channel", - notificationTitle: "Sampleapp is running in the background", - }, - }, - ios: { appName: "Sampleapp" }, -}; - -function uuid() { - return "xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx".replace(/[xy]/g, (c) => { - const r = Math.floor(Math.random() * 16); - const v = c === "x" ? r : (r & 0x3) | 0x8; - return v.toString(16); - }); -} - -class VoipNotificationHandler { - channelId = ""; - isRinging = false; - isAnswered = false; - pendingAcceptance = false; - callerId = ""; - msg: any = {}; - initialized = false; - private setupPromise: Promise | null = null; - private listenersAttached = false; - - async initialize() { - if (this.initialized && this.setupPromise) { - await this.setupPromise; - return; - } - if (!this.setupPromise) { - this.setupPromise = (async () => { - if (Platform.OS === "android") { - await this.createNotificationChannel(); - } - await this.getPermissions(); - this.setupEventListeners(); - this.initialized = true; - })().catch((err) => { - this.setupPromise = null; - throw err; - }); - } - await this.setupPromise; - } - - async getPermissions() { - await RNCallKeep.setup(options); - RNCallKeep.setAvailable(true); - RNCallKeep.setReachable(); - try { - await RNCallKeep.checkPhoneAccountEnabled(); - } catch {} - } - - async createNotificationChannel() { - this.channelId = await notifee.createChannel({ - id: "message", - name: "Messages", - lights: true, - vibration: true, - importance: AndroidImportance.HIGH, - }); - } - - async displayNotification({ - title, - body, - data, - }: { - title: string; - body: string; - data?: any; - }) { - if (Platform.OS === "android" && !this.channelId) - await this.createNotificationChannel(); - await notifee.displayNotification({ - title, - body, - data, - android: this.channelId - ? { channelId: this.channelId, smallIcon: "ic_launcher" } - : undefined, - }); - } - - async displayCallAndroid() { - if (this.isAnswered || this.pendingAcceptance) return; - await this.initialize(); - this.isRinging = true; - this.callerId = uuid(); - const callerName = this.msg?.senderName || "Incoming Call"; - await RNCallKeep.displayIncomingCall( - this.callerId, - callerName, - callerName, - "generic", - ); - } - - onAnswerCall = async ({ callUUID }: { callUUID: string }) => { - if (this.isAnswered) return; - this.isRinging = false; - this.isAnswered = true; - const sessionID = this.msg?.sessionId; - if (!sessionID) return; - - setTimeout(async () => { - const loggedInUser = await CometChat.getLoggedinUser().catch(() => null); - if (!loggedInUser) { - this.pendingAcceptance = true; - await setPendingAnsweredCall({ - sessionId: sessionID, - raw: this.msg, - storedAt: Date.now(), - }); - try { - RNCallKeep.backToForeground(); - } catch (err) { - // Activity may not exist yet if app was killed - the pending call will be handled when app opens - console.log( - "[VoIP] backToForeground failed, pending call saved:", - err, - ); - } - return; - } - try { - await CometChat.acceptCall(sessionID); - } catch (error: any) { - if (error?.code !== "ERR_CALL_USER_ALREADY_JOINED") throw error; - } - RNCallKeep.endAllCalls(); - this.pendingAcceptance = false; - }, 600); - }; - - endCall = async ({ callUUID }: { callUUID: string }) => { - if (this.msg?.type === "call") { - const sessionID = this.msg.sessionId; - if (this.isAnswered && sessionID) { - this.isAnswered = false; - CometChat.endCall(sessionID); - } else if (sessionID) { - const loggedInUser = await CometChat.getLoggedinUser().catch( - () => null, - ); - if (loggedInUser) { - setTimeout(() => { - CometChat.rejectCall(sessionID, CometChat.CALL_STATUS.REJECTED); - }, 300); - } - } - } - const id = callUUID || this.callerId; - if (id) RNCallKeep.endCall(id); - RNCallKeep.endAllCalls(); - this.isRinging = false; - this.isAnswered = false; - this.pendingAcceptance = false; - this.callerId = ""; - this.msg = {}; - }; - - removeCallDialerWithUUID = (callerId: string) => { - const id = callerId || this.callerId; - if (id) RNCallKeep.reportEndCallWithUUID(id, 6); - }; - - setupEventListeners() { - if (this.listenersAttached) return; - RNCallKeep.addEventListener("answerCall", this.onAnswerCall); - RNCallKeep.addEventListener("endCall", this.endCall); - RNCallKeep.addEventListener("didDisplayIncomingCall", ({ callUUID }) => { - if (callUUID) this.callerId = callUUID; - this.isRinging = true; - }); - this.listenersAttached = true; - } -} - -export const voipHandler = new VoipNotificationHandler(); -``` - -### 5.4 Add `PendingCallManager.ts` -Stores an answered call during cold-start so you can accept it once login/navigation is ready. - -```ts lines -import AsyncStorage from "@react-native-async-storage/async-storage"; - -export interface PendingAnsweredCallPayload { - sessionId: string; - raw: any; - storedAt: number; -} - -let inMemoryPending: PendingAnsweredCallPayload | null = null; -const STORAGE_KEY = "pendingAnsweredCall"; - -export async function setPendingAnsweredCall(payload: PendingAnsweredCallPayload) { - inMemoryPending = payload; - try { await AsyncStorage.setItem(STORAGE_KEY, JSON.stringify(payload)); } catch {} -} - -export async function consumePendingAnsweredCall(): Promise { - if (inMemoryPending) { - const tmp = inMemoryPending; - inMemoryPending = null; - try { await AsyncStorage.removeItem(STORAGE_KEY); } catch {} - return tmp; - } - try { - const raw = await AsyncStorage.getItem(STORAGE_KEY); - if (raw) { - await AsyncStorage.removeItem(STORAGE_KEY); - const parsed: PendingAnsweredCallPayload = JSON.parse(raw); - inMemoryPending = null; - return parsed; - } - } catch {} - return null; -} - -export function isPendingStale(p: PendingAnsweredCallPayload, maxAgeMs = 2 * 60 * 1000) { - return Date.now() - p.storedAt > maxAgeMs; -} -``` - -### 5.5 Wire `App.tsx` to init VoIP + consume pending accepts -Add this after CometChat init/login: - -```ts lines -import { Platform } from "react-native"; -import messaging from "@react-native-firebase/messaging"; -import { CometChat, CometChatNotifications } from "@cometchat/chat-sdk-react-native"; -import { voipHandler } from "./VoipNotificationHandler"; -import { consumePendingAnsweredCall, isPendingStale } from "./PendingCallManager"; - -if (Platform.OS === "android") { - const fcmToken = await messaging().getToken(); - await CometChatNotifications.registerPushToken( - fcmToken, - CometChatNotifications.PushPlatforms.FCM_REACT_NATIVE_ANDROID, - "YOUR_FCM_PROVIDER_ID" - ); -} - -useEffect(() => { - if (Platform.OS === "android" && loggedIn) { - const t = setTimeout(() => voipHandler.initialize(), 3000); - return () => clearTimeout(t); - } -}, [loggedIn]); - -// Handle pending calls in a useEffect -useEffect(() => { - const handlePendingCall = async () => { - const pending = await consumePendingAnsweredCall(); - if (pending && !isPendingStale(pending)) { - try { - await CometChat.acceptCall(pending.sessionId); - } catch (err) { - console.log(err); - } - } - }; - handlePendingCall(); -}, []); -``` - -### 5.6 Call push payload (FCM data) -Send a data-only FCM message like: - -```json -{ - "to": "", - "priority": "high", - "data": { - "type": "call", - "callAction": "initiated", - "sessionId": "", - "senderName": "Alice", - "receiverName": "Bob" - } -} -``` - -### 5.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: - -```ts lines -import { Platform } from "react-native"; -import notifee, { AndroidImportance } from "@notifee/react-native"; - -const CHANNEL_ID = "default"; - -async function ensureChannel(): Promise { - if (Platform.OS !== "android") return undefined; - return notifee.createChannel({ - id: CHANNEL_ID, - name: "Default", - lights: true, - vibration: true, - importance: AndroidImportance.HIGH, - }); -} - -export async function displayLocalNotification(remoteMessage: any) { - try { - const { notification = {}, data = {} } = remoteMessage || {}; - const title = notification?.title || data?.title || "Notification"; - const body = notification?.body || data?.body || ""; - - if (Platform.OS === "ios") { - await notifee.requestPermission(); - } - - const channelId = await ensureChannel(); - - await notifee.displayNotification({ - title, - body, - data, - android: channelId - ? { - channelId, - pressAction: { id: "default" }, - importance: AndroidImportance.HIGH, - smallIcon: "ic_launcher", - } - : undefined, - }); - } catch (error) { - console.error("[LocalNotificationHandler] Failed to display notification", error); - } -} -``` -- 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 - -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 - -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 - - - - Go to **CometChat Dashboard → Notifications Engine → Settings → Preferences → Push Notification Preferences**. - - - Scroll down and enable the **Unread Badge Count** toggle. - - - -Once enabled, CometChat automatically includes the `unreadMessageCount` field in every push payload sent to your app. - -### 7.2 Expected Payload Format - -CometChat sends push notifications with the following structure: - -```jsonc -{ - "data": { - "unreadMessageCount": "5", - "title": "New Message", - "body": "John: Hello!", - "conversationId": "user_abc123", - "parentId": "176001", // Optional - parent message ID; sent only for threaded notifications - "receiverType": "user", - "type": "chat" - } -} -``` - - -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 - -Update your FCM background message handler in `index.js` to extract and set the badge count: - -```javascript -import messaging from "@react-native-firebase/messaging"; -import notifee from "@notifee/react-native"; - -messaging().setBackgroundMessageHandler(async (remoteMessage) => { - const data = remoteMessage.data || {}; - - // Extract and set badge count from push payload - const unreadCount = data?.unreadMessageCount; - if (unreadCount !== undefined && unreadCount !== null) { - const count = parseInt(unreadCount, 10); - if (!isNaN(count) && count >= 0) { - try { - await notifee.setBadgeCount(count); - console.log("Badge count updated (Android):", count); - } catch (error) { - console.error("Error setting badge:", error); - } - } - } - - // Display local notification - await displayLocalNotification(remoteMessage); -}); -``` - -### 7.4 Handle Badge Count in Foreground Messages - -In your `App.tsx`, set up a listener for foreground FCM messages: - -```typescript -import messaging from "@react-native-firebase/messaging"; -import notifee from "@notifee/react-native"; - -useEffect(() => { - if (Platform.OS === "android") { - const unsubscribe = messaging().onMessage(async (remoteMessage) => { - // Extract and set badge count from push payload - const unreadCount = remoteMessage.data?.unreadMessageCount; - if (unreadCount !== undefined && unreadCount !== null) { - const count = parseInt(unreadCount as string, 10); - if (!isNaN(count) && count >= 0) { - try { - await notifee.setBadgeCount(count); - console.log("Badge count updated (Android):", count); - } catch (error) { - console.error("Error setting badge:", error); - } - } - } - - // Display local notification - await displayLocalNotification(remoteMessage); - }); - - return () => unsubscribe(); - } -}, []); -``` - -### 7.5 Display Local Notification with Badge Count - -Update your notification display function to include the badge count: - -```typescript -import notifee, { AndroidImportance } from "@notifee/react-native"; - -export async function displayLocalNotification(remoteMessage: any) { - const { title, body, senderAvatar } = remoteMessage.data || {}; - - // Create notification channel - const channelId = await notifee.createChannel({ - id: "chat-messages", - name: "Chat Messages", - vibration: true, - importance: AndroidImportance.HIGH, - }); - - // Parse badge count from payload - const unreadCount = remoteMessage.data?.unreadMessageCount; - const badgeCount = unreadCount ? parseInt(unreadCount, 10) : undefined; - - // Optionally enhance title with unread count - const displayTitle = - badgeCount && badgeCount > 1 - ? `${title || "New Message"} (${badgeCount} unread)` - : title || "New Message"; - - // Update badge count - if (badgeCount && badgeCount > 0) { - await notifee.setBadgeCount(badgeCount); - } - - // Display notification with fixed ID to prevent badge accumulation - // on devices that sum badge counts from multiple notifications - await notifee.displayNotification({ - id: "chat-notification", - title: displayTitle, - body: body || "You received a new message.", - android: { - channelId, - autoCancel: true, - smallIcon: "ic_notification", - largeIcon: - senderAvatar || - "https://cdn-icons-png.flaticon.com/512/149/149071.png", - importance: AndroidImportance.HIGH, - badgeCount: badgeCount, - pressAction: { - id: "default", - }, - }, - data: { - receiverType: remoteMessage.data?.receiverType, - sender: remoteMessage.data?.sender, - conversationId: remoteMessage.data?.conversationId, - }, - }); -} -``` - -### 7.6 Clear Badge When App Becomes Active - -Clear all notifications and reset the badge when the app returns to the foreground: - -```typescript -import { AppState, AppStateStatus, Platform } from "react-native"; -import notifee from "@notifee/react-native"; - -useEffect(() => { - const handleAppStateChange = async (nextState: AppStateStatus) => { - if (nextState === "active" && Platform.OS === "android") { - // Clear all notifications (also resets badge count) - await notifee.cancelAllNotifications(); - console.log("Notifications cleared (Android)"); - } - }; - - const subscription = AppState.addEventListener("change", handleAppStateChange); - return () => subscription.remove(); -}, []); -``` - -### 7.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: - -```typescript -import notifee from "@notifee/react-native"; -import { CometChat, CometChatNotifications } from "@cometchat/chat-sdk-react-native"; - -const handleLogout = async () => { - // Unregister push token first - await CometChatNotifications.unregisterPushToken(); - - // Clear badge before logout - await notifee.setBadgeCount(0); - await notifee.cancelAllNotifications(); - - // Logout from CometChat - await CometChat.logout(); - console.log("User logged out, badge cleared"); -}; -``` - -### 7.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: - -```typescript -import notifee from "@notifee/react-native"; -import { CometChat } from "@cometchat/chat-sdk-react-native"; - -// During app initialization, after CometChat.init() -const initializeApp = async () => { - // Initialize CometChat first - await CometChatUIKit.init(uiKitSettings); - - // Check if user is logged in - const loggedInUser = await CometChat.getLoggedinUser(); - - if (!loggedInUser) { - // No user logged in - clear any stale badge - await notifee.setBadgeCount(0); - await notifee.cancelAllNotifications(); - console.log("No logged-in user, badge cleared"); - } -}; -``` - -### 7.9 Clear Badge in Login Listener (Safety Net) - -Register a login listener to clear the badge on logout as a backup mechanism: - -```typescript -import notifee from "@notifee/react-native"; -import { CometChat } from "@cometchat/chat-sdk-react-native"; - -useEffect(() => { - const listenerID = "BADGE_LOGOUT_LISTENER"; - - CometChat.addLoginListener( - listenerID, - new CometChat.LoginListener({ - logoutOnSuccess: async () => { - // Safety net: clear badge when logout succeeds - await notifee.setBadgeCount(0); - await notifee.cancelAllNotifications(); - console.log("Logout listener: badge cleared"); - }, - }) - ); - - return () => { - CometChat.removeLoginListener(listenerID); - }; -}, []); -``` - -### 7.10 Key Implementation Notes - -| Consideration | Details | -| --- | --- | -| **Backend-driven badge count** | The `unreadMessageCount` value comes directly from CometChat's backend via the push payload, ensuring consistency across all devices. | -| **Fixed notification ID** | Using a fixed notification ID (`'chat-notification'`) prevents certain devices from accumulating badge counts across multiple notifications. The badge always reflects the exact `unreadMessageCount` from the backend. | -| **Clear on app active** | Always clear the badge when the app becomes active. New notifications will update the badge with the fresh `unreadMessageCount` from the backend. | -| **Clear on logout** | Always clear the badge when a user logs out to prevent stale counts for the next user. | -| **Clear on fresh install** | Clear the badge during app initialization when no user is logged in to handle reinstall scenarios. | -| **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 - -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: - - **Foreground:** Notifee banner appears unless that chat is already open. - - **Background/terminated:** Tap opens the correct conversation; Notifee background handler runs. -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 - -| Symptom | Quick Checks | -| --- | --- | -| No pushes | Confirm `google-services.json` location, package IDs match Firebase, Push extension enabled with correct provider IDs, permissions granted. | -| Token registration fails | Ensure registration runs **after login**, provider IDs are set, and `registerDeviceForRemoteMessages()` is called. | -{/* | Notification taps do nothing | Keep Notifee foreground/background handlers and ensure the navigation ref is ready before routing. | */} -{/* | Call UI not showing | Verify CallKeep setup, telecom permissions, and that `VoipNotificationHandler.initialize()` runs post-login. | */} -{/* | Inline reply needed | Extend Notifee action buttons; CometChat expects you to send the message manually after reading `remoteMessage.data`. | */} - ---- - -## Next Steps - - - -Set up APNs push notifications for iOS - - -Strip HTML tags and customize notification content - - -Learn how to send different types of messages - - -Handle incoming messages in real time - - diff --git a/notifications/react-native-push-notifications-android.mdx b/notifications/react-native-push-notifications-android.mdx index 055dac326..c39210f61 100644 --- a/notifications/react-native-push-notifications-android.mdx +++ b/notifications/react-native-push-notifications-android.mdx @@ -24,10 +24,6 @@ description: "CometChat push notifications and VoIP calls in React Native apps o Reference implementation of React Native UI Kit, FCM and Push Notification Setup.
- -Still on the previous setup that copies the sample app's notification stack? The [legacy Android guide](/notifications/react-native-push-notifications-android-legacy) still covers it. - - ## What this guide covers - CometChat dashboard setup (enable push, add FCM provider) with screenshots. diff --git a/notifications/react-native-push-notifications-ios-legacy.mdx b/notifications/react-native-push-notifications-ios-legacy.mdx deleted file mode 100644 index 27e705cab..000000000 --- a/notifications/react-native-push-notifications-ios-legacy.mdx +++ /dev/null @@ -1,851 +0,0 @@ ---- -title: "React Native Push Notifications (iOS) — Legacy" -description: "Bring the SampleAppWithPushNotifications experience—APNs + VoIP—into any React Native project using CometChat UI Kit." ---- - - -**Legacy guide.** This page covers the previous setup, which copies the sample app's notification stack (APNs, CallKeep, react-native-voip-push-notification). For new integrations, use the `@cometchat/push-notifications-react-native` package — see [React Native Push Notifications (iOS)](/notifications/react-native-push-notifications-ios). - - - - -| Field | Value | -| --- | --- | -| Platform | iOS (APNs + PushKit/CallKit) | -| Key Classes | `CometChatNotifications`, `VoipNotificationHandler`, `PendingCallManager` | -| Key Methods | `registerPushToken()`, `unregisterPushToken()`, `PushNotificationIOS.requestPermissions()` | -| Push Platforms | `APNS_REACT_NATIVE_DEVICE`, `APNS_REACT_NATIVE_VOIP` | -| Prerequisites | CometChat SDK initialized, user logged in, APNs `.p8` key uploaded, physical iOS device | - - - - - Reference implementation of React Native UI Kit and APNs Push Notification setup. - - -## What this guide covers - -- CometChat Dashboard setup (enable push, add APNs provider). -- Platform credentials (Apple entitlements). -- 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. - -## What you need first - -- CometChat app credentials (App ID, Region, Auth Key) and Push Notifications enabled with an **APNs provider (React Native iOS)**; add an **APNs VoIP provider** if you plan to receive call invites via PushKit. -- Apple push setup: APNs `.p8` key/cert in CometChat, iOS project with Push Notifications + Background Modes (Remote notifications) permissions. -- React Native 0.81+, Node 18+, physical iOS device for reliable push/call testing. - -## How APNs + CometChat work together - -- **APNs (iOS) is the transport:** Apple issues the APNs token and delivers payloads to devices. -- **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) - -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 - -- Update `src/utils/AppConstants.tsx` with `appId`, `authKey`, `region`, and `apnProviderId`. -- Keep `app.json` name consistent with your bundle ID / applicationId. - -```ts lines -const APP_ID = ""; -const AUTH_KEY = ""; -const REGION = ""; -const DEMO_UID = "cometchat-uid-1"; -``` - -### 3.1 Dependencies snapshot (from Sample App) - -Install these dependencies in your React Native app: - -```npm lines -npm install \ - @cometchat/chat-sdk-react-native@4.0.18 \ - @cometchat/calls-sdk-react-native@4.4.0 \ - @cometchat/chat-uikit-react-native@5.2.6 \ - @notifee/react-native@9.1.8 \ - @react-native-async-storage/async-storage@2.2.0 \ - @react-native-community/push-notification-ios@1.12.0 \ - react-native-push-notification@8.1.1 \ - react-native-callkeep@4.3.16 \ - react-native-voip-push-notification@3.3.3 -``` - -Match these or newer compatible versions in your app. - -## 4. iOS App setup - -### 4.1 Project Setup - -Enable **Push Notifications** and **Background Modes** (Remote notifications) in Xcode. - - - Enable Push Notifications - - -### 4.2 Install dependencies + pods - -After running the npm install above, install pods from the `ios` directory: -```bash lines -cd ios -pod install -``` - -### 4.3 AppDelegate.swift modifications: - -Add imports at the top: -```swift lines -import UserNotifications -import RNCPushNotificationIOS -``` - -Add `UNUserNotificationCenterDelegate` to the `AppDelegate` class declaration: -```swift -class AppDelegate: UIResponder, UIApplicationDelegate, UNUserNotificationCenterDelegate -``` - -Add the following inside the `didFinishLaunchingWithOptions` method: -```swift lines -UNUserNotificationCenter.current().delegate = self - -UNUserNotificationCenter.current().requestAuthorization( - options: [.alert, .badge, .sound] -) { - granted, - error in - if granted { - DispatchQueue.main.async { - application.registerForRemoteNotifications() - } - } else { - print("Push Notification permission not granted: \(String(describing: error))") - } -} -``` - -Add the following methods to handle push notification events: -```swift lines -func application(_ application: UIApplication, didRegisterForRemoteNotificationsWithDeviceToken deviceToken: Data) { - print("APNs device token received: \(deviceToken)") - RNCPushNotificationIOS.didRegisterForRemoteNotifications(withDeviceToken: deviceToken) -} - -func application(_ application: UIApplication, didFailToRegisterForRemoteNotificationsWithError error: Error) { - print("APNs registration failed: \(error)") - RNCPushNotificationIOS.didFailToRegisterForRemoteNotificationsWithError(error) -} - -func application(_ application: UIApplication, didReceiveRemoteNotification userInfo: [AnyHashable: Any], fetchCompletionHandler completionHandler: @escaping (UIBackgroundFetchResult) -> Void) { - RNCPushNotificationIOS.didReceiveRemoteNotification(userInfo, fetchCompletionHandler: completionHandler) -} - -func userNotificationCenter(_ center: UNUserNotificationCenter, willPresent notification: UNNotification, withCompletionHandler completionHandler: @escaping (UNNotificationPresentationOptions) -> Void) { - completionHandler([.banner, .sound, .badge]) -} - -func userNotificationCenter(_ center: UNUserNotificationCenter, didReceive response: UNNotificationResponse, withCompletionHandler completionHandler: @escaping () -> Void) { - RNCPushNotificationIOS.didReceive(response) - completionHandler() -} -``` - -Add the following to `Podfile` to avoid framework linkage issues: -```ruby -use_frameworks! :linkage => :static -``` - -You might have to remove below code if already present in your Podfile: -```ruby lines -linkage = ENV['USE_FRAMEWORKS'] -if linkage != nil - Pod::UI.puts "Configuring Pod with #{linkage}ally linked Frameworks".green - use_frameworks! :linkage => linkage.to_sym -end -``` - -Then lets install pods and open the workspace: -```bash lines -cd ios -pod install -open YourProjectName.xcworkspace -``` - -### 4.4 App.tsx modifications: - -Import CometChatNotifications and PushNotificationIOS: - -```tsx -import { CometChat, CometChatNotifications } from "@cometchat/chat-sdk-react-native"; -import PushNotificationIOS from "@react-native-community/push-notification-ios"; -``` - -Get device token and store it in a ref: -Also, define your APNs provider ID from the CometChat Dashboard. -And request permissions on mount: - -```tsx lines -const APNS_PROVIDER_ID = 'YOUR_APNS_PROVIDER_ID'; // from CometChat Dashboard -const apnsTokenRef = useRef < string | null > (null); - -useEffect(() => { - if (Platform.OS !== 'ios') return; - - const onRegister = (deviceToken: string) => { - console.log(' APNs device token captured:', deviceToken); - apnsTokenRef.current = deviceToken; - }; - - PushNotificationIOS.addEventListener('register', onRegister); - - PushNotificationIOS.addEventListener('registrationError', error => { - console.error(' APNs registration error:', error); - }); - - // Trigger permission + native registration - PushNotificationIOS.requestPermissions().then(p => - console.log('Push permissions:', p), - ); - - return () => { - PushNotificationIOS.removeEventListener('register'); - PushNotificationIOS.removeEventListener('registrationError'); - }; -}, []); -``` - -After user login, register the APNs token: -```tsx lines -// Register token ONLY if we already have it -if (apnsTokenRef.current) { - await CometChatNotifications.registerPushToken( - apnsTokenRef.current, - CometChatNotifications.PushPlatforms.APNS_REACT_NATIVE_DEVICE, - APNS_PROVIDER_ID - ); - console.log(' APNs token registered with CometChat'); -} -``` - -Prior to logout, unregister the APNs token: -```tsx -await CometChatNotifications.unregisterPushToken(); -``` - -## 5. VoIP call notifications (iOS) - -These steps are iOS-only—copy/paste and fill your IDs. - -### 5.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) -Update your `AppDelegate` to register for VoIP pushes ASAP and forward events to JS/CallKeep: - -```swift lines -import PushKit -import RNVoipPushNotification -import RNCallKeep -// ... -@main -class AppDelegate: UIResponder, UIApplicationDelegate, UNUserNotificationCenterDelegate, PKPushRegistryDelegate { - // ... - func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? = nil) -> Bool { - // existing UNUserNotificationCenter code ... - RNVoipPushNotificationManager.voipRegistration() // triggers PushKit token - return true - } - - // APNs device token handlers stay unchanged - - // PushKit token -> JS - func pushRegistry(_ registry: PKPushRegistry, didUpdate pushCredentials: PKPushCredentials, for type: PKPushType) { - RNVoipPushNotificationManager.didUpdate(pushCredentials, forType: type.rawValue) - } - - // Incoming VoIP push -> CallKit + JS - func pushRegistry(_ registry: PKPushRegistry, didReceiveIncomingPushWith payload: PKPushPayload, for type: PKPushType, completion: @escaping () -> Void) { - let dict = payload.dictionaryPayload - let uuid = (dict["uuid"] as? String) ?? UUID().uuidString - RNVoipPushNotificationManager.addCompletionHandler(uuid, completionHandler: completion) - RNVoipPushNotificationManager.didReceiveIncomingPush(with: payload, forType: type.rawValue) - RNCallKeep.reportNewIncomingCall(uuid, handle: (dict["handle"] as? String) ?? "Unknown", handleType: "generic", hasVideo: false, localizedCallerName: (dict["callerName"] as? String) ?? "Incoming Call", supportsHolding: true, supportsDTMF: true, supportsGrouping: true, supportsUngrouping: true, fromPushKit: true, payload: nil) - } -} -``` - -### 5.3 Drop in `VoipNotificationHandler.ts` -Handles CallKeep UI, defers acceptance until login, and listens for PushKit events. - -```ts lines -import { Platform } from "react-native"; -import notifee, { AndroidImportance } from "@notifee/react-native"; -import RNCallKeep, { IOptions } from "react-native-callkeep"; -import { CometChat } from "@cometchat/chat-sdk-react-native"; -import VoipPushNotification from "react-native-voip-push-notification"; -import { setPendingAnsweredCall } from "./PendingCallManager"; - -const options: IOptions = { - ios: { appName: "YourAppName" }, - android: { alertTitle: "VOIP required", alertDescription: "Allow phone account access", cancelButton: "Cancel", okButton: "OK", imageName: "ic_notification" }, -}; - -type IncomingPayload = { sessionId?: string; senderName?: string; callerName?: string; name?: string; type?: string; [k: string]: any; }; - -class VoipNotificationHandler { - channelId = ""; - isRinging = false; - isAnswered = false; - pendingAcceptance = false; - callerId = ""; - msg: IncomingPayload | null = null; - initialized = false; - private setupPromise: Promise | null = null; - private listenersAttached = false; - private lastSessionId: string | null = null; - private lastRingAt = 0; - - async initialize() { - if (this.initialized && this.setupPromise) { await this.setupPromise; return; } - if (!this.setupPromise) { - this.setupPromise = (async () => { - if (Platform.OS === "android") { await this.createNotificationChannel(); } - await this.setupCallKeep(); - this.setupEventListeners(); - this.initialized = true; - })().catch(err => { this.setupPromise = null; throw err; }); - } - await this.setupPromise; - } - - private async setupCallKeep() { - await RNCallKeep.setup(options); - RNCallKeep.setAvailable(true); - if (Platform.OS === "android") { RNCallKeep.setReachable(); } - } - - private async createNotificationChannel() { - this.channelId = await notifee.createChannel({ id: "message", name: "Messages", lights: true, vibration: true, importance: AndroidImportance.HIGH }); - } - - async displayIncomingCall(payload: IncomingPayload) { - this.msg = payload || {}; - const sessionId = this.msg?.sessionId; - const now = Date.now(); - if (sessionId && this.lastSessionId === sessionId && now - this.lastRingAt < 5000) return; - if (this.isAnswered || this.pendingAcceptance) return; - await this.initialize(); - - const callerName = this.msg?.senderName || this.msg?.callerName || this.msg?.name || "Incoming Call"; - this.callerId = this.callerId || Math.random().toString(); - this.isRinging = true; - - await RNCallKeep.displayIncomingCall(this.callerId, callerName, callerName, "generic", true); - this.lastSessionId = sessionId || null; - this.lastRingAt = now; - } - - onAnswerCall = async ({ callUUID }: { callUUID: string }) => { - if (this.isAnswered) return; - this.isRinging = false; this.isAnswered = true; - const sessionID = this.msg?.sessionId; if (!sessionID) return; - RNCallKeep.backToForeground(); - setTimeout(async () => { - const loggedInUser = await CometChat.getLoggedinUser().catch(() => null); - if (!loggedInUser) { this.pendingAcceptance = true; await setPendingAnsweredCall({ sessionId: sessionID, raw: this.msg, storedAt: Date.now() }); return; } - try { await CometChat.acceptCall(sessionID); } catch (error: any) { if (error?.code !== "ERR_CALL_USER_ALREADY_JOINED") throw error; } - RNCallKeep.endAllCalls(); this.pendingAcceptance = false; - }, 350); - }; - - endCall = async ({ callUUID }: { callUUID: string }) => { - const sessionID = this.msg?.sessionId; - if (sessionID) { - const loggedInUser = await CometChat.getLoggedinUser().catch(() => null); - if (this.isAnswered) { await CometChat.endCall(sessionID).catch(() => {}); } - else if (loggedInUser) { await CometChat.rejectCall(sessionID, CometChat.CALL_STATUS.REJECTED).catch(() => {}); } - } - const id = callUUID || this.callerId; - if (id) RNCallKeep.endCall(id); - RNCallKeep.endAllCalls(); - this.isRinging = false; this.isAnswered = false; this.pendingAcceptance = false; this.callerId = ""; this.msg = null; this.lastSessionId = null; this.lastRingAt = 0; - }; - - setupEventListeners() { - if (this.listenersAttached) return; - if (Platform.OS === "ios") { - VoipPushNotification.addEventListener("notification", (notification: any) => this.displayIncomingCall(notification)); - VoipPushNotification.addEventListener("didLoadWithEvents", (events: any[]) => { - (events || []).forEach(event => { - if (event?.name === VoipPushNotification.RNVoipPushRemoteNotificationReceivedEvent) { - this.displayIncomingCall(event.data); - } - }); - }); - } - RNCallKeep.addEventListener("answerCall", this.onAnswerCall); - RNCallKeep.addEventListener("endCall", this.endCall); - RNCallKeep.addEventListener("didDisplayIncomingCall", ({ callUUID }) => { if (callUUID) this.callerId = callUUID; this.isRinging = true; }); - this.listenersAttached = true; - } -} - -export const voipHandler = new VoipNotificationHandler(); -``` - -### 5.4 Add `PendingCallManager.ts` -Stores an answered call during cold start so you can accept it after login/navigation is ready. - -```ts lines -import AsyncStorage from "@react-native-async-storage/async-storage"; - -export interface PendingAnsweredCallPayload { sessionId: string; raw: any; storedAt: number; } -let inMemoryPending: PendingAnsweredCallPayload | null = null; -const STORAGE_KEY = "pendingAnsweredCall"; - -export async function setPendingAnsweredCall(payload: PendingAnsweredCallPayload) { - inMemoryPending = payload; try { await AsyncStorage.setItem(STORAGE_KEY, JSON.stringify(payload)); } catch {} -} - -export async function consumePendingAnsweredCall(): Promise { - if (inMemoryPending) { const tmp = inMemoryPending; inMemoryPending = null; try { await AsyncStorage.removeItem(STORAGE_KEY); } catch {} return tmp; } - try { const raw = await AsyncStorage.getItem(STORAGE_KEY); if (raw) { await AsyncStorage.removeItem(STORAGE_KEY); return JSON.parse(raw); } } catch {} - return null; -} - -export function isPendingStale(p: PendingAnsweredCallPayload, maxAgeMs = 2 * 60 * 1000) { - return Date.now() - p.storedAt > maxAgeMs; -} -``` - -### 5.5 Wire `App.tsx` for APNs + VoIP token registration and handler init - -```tsx lines -import PushNotificationIOS from "@react-native-community/push-notification-ios"; -import VoipPushNotification from "react-native-voip-push-notification"; -import { voipHandler } from "./VoipNotificationHandler"; -import { consumePendingAnsweredCall, isPendingStale } from "./PendingCallManager"; - -const APNS_PROVIDER_ID = "YOUR_APNS_PROVIDER_ID"; - -// Capture APNs device token -useEffect(() => { - if (Platform.OS !== "ios") return; - const onRegister = (deviceToken: string) => { apnsTokenRef.current = deviceToken; }; - PushNotificationIOS.addEventListener("register", onRegister); - PushNotificationIOS.requestPermissions(); - return () => PushNotificationIOS.removeEventListener("register"); -}, []); - -// Capture VoIP token -useEffect(() => { - if (Platform.OS !== "ios") return; - const onVoipRegister = (token: string) => { - CometChatNotifications.registerPushToken( - token, - CometChatNotifications.PushPlatforms.APNS_REACT_NATIVE_VOIP, - APNS_PROVIDER_ID - ).catch(err => console.log("[VoIP] register failed", err)); - }; - VoipPushNotification.addEventListener("register", onVoipRegister); - // token request is triggered in AppDelegate via RNVoipPushNotificationManager.voipRegistration() - return () => VoipPushNotification.removeEventListener("register"); -}, []); - -// After login: register APNs token + init VoIP handler + consume pending accepts -useEffect(() => { - const run = async () => { - if (!loggedIn || Platform.OS !== "ios") return; - const pending = await consumePendingAnsweredCall(); - if (pending && !isPendingStale(pending)) { await CometChat.acceptCall(pending.sessionId).catch(console.log); } - const token = apnsTokenRef.current; - if (token) { - await CometChatNotifications.registerPushToken( - token, - CometChatNotifications.PushPlatforms.APNS_REACT_NATIVE_DEVICE, - APNS_PROVIDER_ID - ); - } - await voipHandler.initialize(); - }; - run(); -}, [loggedIn]); -``` - -### 5.6 VoIP push payload (APNs / PushKit) -Send a VoIP push with `push_type=voip` via APNs using a payload shaped like: - -```json -{ - "aps": { "alert": { "title": "Alice", "body": "Incoming call" }, "content-available": 1 }, - "sessionId": "", - "callerName": "Alice", - "handle": "alice", - "type": "call", - "uuid": "" -} -``` - -## 6. 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 - -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 - - - - Go to **CometChat Dashboard → Notifications Engine → Settings → Preferences → Push Notification Preferences**. - - - Scroll down and enable the **Unread Badge Count** toggle. - - - -Once enabled, CometChat automatically includes the `unreadMessageCount` field in every push payload sent to your app. - -### 7.2 Expected Payload Format - -CometChat sends APNs payloads with the following structure: - -```jsonc -{ - "aps": { - "alert": { - "title": "New Message", - "body": "John: Hello!" - }, - "badge": 5, - "sound": "default" - }, - "unreadMessageCount": "5", - "conversationId": "user_abc123", - "parentId": "176001" // Optional - parent message ID; sent only for threaded notifications -} -``` - - -The `aps.badge` field is set server-side by CometChat. iOS automatically updates the app icon badge when the push notification is delivered. - - -### 7.3 Handle Badge Count from Notifications - -Update your iOS notification handler to set the badge count programmatically: - -```typescript -import PushNotificationIOS from "@react-native-community/push-notification-ios"; - -export async function onRemoteNotificationIOS(notification: any) { - // Extract badge count from push payload - const data = notification.getData(); - const unreadCount = data?.unreadMessageCount; - - if (unreadCount !== undefined && unreadCount !== null) { - const count = parseInt(unreadCount, 10); - if (!isNaN(count) && count >= 0) { - PushNotificationIOS.setApplicationIconBadgeNumber(count); - console.log("Badge count updated (iOS):", count); - } - } - - // Handle notification tap - const isClicked = data?.userInteraction === 1; - if (isClicked && data?.type === "chat") { - // Navigate to conversation... - } - - // Required: Notify iOS that processing is complete - notification.finish(PushNotificationIOS.FetchResult.NoData); -} -``` - -### 7.4 Register Notification Listener - -In your `App.tsx`, set up the notification listener: - -```typescript -import PushNotificationIOS from "@react-native-community/push-notification-ios"; - -useEffect(() => { - if (Platform.OS === "ios") { - const onNotification = async (notification: any) => { - try { - await onRemoteNotificationIOS(notification); - } catch (error) { - console.log("Error in onRemoteNotificationIOS:", error); - } - }; - - PushNotificationIOS.addEventListener("notification", onNotification); - - return () => { - PushNotificationIOS.removeEventListener("notification"); - }; - } -}, []); -``` - -### 7.5 Clear Badge When App Becomes Active - -Clear the badge count when the app launches or returns to the foreground: - -```typescript -import { AppState, AppStateStatus, Platform } from "react-native"; -import PushNotificationIOS from "@react-native-community/push-notification-ios"; - -useEffect(() => { - const handleAppStateChange = async (nextState: AppStateStatus) => { - if (nextState === "active" && Platform.OS === "ios") { - PushNotificationIOS.setApplicationIconBadgeNumber(0); - console.log("Badge cleared (iOS)"); - } - }; - - const subscription = AppState.addEventListener("change", handleAppStateChange); - return () => subscription.remove(); -}, []); -``` - -### 7.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: - -```typescript -import PushNotificationIOS from "@react-native-community/push-notification-ios"; -import { CometChat, CometChatNotifications } from "@cometchat/chat-sdk-react-native"; - -const handleLogout = async () => { - // Unregister push token first - await CometChatNotifications.unregisterPushToken(); - - // Clear badge before logout - PushNotificationIOS.setApplicationIconBadgeNumber(0); - - // Logout from CometChat - await CometChat.logout(); - console.log("User logged out, badge cleared"); -}; -``` - -### 7.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: - -```typescript -import PushNotificationIOS from "@react-native-community/push-notification-ios"; -import { CometChat } from "@cometchat/chat-sdk-react-native"; - -// During app initialization, after CometChat.init() -const initializeApp = async () => { - // Initialize CometChat first - await CometChatUIKit.init(uiKitSettings); - - // Check if user is logged in - const loggedInUser = await CometChat.getLoggedinUser(); - - if (!loggedInUser) { - // No user logged in - clear any stale badge - PushNotificationIOS.setApplicationIconBadgeNumber(0); - console.log("No logged-in user, badge cleared"); - } -}; -``` - -### 7.8 Clear Badge in Login Listener (Safety Net) - -Register a login listener to clear the badge on logout as a backup mechanism: - -```typescript -import PushNotificationIOS from "@react-native-community/push-notification-ios"; -import { CometChat } from "@cometchat/chat-sdk-react-native"; - -useEffect(() => { - const listenerID = "BADGE_LOGOUT_LISTENER"; - - CometChat.addLoginListener( - listenerID, - new CometChat.LoginListener({ - logoutOnSuccess: () => { - // Safety net: clear badge when logout succeeds - PushNotificationIOS.setApplicationIconBadgeNumber(0); - console.log("Logout listener: badge cleared"); - }, - }) - ); - - return () => { - CometChat.removeLoginListener(listenerID); - }; -}, []); -``` - -### 7.9 Key Implementation Notes - -| Consideration | Details | -| --- | --- | -| **Backend-driven badge count** | The `unreadMessageCount` value comes directly from CometChat's backend via the push payload, ensuring consistency across all devices. | -| **iOS server-side badge** | For iOS using APNs, the `aps.badge` field is set server-side by CometChat, so the badge updates automatically even without client-side code. However, you still need to clear it when the app opens. | -| **Clear on app active** | Always clear the badge when the app becomes active. New notifications will update the badge with the fresh `unreadMessageCount` from the backend. | -| **Clear on logout** | Always clear the badge when a user logs out to prevent stale counts for the next user. | -| **Clear on fresh install** | On iOS, the badge count may persist after app reinstall in certain scenarios. Clear the badge during app initialization when no user is logged in. | -| **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 - -If you're building a cross-platform app, use this combined handler for both iOS and Android: - -```typescript -import { AppState, AppStateStatus, Platform } from "react-native"; -import PushNotificationIOS from "@react-native-community/push-notification-ios"; -import notifee from "@notifee/react-native"; - -useEffect(() => { - const handleAppStateChange = async (nextState: AppStateStatus) => { - if (nextState === "active") { - // Clear badge for iOS - if (Platform.OS === "ios") { - PushNotificationIOS.setApplicationIconBadgeNumber(0); - console.log("Badge cleared (iOS)"); - } - // Clear all notifications for Android (also resets badge) - else if (Platform.OS === "android") { - await notifee.cancelAllNotifications(); - console.log("Notifications cleared (Android)"); - } - } - }; - - const subscription = AppState.addEventListener("change", handleAppStateChange); - return () => subscription.remove(); -}, []); -``` - -## 8. Testing Checklist - -1. Install on a physical iOS device, log in, and verify APNs token registration succeeds. -2. Send a message from another user: - - **Foreground:** Banner appears unless that chat is already open. - - **Background/terminated:** Tap opens the correct conversation; handler runs. -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 - -| Symptom | Quick Checks | -| --- | --- | -| No pushes | Confirm APNs key uploaded, bundle ID matches, Push extension enabled with correct provider IDs, permissions granted. | -| Token registration fails | Ensure registration runs **after login**, provider IDs are set, and `registerForRemoteNotifications()` is called. | -{/* | Notification taps do nothing | Keep foreground/background handlers and ensure navigation ref is ready before routing. | */} -{/* | Call UI not showing | Verify PushKit VoIP capability, CallKeep entitlements/permissions, and that `voipHandler.initialize()` runs after login. | */} -{/* | Inline reply needed | Extend Notifee action buttons; CometChat expects you to send the message manually after reading `remoteMessage.data`. | */} - ---- - -## Next Steps - - - -Set up FCM push notifications for Android - - -Strip HTML tags and customize notification content - - -Learn how to send different types of messages - - -Handle incoming messages in real time - - diff --git a/notifications/react-native-push-notifications-ios.mdx b/notifications/react-native-push-notifications-ios.mdx index 2dbb111d6..5bebb86fb 100644 --- a/notifications/react-native-push-notifications-ios.mdx +++ b/notifications/react-native-push-notifications-ios.mdx @@ -24,10 +24,6 @@ description: "CometChat push notifications and VoIP calls in React Native apps o Reference implementation of React Native UI Kit, APNs and Push Notification Setup.
- -Still on the previous setup that copies the sample app's notification stack? The [legacy iOS guide](/notifications/react-native-push-notifications-ios-legacy) still covers it. - - ## What this guide covers - CometChat dashboard setup (enable push, add an APNs provider) with screenshots. From a6c767c618ff47d1391cdf3ba8c2e53318405f1c Mon Sep 17 00:00:00 2001 From: Suraj Chauhan Date: Fri, 18 Sep 2026 12:37:56 +0530 Subject: [PATCH 18/20] docs(react-native): ringInForeground: false needs an in-app incoming-call screen MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit With ringInForeground: false (as the guide's init() sets it), the package doesn't ring while the app is open, so the app has to show its own incoming-call screen — the guide implied the UI Kit does this by itself, and a new app following it had no ringing at all while open. - New src/calls/IncomingCall.tsx: shows the UI Kit's CometChatIncomingCall for a call that arrives while the app is open (it accepts and shows the call itself), and removes it on decline, caller hang-up, or call end. Rendered before the navigator — it isn't a modal. - Tip: already have an in-app screen → keep it; don't want one → ringInForeground: true (the default). - init() options, call-event tables, testing checklist, troubleshooting and the non-UI-Kit note updated to match. Verified on a from-scratch React Native 0.81 app on Android: with the app open, the in-app screen rang (not the system UI); accept connected with audio; decline rejected; caller hang-up removed the screen. The code blocks are byte-identical to the tested files. Co-Authored-By: Claude Opus 5 --- ...eact-native-push-notifications-android.mdx | 61 ++++++++++++++++--- .../react-native-push-notifications-ios.mdx | 61 ++++++++++++++++--- 2 files changed, 108 insertions(+), 14 deletions(-) diff --git a/notifications/react-native-push-notifications-android.mdx b/notifications/react-native-push-notifications-android.mdx index c39210f61..1a9ec3441 100644 --- a/notifications/react-native-push-notifications-android.mdx +++ b/notifications/react-native-push-notifications-android.mdx @@ -214,7 +214,7 @@ export function setupPushOnLogin(): () => void { apnsProviderId: AppCredentials.apnsProviderId, // iOS (APNs device + VoIP) notificationSmallIcon: 'ic_notification', // Android status-bar icon showInForeground: true, // one notification while the app is open, too - ringInForeground: false, // the UI Kit shows its own incoming-call screen while open + ringInForeground: false, // your app rings while it's open — see src/calls/IncomingCall.tsx }); }; start().catch(error => console.log('Push setup failed:', error)); @@ -315,11 +315,55 @@ function endCall(info: PNCallEndEvent): void { } ``` +**`src/calls/IncomingCall.tsx`** — `init()` above sets `ringInForeground: false`, so **while the app is open the package doesn't ring: your app must show its own incoming-call screen**, or calls won't ring at all while it's open. Calls reach an open app over the Chat SDK's connection; this component listens for them and shows the UI Kit's `CometChatIncomingCall`, which accepts the call and shows the call screen itself: + +```tsx src/calls/IncomingCall.tsx lines +import React, { useEffect, useState } from 'react'; +import { CometChat } from '@cometchat/chat-sdk-react-native'; +import { CometChatIncomingCall, CometChatUIEventHandler } from '@cometchat/chat-uikit-react-native'; + +const LISTENER_ID = 'incoming-call'; + +/** + * Rings for a call while the app is open — init() sets ringInForeground: false, so the + * package leaves this to the app. CometChatIncomingCall accepts the call and shows the call + * screen itself; this component shows it when a call arrives and removes it when the call is + * declined, cancelled by the caller, or ends. + */ +export function IncomingCall() { + const [call, setCall] = useState(null); + + useEffect(() => { + CometChat.addCallListener( + LISTENER_ID, + new CometChat.CallListener({ + onIncomingCallReceived: (incoming: CometChat.Call) => setCall(incoming), + onIncomingCallCancelled: () => setCall(null), // the caller hung up while it was ringing + }), + ); + // An accepted call ended. + CometChatUIEventHandler.addCallListener(LISTENER_ID, { ccCallEnded: () => setCall(null) }); + + return () => { + CometChat.removeCallListener(LISTENER_ID); + CometChatUIEventHandler.removeCallListener(LISTENER_ID); + }; + }, []); + + if (!call) return null; + return setCall(null)} />; +} +``` + + +Your app already shows an incoming-call screen while it's open? Keep it and skip this file. Don't want one? Set `ringInForeground: true` — the default — and skip this file: the package then rings with the system call UI while the app is open, too. + + -Not using the UI Kit? Delete the `@cometchat/chat-uikit-react-native` import and the `emitMessageEvent` block in `markConversationRead`, point `SCREENS` and the route params at your own screens, and in `logout.ts` call `CometChat.logout()` instead of `CometChatUIKit.logout()`. +Not using the UI Kit? Delete the `@cometchat/chat-uikit-react-native` import and the `emitMessageEvent` block in `markConversationRead`, point `SCREENS` and the route params at your own screens, and in `logout.ts` call `CometChat.logout()` instead of `CometChatUIKit.logout()`. For calls while the app is open, set `ringInForeground: true`, or build your own incoming-call screen on `CometChat.addCallListener` in place of `IncomingCall.tsx`. -**`App.tsx`** — call `usePushOnLogin()` once, in a component that renders **after** CometChat is initialized, and pass `navigationRef` to your `NavigationContainer`: +**`App.tsx`** — call `usePushOnLogin()` once, in a component that renders **after** CometChat is initialized, pass `navigationRef` to your `NavigationContainer`, and render `` **before** your navigator: ```tsx App.tsx lines import React, { useEffect, useState } from 'react'; @@ -330,6 +374,7 @@ import { CometChatUIKit, UIKitSettings } from '@cometchat/chat-uikit-react-nativ import { AppCredentials } from './AppCredentials'; import { navigationRef } from './navigation/navigationRef'; import { usePushOnLogin } from './push/pushNotifications'; +import { IncomingCall } from './calls/IncomingCall'; export default function App() { const [initialized, setInitialized] = useState(false); @@ -356,13 +401,14 @@ function Root() { return ( + {/* before your navigator: it shows at the top, and an accepted call fills the screen */} {/* your existing navigator */} ); } ``` -`usePushOnLogin()` starts push after a fresh login **and** when a session is restored on launch, and removes the handlers when the user logs out — so a later login never registers them twice. +`usePushOnLogin()` starts push after a fresh login **and** when a session is restored on launch, and removes the handlers when the user logs out — so a later login never registers them twice. `` goes before your navigator because the UI Kit's incoming-call screen isn't a modal: rendered first, it shows at the top of the screen, and an accepted call fills the screen. ### 3.3 Align dependencies and configuration @@ -371,7 +417,7 @@ function Root() { - `fcmProviderId` (Android) and `apnsProviderId` (iOS) — from step 1. - `notificationSmallIcon` — the Android status-bar icon. - `showInForeground` (default `false`) — show chat notifications while the app is open. - - `ringInForeground` (default `true`) — ring with the system call UI while the app is open. Set `false` when your app shows its own incoming-call screen, as the UI Kit does. + - `ringInForeground` (default `true`) — ring with the system call UI while the app is open. With `false`, a call that arrives while the app is open is left to your app, so your app must show its own incoming-call screen — `IncomingCall.tsx` above. With `false` and no such screen, calls don't ring while the app is open. - `voip` (default `true`), `androidChannelId`, `androidChannelName`. ## 4. Configure the native Android layer @@ -546,7 +592,7 @@ await CometChatPushNotifications.registerToken('fcm', token); // or 'apns' / 'vo | Event | What happens | | --- | --- | -| **Call push arrives** | App in the background or killed: the package starts a phone-call foreground service with the full-screen ringing screen, over the lock screen too. App open: it rings the same way unless `ringInForeground` is `false`. The ring stops itself after 45 seconds if nothing ends it. | +| **Call push arrives** | App in the background or killed: the package starts a phone-call foreground service with the full-screen ringing screen, over the lock screen too. App open: it rings the same way unless `ringInForeground` is `false` — then your app's `IncomingCall` screen rings instead. The ring stops itself after 45 seconds if nothing ends it. | | **Accept** | The app opens and the package accepts the call through the Chat SDK; then `onCallAccepted` fires and `openCallScreen` opens your call screen. | | **Decline** | The package rejects the call through the Chat SDK. In a **fully killed** app the background task registered in `index.js` does it, so the caller sees the call rejected right away. | | **Caller hangs up** | A cancel push stops the ring and `onCallEnded` fires; `endCall` tears the call down and leaves the call screen. | @@ -650,7 +696,7 @@ Use physical devices and a **release** build: a debug build loads its JavaScript - **Decline** shows the call as rejected on the caller's side. - The caller **cancelling** stops the ring. 4. **Calls, app in the background:** the ringing screen shows, and accept and decline both work. -5. **Calls, app open:** only your in-app incoming-call screen rings (`ringInForeground: false`). +5. **Calls, app open** (`ringInForeground: false`): your in-app incoming-call screen rings, not the system call UI. **Accept** opens the call full-screen with audio both ways; **Decline** shows the call as rejected on the caller's side; the caller **hanging up** removes the screen. 6. **Logout:** log out, send a message from another user — nothing arrives. Log in as another user — only that user's notifications arrive. 7. **OEM devices** (MIUI, Oppo, Vivo): grant the step 4.4 permissions and re-check locked and killed calls. @@ -665,6 +711,7 @@ Use physical devices and a **release** build: a debug build loads its JavaScript | Declining a call in a killed app doesn't reject it | `registerBackgroundCallTask()` is called at module scope in `index.js`, and the app has been opened and logged in once since installing (so `init()` saved the Chat SDK settings). Test on a release build. | | Token registration errors | The provider IDs match the dashboard exactly, and `usePushOnLogin()` is rendered after CometChat is initialized. | | No notification while the app is open | Expected with `showInForeground: false` (the default) — set it to `true`. For calls, `ringInForeground` decides whether the system call UI or your in-app screen rings. | +| A call doesn't ring while the app is open | `ringInForeground` is `false`, so your app must ring: render `` before your navigator (see *Wire the entry points*), or set `ringInForeground: true`. | | Tapping a notification opens the app but not the conversation | `navigationRef` is passed to your `NavigationContainer`, navigation goes through `navigate()` from `navigationRef.ts`, and the route names in `SCREENS` match your navigator. | | Thread reply opens an empty thread screen | The thread screen is given the user or group as well as the parent message, as `openFromNotification` does. | | Handlers fire twice after logging out and in | Use `usePushOnLogin()` rather than calling `setupPushOnLogin()` directly — its cleanup must run on logout. | diff --git a/notifications/react-native-push-notifications-ios.mdx b/notifications/react-native-push-notifications-ios.mdx index 5bebb86fb..ac222cbbd 100644 --- a/notifications/react-native-push-notifications-ios.mdx +++ b/notifications/react-native-push-notifications-ios.mdx @@ -205,7 +205,7 @@ export function setupPushOnLogin(): () => void { apnsProviderId: AppCredentials.apnsProviderId, // iOS (APNs device + VoIP) notificationSmallIcon: 'ic_notification', // Android status-bar icon showInForeground: true, // one notification while the app is open, too - ringInForeground: false, // the UI Kit shows its own incoming-call screen while open + ringInForeground: false, // your app rings while it's open — see src/calls/IncomingCall.tsx }); }; start().catch(error => console.log('Push setup failed:', error)); @@ -306,11 +306,55 @@ function endCall(info: PNCallEndEvent): void { } ``` +**`src/calls/IncomingCall.tsx`** — `init()` above sets `ringInForeground: false`, so **while the app is open the package doesn't ring: your app must show its own incoming-call screen**, or calls won't ring at all while it's open. Calls reach an open app over the Chat SDK's connection; this component listens for them and shows the UI Kit's `CometChatIncomingCall`, which accepts the call and shows the call screen itself: + +```tsx src/calls/IncomingCall.tsx lines +import React, { useEffect, useState } from 'react'; +import { CometChat } from '@cometchat/chat-sdk-react-native'; +import { CometChatIncomingCall, CometChatUIEventHandler } from '@cometchat/chat-uikit-react-native'; + +const LISTENER_ID = 'incoming-call'; + +/** + * Rings for a call while the app is open — init() sets ringInForeground: false, so the + * package leaves this to the app. CometChatIncomingCall accepts the call and shows the call + * screen itself; this component shows it when a call arrives and removes it when the call is + * declined, cancelled by the caller, or ends. + */ +export function IncomingCall() { + const [call, setCall] = useState(null); + + useEffect(() => { + CometChat.addCallListener( + LISTENER_ID, + new CometChat.CallListener({ + onIncomingCallReceived: (incoming: CometChat.Call) => setCall(incoming), + onIncomingCallCancelled: () => setCall(null), // the caller hung up while it was ringing + }), + ); + // An accepted call ended. + CometChatUIEventHandler.addCallListener(LISTENER_ID, { ccCallEnded: () => setCall(null) }); + + return () => { + CometChat.removeCallListener(LISTENER_ID); + CometChatUIEventHandler.removeCallListener(LISTENER_ID); + }; + }, []); + + if (!call) return null; + return setCall(null)} />; +} +``` + + +Your app already shows an incoming-call screen while it's open? Keep it and skip this file. Don't want one? Set `ringInForeground: true` — the default — and skip this file: the package then rings with the system call UI while the app is open, too. + + -Not using the UI Kit? Delete the `@cometchat/chat-uikit-react-native` import and the `emitMessageEvent` block in `markConversationRead`, point `SCREENS` and the route params at your own screens, and in `logout.ts` call `CometChat.logout()` instead of `CometChatUIKit.logout()`. +Not using the UI Kit? Delete the `@cometchat/chat-uikit-react-native` import and the `emitMessageEvent` block in `markConversationRead`, point `SCREENS` and the route params at your own screens, and in `logout.ts` call `CometChat.logout()` instead of `CometChatUIKit.logout()`. For calls while the app is open, set `ringInForeground: true`, or build your own incoming-call screen on `CometChat.addCallListener` in place of `IncomingCall.tsx`. -**`App.tsx`** — call `usePushOnLogin()` once, in a component that renders **after** CometChat is initialized, and pass `navigationRef` to your `NavigationContainer`: +**`App.tsx`** — call `usePushOnLogin()` once, in a component that renders **after** CometChat is initialized, pass `navigationRef` to your `NavigationContainer`, and render `` **before** your navigator: ```tsx App.tsx lines import React, { useEffect, useState } from 'react'; @@ -321,6 +365,7 @@ import { CometChatUIKit, UIKitSettings } from '@cometchat/chat-uikit-react-nativ import { AppCredentials } from './AppCredentials'; import { navigationRef } from './navigation/navigationRef'; import { usePushOnLogin } from './push/pushNotifications'; +import { IncomingCall } from './calls/IncomingCall'; export default function App() { const [initialized, setInitialized] = useState(false); @@ -347,13 +392,14 @@ function Root() { return ( + {/* before your navigator: it shows at the top, and an accepted call fills the screen */} {/* your existing navigator */} ); } ``` -`usePushOnLogin()` starts push after a fresh login **and** when a session is restored on launch, and removes the handlers when the user logs out — so a later login never registers them twice. +`usePushOnLogin()` starts push after a fresh login **and** when a session is restored on launch, and removes the handlers when the user logs out — so a later login never registers them twice. `` goes before your navigator because the UI Kit's incoming-call screen isn't a modal: rendered first, it shows at the top of the screen, and an accepted call fills the screen. ### 4.3 Align dependencies and configuration @@ -362,7 +408,7 @@ function Root() { - `fcmProviderId` (Android) and `apnsProviderId` (iOS) — from step 1. - `notificationSmallIcon` — the Android status-bar icon. - `showInForeground` (default `false`) — show chat notifications while the app is open. - - `ringInForeground` (default `true`) — ring with the system call UI while the app is open. Set `false` when your app shows its own incoming-call screen, as the UI Kit does. + - `ringInForeground` (default `true`) — ring with the system call UI while the app is open. With `false`, a call that arrives while the app is open is left to your app, so your app must show its own incoming-call screen — `IncomingCall.tsx` above. With `false` and no such screen, calls don't ring while the app is open. - `voip` (default `true`), `androidChannelId`, `androidChannelName`. ## 5. Configure the native iOS layer @@ -489,7 +535,7 @@ You don't write PushKit or CallKit code, and you don't set a `UNUserNotification | Event | What happens | | --- | --- | -| **VoIP push arrives** | The package reports it to CallKit immediately, in every app state, as iOS requires. App open with `ringInForeground: false`: it reports the call and ends it at once, and your in-app screen rings instead. | +| **VoIP push arrives** | The package reports it to CallKit immediately, in every app state, as iOS requires. App open with `ringInForeground: false`: it reports the call and ends it at once, and your app's `IncomingCall` screen rings instead. | | **Accept** | The package puts the audio session in call mode, accepts the call through the Chat SDK, and hands CallKit's audio to WebRTC; then `onCallAccepted` fires and `openCallScreen` opens your call screen. | | **Decline** | The package rejects the call through the Chat SDK. In a killed app, iOS has already launched the app for the VoIP push, and the package keeps it running long enough to reject. | | **Caller hangs up** | The cancel VoIP push ends the CallKit call and `onCallEnded` fires. | @@ -590,7 +636,7 @@ Use a physical iPhone — the Simulator can't receive APNs or VoIP pushes — an - **Decline** shows the call as rejected on the caller's side. - The caller **cancelling** stops the ring. 4. **Calls, app in the background:** CallKit rings, and ending the call from the iOS call screen closes your call screen. -5. **Calls, app open:** only your in-app incoming-call screen rings (`ringInForeground: false`). +5. **Calls, app open** (`ringInForeground: false`): your in-app incoming-call screen rings, not the system call UI. **Accept** opens the call full-screen with audio both ways; **Decline** shows the call as rejected on the caller's side; the caller **hanging up** removes the screen. 6. **Logout:** log out, send a message from another user — nothing arrives. Log in as another user — only that user's notifications arrive. ## 9. Troubleshooting tips @@ -603,6 +649,7 @@ Use a physical iPhone — the Simulator can't receive APNs or VoIP pushes — an | Accepted call connects but has no audio | The **Audio** background mode is enabled, and the Calls SDK (with `react-native-webrtc`) is installed. | | Token registration errors | The provider IDs match the dashboard exactly, and `usePushOnLogin()` is rendered after CometChat is initialized. | | No notification while the app is open | Expected with `showInForeground: false` (the default) — set it to `true`. For calls, `ringInForeground` decides whether the system call UI or your in-app screen rings. | +| A call doesn't ring while the app is open | `ringInForeground` is `false`, so your app must ring: render `` before your navigator (see *Wire the entry points*), or set `ringInForeground: true`. | | Tapping a notification opens the app but not the conversation | `navigationRef` is passed to your `NavigationContainer`, navigation goes through `navigate()` from `navigationRef.ts`, and the route names in `SCREENS` match your navigator. | | Thread reply opens an empty thread screen | The thread screen is given the user or group as well as the parent message, as `openFromNotification` does. | | Handlers fire twice after logging out and in | Use `usePushOnLogin()` rather than calling `setupPushOnLogin()` directly — its cleanup must run on logout. | From 55c764d10ef091f483d38c9b6d7251c9ac718c58 Mon Sep 17 00:00:00 2001 From: Suraj Chauhan Date: Fri, 18 Sep 2026 17:41:15 +0530 Subject: [PATCH 19/20] =?UTF-8?q?docs(rn-push):=20match=20the=20published?= =?UTF-8?q?=201.0.0=20=E2=80=94=20versions,=20voip:=20false,=20iOS=2026.4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Prerequisites on both pages: React Native 0.78+ and @cometchat/chat-sdk-react-native 4.0.10+, the package's peer ranges. - voip: false turns calls off for a chat-only app; the Android chat-only note passes it, and iOS gets a chat-only note (skip registerForVoIPPushes() and the VoIP and Audio background modes). - iOS: with the app open and ringInForeground: false, iOS 26.4+ no longer needs the CallKit report, so the package leaves CallKit out; earlier iOS still reports the call and ends it at once. Co-Authored-By: Claude Opus 5 --- .../react-native-push-notifications-android.mdx | 7 ++++--- notifications/react-native-push-notifications-ios.mdx | 11 ++++++++--- 2 files changed, 12 insertions(+), 6 deletions(-) diff --git a/notifications/react-native-push-notifications-android.mdx b/notifications/react-native-push-notifications-android.mdx index 1a9ec3441..4ba6e6121 100644 --- a/notifications/react-native-push-notifications-android.mdx +++ b/notifications/react-native-push-notifications-android.mdx @@ -12,7 +12,7 @@ description: "CometChat push notifications and VoIP calls in React Native apps o | Key APIs | `CometChatPushNotifications.init()`, `onNotificationTap()`, `onCallAccepted()`, `onCallEnded()`, `unregister()`, `registerBackgroundCallTask()`, `CometChatPNHelper.requestNotificationPermission()` | | Push Platform | `FCM_REACT_NATIVE_ANDROID`, registered by `init()` with `fcmProviderId` | | Native setup | `google-services.json` + Google Services plugin, `minSdkVersion 24`, an `ic_notification` drawable. No manifest or Kotlin changes | -| Prerequisites | CometChat initialized and the user logged in before `init()`, an FCM provider ID, a physical device for call tests | +| Prerequisites | React Native 0.78 or later and `@cometchat/chat-sdk-react-native` 4.0.10 or later; CometChat initialized and the user logged in before `init()`, an FCM provider ID, a physical device for call tests | @@ -418,7 +418,8 @@ function Root() { - `notificationSmallIcon` — the Android status-bar icon. - `showInForeground` (default `false`) — show chat notifications while the app is open. - `ringInForeground` (default `true`) — ring with the system call UI while the app is open. With `false`, a call that arrives while the app is open is left to your app, so your app must show its own incoming-call screen — `IncomingCall.tsx` above. With `false` and no such screen, calls don't ring while the app is open. - - `voip` (default `true`), `androidChannelId`, `androidChannelName`. + - `voip` (default `true`) — `false` turns calls off for a chat-only app: no call rings, and iOS doesn't register the VoIP token. + - `androidChannelId`, `androidChannelName` — the Android notification channel. ## 4. Configure the native Android layer @@ -452,7 +453,7 @@ You don't edit `AndroidManifest.xml`. The package's manifest is merged into your **Google Play (Android 14+):** apps with calls complete two declarations in Play Console under **App content** — **Full-screen intent permission** (calling as core functionality) and **Foreground service permissions** (the **Phone call** type). Without the first, Play revokes `USE_FULL_SCREEN_INTENT` and calls ring only as a heads-up notification. -**Chat-only apps** remove the call permissions and components (with `xmlns:tools="http://schemas.android.com/tools"` on the `manifest` element), and skip `requestCallPermissions()` and `registerBackgroundCallTask()`: +**Chat-only apps** pass `voip: false` to `init()` so call pushes never ring, remove the call permissions and components (with `xmlns:tools="http://schemas.android.com/tools"` on the `manifest` element), and skip `requestCallPermissions()` and `registerBackgroundCallTask()`: ```xml android/app/src/main/AndroidManifest.xml lines diff --git a/notifications/react-native-push-notifications-ios.mdx b/notifications/react-native-push-notifications-ios.mdx index ac222cbbd..e25b351af 100644 --- a/notifications/react-native-push-notifications-ios.mdx +++ b/notifications/react-native-push-notifications-ios.mdx @@ -12,7 +12,7 @@ description: "CometChat push notifications and VoIP calls in React Native apps o | Key APIs | `CometChatPushNotifications.init()`, `onNotificationTap()`, `onCallAccepted()`, `onCallEnded()`, `unregister()`, `CometChatPushNotificationsAppDelegate.registerForVoIPPushes()`, `CometChatPushNotificationsAppDelegate.didRegisterAPNsToken(_:)` | | Push Platforms | `APNS_REACT_NATIVE_DEVICE` and `APNS_REACT_NATIVE_VOIP`, registered by `init()` with one `apnsProviderId` | | Native setup | Push Notifications + Background Modes (Voice over IP, Remote notifications, Audio), microphone and camera usage strings, and in `AppDelegate`: `registerForVoIPPushes()` before React Native starts plus the APNs token method | -| Prerequisites | CometChat initialized and the user logged in before `init()`, an APNs provider ID, a physical device | +| Prerequisites | React Native 0.78 or later and `@cometchat/chat-sdk-react-native` 4.0.10 or later; CometChat initialized and the user logged in before `init()`, an APNs provider ID, a physical device | @@ -409,7 +409,8 @@ function Root() { - `notificationSmallIcon` — the Android status-bar icon. - `showInForeground` (default `false`) — show chat notifications while the app is open. - `ringInForeground` (default `true`) — ring with the system call UI while the app is open. With `false`, a call that arrives while the app is open is left to your app, so your app must show its own incoming-call screen — `IncomingCall.tsx` above. With `false` and no such screen, calls don't ring while the app is open. - - `voip` (default `true`), `androidChannelId`, `androidChannelName`. + - `voip` (default `true`) — `false` turns calls off for a chat-only app: no call rings, and iOS doesn't register the VoIP token. + - `androidChannelId`, `androidChannelName` — the Android notification channel. ## 5. Configure the native iOS layer @@ -511,6 +512,10 @@ You don't write PushKit or CallKit code, and you don't set a `UNUserNotification **Don't create a `PKPushRegistry` of your own.** The package owns it, and a second registry — yours or a library's — makes iOS deliver every VoIP push twice. If another library must own PushKit, skip `registerForVoIPPushes()` and forward that registry's `didUpdate` and `didReceiveIncomingPushWith` callbacks to `CometChatPushNotificationsAppDelegate.didUpdateVoIPToken(_:)` and `.didReceiveIncomingVoIPPush(_:)`, calling `completion()` after it. + +**Chat-only apps** skip `registerForVoIPPushes()` and the *Voice over IP* and *Audio* background modes, and pass `voip: false` to `init()`. + + **Older Swift template** (an `RCTAppDelegate` subclass): call `registerForVoIPPushes()` before `return super.application(...)`, which starts React Native, and add the same token method. **Objective-C `AppDelegate.mm`:** the package's iOS entry points are Swift-only, so move the AppDelegate to Swift first — the [React Native Upgrade Helper](https://react-native-community.github.io/upgrade-helper/) shows the change. @@ -535,7 +540,7 @@ You don't write PushKit or CallKit code, and you don't set a `UNUserNotification | Event | What happens | | --- | --- | -| **VoIP push arrives** | The package reports it to CallKit immediately, in every app state, as iOS requires. App open with `ringInForeground: false`: it reports the call and ends it at once, and your app's `IncomingCall` screen rings instead. | +| **VoIP push arrives** | The package reports the call to CallKit, which rings, in every app state — except while the app is open with `ringInForeground: false`, when your app's `IncomingCall` screen rings instead. Then, on iOS 26.4 and later, the package leaves CallKit out, since iOS no longer requires the report while the app is open; on earlier iOS it still reports the call, as iOS requires, and ends it at once. | | **Accept** | The package puts the audio session in call mode, accepts the call through the Chat SDK, and hands CallKit's audio to WebRTC; then `onCallAccepted` fires and `openCallScreen` opens your call screen. | | **Decline** | The package rejects the call through the Chat SDK. In a killed app, iOS has already launched the app for the VoIP push, and the package keeps it running long enough to reject. | | **Caller hangs up** | The cancel VoIP push ends the CallKit call and `onCallEnded` fires. | From ced9998d195e38c3d86ac443d7d1884b852be3ed Mon Sep 17 00:00:00 2001 From: Suraj Chauhan Date: Sat, 19 Sep 2026 11:54:42 +0530 Subject: [PATCH 20/20] docs(rn-push): add ERR_PN_CONFIGURE to Android troubleshooting init() rejects with ERR_PN_CONFIGURE when the package's native setup throws (CometChatPushNotificationsModule.configure), for example when Firebase isn't initialized. It isn't a permission-request rejection, so it goes in Troubleshooting rather than the requestNotificationPermission() table. Android only: the iOS side has no rejection codes. Co-Authored-By: Claude Opus 5 --- notifications/react-native-push-notifications-android.mdx | 1 + 1 file changed, 1 insertion(+) diff --git a/notifications/react-native-push-notifications-android.mdx b/notifications/react-native-push-notifications-android.mdx index 4ba6e6121..a05bada63 100644 --- a/notifications/react-native-push-notifications-android.mdx +++ b/notifications/react-native-push-notifications-android.mdx @@ -706,6 +706,7 @@ Use physical devices and a **release** build: a debug build loads its JavaScript | Symptom | Quick checks | | --- | --- | | No notifications received | `google-services.json` is in `android/app`, its package name matches the app, the Google Services plugin is applied, and `POST_NOTIFICATIONS` is granted (Android 13+). | +| `init()` rejects with `ERR_PN_CONFIGURE` | The native setup failed, and the error message says why. If it mentions `FirebaseApp`, Firebase isn't initialized: `google-services.json` must be in `android/app` and the Google Services plugin applied (step 4.1). | | Notifications work on an existing install but not on a fresh one | A permission request wasn't awaited, so the notification dialog was cancelled unseen (step 5.1). `adb shell dumpsys package \| grep POST_NOTIFICATIONS`: `granted=false` with no `USER_SET` flag means the dialog was never answered. | | Two notifications for one message | Another push library is still installed (step 3.1). With `@react-native-firebase/messaging`, follow step 4.5. | | Incoming call is a plain notification, not the full-screen ringing screen | The merged manifest still has `MANAGE_OWN_CALLS` and `FOREGROUND_SERVICE_PHONE_CALL`. On MIUI, Oppo and Vivo, grant the step 4.4 permissions. From Google Play, complete the full-screen intent declaration (step 4.2). |