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..522c620b3 --- /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-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-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 + +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-overview)): + + 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