From 926d08233a00e6f7604ac4bbd96357098c125d9f Mon Sep 17 00:00:00 2001 From: Suraj Chauhan Date: Fri, 28 Aug 2026 12:35:34 +0530 Subject: [PATCH 1/6] 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 2/6] 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 3/6] 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 4/6] 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 5/6] 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 6/6] 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`. |